From a6956b00a8055ff4bbc364a9ab9c13a945efbbf5 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Wed, 15 Oct 2025 20:44:17 -0600 Subject: [PATCH 01/44] feat: enhance wideBytes method for unbiased scalar generation and update RandomScalar to use rejection sampling by default --- src/curve.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/curve.ts b/src/curve.ts index abf42bf..9bc8848 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -38,9 +38,14 @@ class RailJubCurvePoint { } wideBytes () { - const wideBytes = randomBytes(48) - const wideBigInt = leBufferToBigInt(wideBytes) - return this.modOrder(wideBigInt) + const nBytes = 48 + const R = 1n << BigInt(nBytes * 8) + const limit = R - (R % this.order) + while (true) { + const bytes = randomBytes(nBytes) + const x = leBufferToBigInt(bytes) + if (x < limit) return x % this.order + } } Order () { @@ -51,7 +56,9 @@ class RailJubCurvePoint { return this.identity } - RandomScalar (rejectionSampling = false): bigint { + RandomScalar (rejectionSampling = true): bigint { + // RFC 9591: Scalars must be uniformly sampled in [0, order-1] + // Use rejection sampling by default to avoid modulo reduction bias. return rejectionSampling ? this.rejectionSampling() : this.wideBytes() } From 6324d4bb673fab3318048082ee65ee7c62aced36 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Wed, 15 Oct 2025 21:15:09 -0600 Subject: [PATCH 02/44] feat: refactor BabyFrostVSSDKG to replace mulPointEscalar with ScalarBaseMult and ScalarMult for improved clarity --- src/frost/vss-dkg.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/frost/vss-dkg.ts b/src/frost/vss-dkg.ts index bacac75..1bd1078 100644 --- a/src/frost/vss-dkg.ts +++ b/src/frost/vss-dkg.ts @@ -6,7 +6,7 @@ import { gcm } from '@noble/ciphers/aes.js' import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' -import { addPoint, mulPointEscalar } from '@zk-kit/baby-jubjub' +import { addPoint } from '@zk-kit/baby-jubjub' import { poseidon3, poseidon4 } from 'poseidon-lite' import { RailJubCurvePoint } from '../curve.js' @@ -26,9 +26,8 @@ function asPoint (p: { x: bigint; y: bigint } | Point): Point { class BabyFrostVSSDKG extends RailJubCurvePoint { // domain tags (as scalars) for clear separation - COEFF_DOMAIN = 0x636f6566n /* 'coef' */ - VIEW_DOMAIN = 0x76696577n /* 'view' */ - ENC_DOMAIN = 0x656e636en /* 'encn' */ + COEFF_DOMAIN = 0x636f6566n /* 'coef' */ + VIEW_DOMAIN = 0x76696577n /* 'view' */ // ---------- math ---------- /** Evaluate polynomial (coeffs in mod L) at x=id in mod L. */ @@ -91,7 +90,7 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { return coeffsL.map((aj) => { const s = this.modOrder(aj) // Base8 * s; stays in prime-order subgroup - return mulPointEscalar(this.generator, s) + return this.ScalarBaseMult(s) }) } @@ -107,13 +106,13 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { /** Check each dealer's C0 ∈ subgroup and aggregate the group public key A = ∑_k C_{k,0}. */ combineGroupPubkeyFromCommitments (allDealerCommitments: Point[][]): Point { - if (!allDealerCommitments.length) throw new Error('no dealer commitments') - let acc: Point = this.identity + if (!allDealerCommitments.length) throw new Error('no dealer commitments') + let acc: Point = this.Identity() for (const dealerCom of allDealerCommitments) { if (!dealerCom?.length) throw new Error('dealer missing commitments') const P0 = dealerCom[0]! // subgroup check: SUBORDER * P0 == 0 - if (!this.pointsEqual(mulPointEscalar(P0, this.order), this.identity)) { + if (!this.pointsEqual(this.ScalarMult(P0, this.order), this.Identity())) { throw new Error('C0 not in subgroup') } acc = addPoint(acc, P0) @@ -132,17 +131,18 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { if (!commitments?.length) return false // LHS = Base8 * s_i - const LHS = mulPointEscalar(this.generator, this.modOrder(sk_i)) + const LHS = this.ScalarBaseMult(this.modOrder(sk_i)) // RHS = Σ_j C_j * (id^j) - let RHS: Point = this.identity + let RHS: Point = this.Identity() let pow = 1n const idL = this.modOrder(BigInt(id)) for (const CjRaw of commitments) { + // can remove this check/formatting no longer using x.y noble points const Cj = asPoint(CjRaw) // Cj must be in subgroup - if (!this.pointsEqual(mulPointEscalar(Cj, this.order), this.identity)) return false - RHS = addPoint(RHS, mulPointEscalar(Cj, pow)) + if (!this.pointsEqual(this.ScalarMult(Cj, this.order), this.Identity())) return false + RHS = addPoint(RHS, this.ScalarMult(Cj, pow)) pow = this.modOrder(pow * idL) } return this.pointsEqual(LHS, RHS) From a1abd22efe538b6731981922b0d7f6512e19e9bb Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Thu, 16 Oct 2025 21:00:09 -0600 Subject: [PATCH 03/44] feat: implement RFC9591Hasher for improved hashing functions and refactor BabyFrostVSSDKG to utilize new hasher --- src/frost/babyfrost.ts | 36 ++----- src/frost/rfc-hashes.ts | 88 ---------------- src/frost/vss-dkg.ts | 194 ++++++++++++++++++++++++++++++++---- src/hashing.ts | 72 +++++++++++++ test/hashes.test.ts | 13 ++- test/vss-extensions.test.ts | 126 +++++++++++++++++++++++ test/vss.test.ts | 12 +-- 7 files changed, 392 insertions(+), 149 deletions(-) delete mode 100644 src/frost/rfc-hashes.ts create mode 100644 src/hashing.ts create mode 100644 test/vss-extensions.test.ts diff --git a/src/frost/babyfrost.ts b/src/frost/babyfrost.ts index 80046b6..63c92bf 100644 --- a/src/frost/babyfrost.ts +++ b/src/frost/babyfrost.ts @@ -1,57 +1,41 @@ /* eslint-disable camelcase, jsdoc/require-jsdoc */ -import { blake2b } from '@noble/hashes/blake2.js' import type { Point } from '@zk-kit/baby-jubjub' import { addPoint } from '@zk-kit/baby-jubjub' -import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' -import { poseidon5 } from 'poseidon-lite' import { RailJubCurvePoint } from '../curve.js' +import RFC9591Hasher from '../hashing.js' import type { BindingFactor, Bindings, Commitment, NoncePair } from './types.js' class BabyFROST extends RailJubCurvePoint { public readonly contextString = 'FROST-EDBABYJUJUB-BLAKE512-v1' + hasher: RFC9591Hasher - Hash (m: Uint8Array): Uint8Array { - return blake2b(m, { dkLen: 64 }) - } - - taggedBlake512 (tag: string, input: any) { - // Convert strings to buffers if needed - const prefixBuf = Buffer.from(this.contextString, 'utf8') - const tagBuf = Buffer.from(tag, 'utf8') - const inputBuf = Buffer.isBuffer(input) ? input : Buffer.from(input) - // Concatenate: prefix + tag + input - const combined = Buffer.concat([prefixBuf, tagBuf, inputBuf]) - return this.Hash(combined) - } - - deriveHashed (tag: string, input: Uint8Array): bigint { - const digest = this.taggedBlake512(tag, input) // 64 bytes - const kLE = leBigIntToBuffer(leBufferToBigInt(digest), 64) // interpret as LE bigint - return this.modOrder(leBufferToBigInt(kLE)) // reduce to scalar field + constructor () { + super() + this.hasher = new RFC9591Hasher(this.contextString, this.order) } H1 (m: Uint8Array): bigint { - return this.deriveHashed('rho', m) + return this.hasher.H1(m) } // H2: challenge computation compatible with eddsaBuild.verifyPoseidon H2 (R8: Point, A: Point, msgHash: bigint): bigint { - return this.modOrder(poseidon5([...R8, ...A, msgHash])) + return this.hasher.H2(R8, A, msgHash) } H3 (m: Uint8Array): bigint { - return this.deriveHashed('nonce', m) + return this.hasher.H3(m) } H4 (m: Uint8Array): bigint { - return this.deriveHashed('msg', m) + return this.hasher.H4(m) } H5 (m: Uint8Array): bigint { - return this.deriveHashed('com', m) + return this.hasher.H5(m) } // section 4.1 helper functions start diff --git a/src/frost/rfc-hashes.ts b/src/frost/rfc-hashes.ts deleted file mode 100644 index c4aaea5..0000000 --- a/src/frost/rfc-hashes.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable jsdoc/require-jsdoc */ -// Tagged Blake512 hash for FROST EdDSA Baby Jubjub - -import type { WithImplicitCoercion } from 'buffer' - -import { blake2b } from '@noble/hashes/blake2.js' -import { subOrder } from '@zk-kit/baby-jubjub' -import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' - -const mod = (a: bigint, n: bigint) => { - const r = a % n - return r >= 0n ? r : r + n -} - -/** - * Creates a tagged Blake512 hash for FROST EdDSA Baby Jubjub operations. - * @param tag - The tag string to include in the hash - * @param input - The input data to hash (string or Buffer) - * @returns The Blake512 hash digest as a Buffer (64 bytes) - */ -function taggedBlake512 (tag: WithImplicitCoercion, input: any) { - const prefix = 'FROST-EDBABYJUJUB-BLAKE512-v1' - - // Convert strings to buffers if needed - const prefixBuf = Buffer.from(prefix, 'utf8') - const tagBuf = Buffer.from(tag, 'utf8') - const inputBuf = Buffer.isBuffer(input) ? input : Buffer.from(input) - - // Concatenate: prefix + tag + input - const combined = Buffer.concat([prefixBuf, tagBuf, inputBuf]) - - return blake2b(combined, { dkLen: 64 }) -} - -function deriveHashed (tag: string, input: Uint8Array): bigint { - const digest = taggedBlake512(tag, input) // 64 bytes - const kLE = leBigIntToBuffer(leBufferToBigInt(digest), 64) // interpret as LE bigint - return mod(leBufferToBigInt(kLE), subOrder) // reduce to scalar field -} - -/** - * Computes H1 hash function for FROST using tagged Blake512 - * @param input - The input data to hash - * @returns The hash result modulo baby jubjub order - */ -function H1 (input: any) { - return deriveHashed('rho', input) - // const babyJubOrder = BigInt('2736030358979909402780800718157159386076813972158567259200215660948447373041') -} - -/** - * Computes H3 hash function for FROST nonce generation using tagged Blake512 - * @param input - The input data to hash - * @returns The hash result modulo baby jubjub order - */ -function H3 (input: any) { - return deriveHashed('nonce', input) - - // const hashedNonce = Scalar.fromRprLE(taggedBlake512('nonce', input), 0, 64) - // // const babyJubOrder = BigInt('2736030358979909402780800718157159386076813972158567259200215660948447373041') - // return Scalar.mod(hashedNonce, subOrder) -} - -/** - * Computes H4 hash function for FROST message hashing using tagged Blake512 - * @param input - The input data to hash - * @returns The hash result modulo baby jubjub order - */ -function H4 (input: any) { - return deriveHashed('msg', input) - // const hashedNonce = Scalar.fromRprLE(taggedBlake512('msg', input), 0, 64) - // // const babyJubOrder = BigInt('2736030358979909402780800718157159386076813972158567259200215660948447373041') - // return Scalar.mod(hashedNonce, subOrder) -} - -/** - * Computes H5 hash function for FROST commitment hashing using tagged Blake512 - * @param input - The input data to hash - * @returns The hash result modulo baby jubjub order - */ -function H5 (input: any) { - return deriveHashed('com', input) - // const hashedNonce = Scalar.fromRprLE(taggedBlake512('com', input), 0, 64) - // // const babyJubOrder = BigInt('2736030358979909402780800718157159386076813972158567259200215660948447373041') - // return Scalar.mod(hashedNonce, subOrder) -} - -export { deriveHashed, taggedBlake512, H1, H3, H4, H5 } diff --git a/src/frost/vss-dkg.ts b/src/frost/vss-dkg.ts index 1bd1078..9a31c51 100644 --- a/src/frost/vss-dkg.ts +++ b/src/frost/vss-dkg.ts @@ -7,11 +7,10 @@ import { gcm } from '@noble/ciphers/aes.js' import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' import { addPoint } from '@zk-kit/baby-jubjub' -import { poseidon3, poseidon4 } from 'poseidon-lite' - -import { RailJubCurvePoint } from '../curve.js' import type { EncryptedShare, ParticipantInput, Share } from './types.js' +import { RailJubCurvePoint } from '../curve.js' +import RFC9591Hasher from '../hashing.js' function objToU8 (o: Record | Uint8Array): Uint8Array { if (o instanceof Uint8Array) return o @@ -20,14 +19,14 @@ function objToU8 (o: Record | Uint8Array): Uint8Array { return out } -function asPoint (p: { x: bigint; y: bigint } | Point): Point { - return Array.isArray(p) ? p : [p.x, p.y] -} - class BabyFrostVSSDKG extends RailJubCurvePoint { - // domain tags (as scalars) for clear separation - COEFF_DOMAIN = 0x636f6566n /* 'coef' */ - VIEW_DOMAIN = 0x76696577n /* 'view' */ + public readonly contextString = 'FROST-EDBABYJUJUB-BLAKE512-v1' + hasher: RFC9591Hasher + + constructor () { + super() + this.hasher = new RFC9591Hasher(this.contextString, this.order) + } // ---------- math ---------- /** Evaluate polynomial (coeffs in mod L) at x=id in mod L. */ @@ -59,7 +58,8 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { let acc = 0n for (const C0 of C0s) { // hash the affine coordinates + domain; interpret as scalar then accumulate in modL - const v_k = this.modOrder(poseidon3([...C0, this.VIEW_DOMAIN])) + const v = this.hasher.H3(this.SerializeElement(C0)) + const v_k = this.modOrder(v) acc = this.modOrder(acc + v_k) } return this.toBytes(acc === 0n ? 1n : acc) // avoid zero key @@ -73,13 +73,26 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { if (threshold <= 0) throw new Error('threshold must be > 0') const a: bigint[] = initialState.slice() if (a.length === 0) { - const a0 = this.modOrder(poseidon4([BigInt(p.id), 0n, this.fromBytes(p.seed), this.fromBytes(p.password)]) + this.COEFF_DOMAIN) + const input = Buffer.concat([ + this.toBytes(BigInt(p.id)), + this.toBytes(0n), + p.seed, + p.password + ]) + const c0 = this.hasher.H1(input) + const a0 = this.modOrder(c0) if (a0 === 0n) throw new Error('a0 must be non-zero') a.push(a0) } for (let j = a.length; j < threshold; j++) { - const c = poseidon4([BigInt(p.id), BigInt(j), this.fromBytes(p.seed), this.fromBytes(p.password)]) - a.push(this.modOrder(c + this.COEFF_DOMAIN)) + const input = Buffer.concat([ + this.toBytes(BigInt(p.id)), + this.toBytes(BigInt(j)), + p.seed, + p.password + ]) + const c = this.hasher.H1(input) + a.push(this.modOrder(c)) } if (a.length !== threshold) throw new Error('incorrect coefficient length') return a @@ -96,6 +109,7 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { /** Compute evaluations s_k(i) for recipients i. */ computeDealerSharesForIds (coeffsL: bigint[], recipientIds: number[]): Record { + BabyFrostVSSDKG.assertSortedConsecutiveIds(recipientIds) const out: Record = {} for (const id of recipientIds) { if (!Number.isInteger(id) || id <= 0) throw new Error(`bad recipient id: ${id}`) @@ -107,6 +121,7 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { /** Check each dealer's C0 ∈ subgroup and aggregate the group public key A = ∑_k C_{k,0}. */ combineGroupPubkeyFromCommitments (allDealerCommitments: Point[][]): Point { if (!allDealerCommitments.length) throw new Error('no dealer commitments') + this.verifyCommitmentsShape(allDealerCommitments) let acc: Point = this.Identity() for (const dealerCom of allDealerCommitments) { if (!dealerCom?.length) throw new Error('dealer missing commitments') @@ -120,7 +135,47 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { return acc } - /** Verify Feldman share s_i against commitments. */ + private verifyCommitmentsShape (allDealerCommitments: Point[][]): number { + const lens = allDealerCommitments.map((c) => (c?.length ?? 0)) + if (lens.length === 0) throw new Error('no dealer commitments') + if (lens.some((l) => l <= 0)) throw new Error('dealer missing commitments') + const t = lens[0]! + for (const l of lens) { + if (l !== t) throw new Error('commitment degree mismatch across dealers') + } + return t + } + + verifyAllCommitmentsSubgroup (allDealerCommitments: Point[][]): boolean { + if (!allDealerCommitments.length) return false + for (const dealerCom of allDealerCommitments) { + if (!dealerCom?.length) return false + for (const Cj of dealerCom) { + if (!this.pointsEqual(this.ScalarMult(Cj, this.order), this.Identity())) return false + } + } + return true + } + + commitmentsDigest (allDealerCommitments: Point[][]): bigint { + const enc = this.encodeCommitmentsBytes(allDealerCommitments) + return this.hasher.H5(enc) + } + + private encodeCommitmentsBytes (allDealerCommitments: Point[][]): Uint8Array { + const dealers = allDealerCommitments.slice().filter(a => a?.length) + dealers.sort((a, b) => (this.sortKey(a[0]!) < this.sortKey(b[0]!) ? -1 : 1)) + let out = new Uint8Array() + for (const dealer of dealers) { + for (const Cj of dealer) { + const enc = this.SerializeElement(Cj) + out = Buffer.concat([out, enc]) + } + } + return out + } + + /** Verify Feldman share sk_i against commitments. */ verifyFeldmanShare ( id: number, sk_i: bigint, @@ -137,9 +192,7 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { let RHS: Point = this.Identity() let pow = 1n const idL = this.modOrder(BigInt(id)) - for (const CjRaw of commitments) { - // can remove this check/formatting no longer using x.y noble points - const Cj = asPoint(CjRaw) + for (const Cj of commitments) { // Cj must be in subgroup if (!this.pointsEqual(this.ScalarMult(Cj, this.order), this.Identity())) return false RHS = addPoint(RHS, this.ScalarMult(Cj, pow)) @@ -166,6 +219,11 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { if (!s_ki_byDealer.length || s_ki_byDealer.length !== allDealerCommitments.length) { throw new Error('dealer/share count mismatch') } + // Normalize and validate dealerIds: strictly increasing positive integers (order of shares does not affect sum) + const dealerIds = s_ki_byDealer.map((d) => d.dealerId) + const sortedDealerIds = dealerIds.slice().sort((a, b) => a - b) + BabyFrostVSSDKG.assertSortedConsecutiveIds(sortedDealerIds) + // s_i = Σ_k s_{k}(i) (mod L) let s_i = 0n for (const { s_ki } of s_ki_byDealer) s_i = this.modOrder(s_i + this.modOrder(s_ki)) @@ -188,6 +246,8 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { shares: Record, keyById: Record ): Record { + const ids = Object.keys(shares).map(Number).sort((a, b) => a - b) + BabyFrostVSSDKG.assertSortedConsecutiveIds(ids) const out: Record = {} for (const [idStr, s] of Object.entries(shares)) { const id = Number(idStr) @@ -205,6 +265,33 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { return out } + /** + * AES-GCM encryption with AAD binding: participant id || commitmentsDigest. + */ + encryptSharesAESGCMWithAAD ( + shares: Record, + keyById: Record, + allDealerCommitments: Point[][] + ): Record { + const ids = Object.keys(shares).map(Number).sort((a, b) => a - b) + BabyFrostVSSDKG.assertSortedConsecutiveIds(ids) + const out: Record = {} + const digest = this.commitmentsDigest(allDealerCommitments) + const digestBytes = this.toBytes(digest) + for (const [idStr, s] of Object.entries(shares)) { + const id = Number(idStr) + const key = keyById[id] + if (!key || key.length !== 32) throw new Error(`bad AES key for id ${id}`) + const nonce = randomBytes(12) + const pt = this.toBytes(this.modOrder(s)) + const aad = Buffer.concat([this.SerializeScalar(BigInt(id)), digestBytes]) + const aead = gcm(key, nonce, aad) + const ciphertext = aead.encrypt(pt) + out[id] = { nonce, ciphertext } + } + return out + } + decryptShareAESGCM ( enc: EncryptedShare, keyBytes: Uint8Array @@ -220,22 +307,85 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { return this.fromBytes(pt) } - // ---------- Extra invariants / utilities ---------- - /** Optional: assert commitments degree matches threshold. */ + /** Decrypt with AAD binding (must match the context used in encryptSharesAESGCMWithAAD). */ + decryptShareAESGCMWithAAD ( + enc: EncryptedShare, + keyBytes: Uint8Array, + participantId: number, + allDealerCommitments: Point[][] + ): bigint { + if (keyBytes.length !== 32) throw new Error('bad AES key length') + if (!Number.isInteger(participantId) || participantId <= 0) throw new Error('bad participant id') + const nonce = objToU8(enc.nonce as any) + const ct = objToU8(enc.ciphertext as any) + if (nonce.length !== 12) throw new Error('bad nonce length (expected 12)') + if (ct.length < 16) throw new Error('bad ciphertext (must include 16B tag)') + const digest = this.commitmentsDigest(allDealerCommitments) + const aad = Buffer.concat([this.SerializeScalar(BigInt(participantId)), this.toBytes(digest)]) + const aead = gcm(keyBytes, nonce, aad) + const pt = aead.decrypt(ct) + return this.fromBytes(pt) + } + assertCommitmentDegree (commitments: Point[], threshold: number) { if (commitments.length !== threshold) throw new Error('commitment degree mismatch') } - /** Optional: sanity check unique positive participant ids. */ static assertUniqueRecipientIds (ids: number[]) { const s = new Set(ids) if (ids.some((i) => !Number.isInteger(i) || i <= 0) || s.size !== ids.length) { throw new Error('bad or duplicate recipient ids') } } + + static assertSortedConsecutiveIds (ids: number[]) { + if (!ids.length) throw new Error('empty recipient id list') + if (ids.some((id) => !Number.isInteger(id) || id <= 0)) { + throw new Error('recipient ids must be strictly increasing positive integers (self may be excluded)') + } + for (let i = 1; i < ids.length; i++) { + if (ids[i]! <= ids[i - 1]!) { + throw new Error('recipient ids must be strictly increasing positive integers (self may be excluded)') + } + } + } + + private invMod (a: bigint, n: bigint): bigint { + let t = 0n; let newT = 1n + let r = n; let newR = this.modOrder(a) + while (newR !== 0n) { + const q = r / newR + ;[t, newT] = [newT, t - q * newT] + ;[r, newR] = [newR, r - q * newR] + } + if (r !== 1n) throw new Error('inverse does not exist') + return t < 0n ? t + n : t + } + + lagrangeBasisAtZero (ids: bigint[], x_i: bigint): bigint { + if (!ids.find((v) => v === x_i)) throw new Error('invalid parameters') + let num = 1n + let den = 1n + for (const x_j of ids) { + if (x_j === x_i) continue + num = this.modOrder(num * this.modOrder(0n - x_j)) + den = this.modOrder(den * this.modOrder(x_i - x_j)) + } + return this.modOrder(num * this.invMod(den, this.order)) + } + + reconstructConstantFromShares (subset: { id: number; s_i: bigint }[]): bigint { + if (!subset.length) throw new Error('no shares') + const ids = subset.map((s) => this.modOrder(BigInt(s.id))) + let a0 = 0n + for (const { id, s_i } of subset) { + const lambda = this.lagrangeBasisAtZero(ids, this.modOrder(BigInt(id))) + a0 = this.modOrder(a0 + this.modOrder(s_i) * lambda) + } + return a0 + } } -// Singleton export (like your original) const vss = new BabyFrostVSSDKG() export { vss } diff --git a/src/hashing.ts b/src/hashing.ts new file mode 100644 index 0000000..08abf62 --- /dev/null +++ b/src/hashing.ts @@ -0,0 +1,72 @@ +/* eslint-disable jsdoc/require-jsdoc */ + +import { blake2b } from '@noble/hashes/blake2.js' +import type { Point } from '@zk-kit/baby-jubjub' +import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' +import { poseidon5 } from 'poseidon-lite' + +type HashFn = (m: Uint8Array) => Uint8Array +// add hash functions? +function blake2BWrapper (m: Uint8Array) { + return blake2b(m, { dkLen: 64 }) +} + +// will initialize in class used +class RFC9591Hasher { + public readonly contextString: string + private readonly hashFn: HashFn + private readonly order: bigint + constructor (contextString: string, order: bigint, hashFn?: HashFn) { + this.contextString = contextString + this.hashFn = hashFn ?? blake2BWrapper + this.order = order + } + + mod (x: bigint) { + const r = x % this.order + return r < 0n ? r + this.order : r + } + + Hash (m: Uint8Array): Uint8Array { + return this.hashFn(m) + } + + taggedHash (tag: string, input: any) { + // Convert strings to buffers if needed + const prefixBuf = Buffer.from(this.contextString, 'utf8') + const tagBuf = Buffer.from(tag, 'utf8') + const inputBuf = Buffer.isBuffer(input) ? input : Buffer.from(input) + // Concatenate: prefix + tag + input + const combined = Buffer.concat([prefixBuf, tagBuf, inputBuf]) + return this.Hash(combined) + } + + deriveHashed (tag: string, input: Uint8Array): bigint { + const digest = this.taggedHash(tag, input) // 64 bytes + const kLE = leBigIntToBuffer(leBufferToBigInt(digest), 64) // interpret as LE bigint + return this.mod(leBufferToBigInt(kLE)) // reduce to scalar field + } + + H1 (m: Uint8Array): bigint { + return this.deriveHashed('rho', m) + } + + // H2: challenge computation compatible with eddsaBuild.verifyPoseidon + H2 (R8: Point, A: Point, msgHash: bigint): bigint { + return this.mod(poseidon5([...R8, ...A, msgHash])) + } + + H3 (m: Uint8Array): bigint { + return this.deriveHashed('nonce', m) + } + + H4 (m: Uint8Array): bigint { + return this.deriveHashed('msg', m) + } + + H5 (m: Uint8Array): bigint { + return this.deriveHashed('com', m) + } +} + +export default RFC9591Hasher diff --git a/test/hashes.test.ts b/test/hashes.test.ts index d7bcb70..dd5761e 100644 --- a/test/hashes.test.ts +++ b/test/hashes.test.ts @@ -2,8 +2,9 @@ import assert from 'node:assert' import { describe, it } from 'node:test' -import {taggedBlake512, H1, H3, H4, H5} from '../src/frost/rfc-hashes' import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js' +import RFC9591Hasher from '../src/hashing'; +import { subOrder } from '@zk-kit/baby-jubjub'; function toHex(buffer: Uint8Array) { return Buffer.from(buffer).toString('hex'); @@ -41,7 +42,9 @@ describe('RFC9591 H1 H3 H4 H5 test vectors', () => { it('Match expectations', () => { // Generate random 32 bytes input - const funcs = [H1, H3, H4, H5] + const hasher = new RFC9591Hasher('FROST-EDBABYJUJUB-BLAKE512-v1', subOrder) + + const funcs = [hasher.H1.bind(hasher), hasher.H3.bind(hasher), hasher.H4.bind(hasher), hasher.H5.bind(hasher)] const names: ('H1'|'H3'|'H4'|'H5')[] = ['H1', 'H3', 'H4', 'H5'] const tags = ['rho', 'nonce', 'msg', 'com'] const results: bigint[] = [] @@ -59,7 +62,7 @@ describe('RFC9591 H1 H3 H4 H5 test vectors', () => { const tag = tags[idx] console.log(`${name} (tag: '${tag}'):`) const expected = VECTORS[name] - const raw = taggedBlake512(tag, randomInput) + const raw = hasher.taggedHash(tag, randomInput) assert.equal(bytesToHex(raw), expected.rawHex, 'invalid blake512 result.') console.log(" Raw hash (64 bytes):", toHex(raw)); const result = fn(randomInput) @@ -75,7 +78,7 @@ describe('RFC9591 H1 H3 H4 H5 test vectors', () => { // Verify that different tags produce different results console.log("=== Verification ==="); console.log("All hash results are different:", - results[0] !== results[2] && + results[0] !== results[2] && results[0] !== results[3] && results[0] !== results[4] && results[2] !== results[3] && @@ -84,7 +87,7 @@ describe('RFC9591 H1 H3 H4 H5 test vectors', () => { ); // Test with same tag twice to verify consistency - const h1Again = H1(randomInput); + const h1Again = hasher.H1(randomInput); console.log("H1 is deterministic:", results[0] === h1Again ? "✓ PASS" : "✗ FAIL"); }) diff --git a/test/vss-extensions.test.ts b/test/vss-extensions.test.ts new file mode 100644 index 0000000..0992ad4 --- /dev/null +++ b/test/vss-extensions.test.ts @@ -0,0 +1,126 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import assert from 'node:assert' +import { describe, it } from 'node:test' + +import type { Point } from '@zk-kit/baby-jubjub' + +import BabyFrostVSSDKG from '../src/frost/vss-dkg' + +describe('VSS-DKG RFC-like extensions', () => { + const vss = new BabyFrostVSSDKG() + + // simple deterministic participants + const participants = [ + { id: 1, seed: Buffer.alloc(32, 0x11), password: Buffer.alloc(32, 0xaa) }, + { id: 2, seed: Buffer.alloc(32, 0x22), password: Buffer.alloc(32, 0xbb) }, + { id: 3, seed: Buffer.alloc(32, 0x33), password: Buffer.alloc(32, 0xcc) }, + ] + + function setupDealers (threshold = 2) { + const coeffs = participants.map(p => vss.makeDealerCoeffsDeterministic(p as any, threshold)) + const commitments: Point[][] = coeffs.map(cs => vss.makeDealerCommitments(cs)) + const ids = participants.map(p => p.id) + const sharesByDealer: Record> = {} + coeffs.forEach((c, idx) => { + sharesByDealer[participants[idx]!.id] = vss.computeDealerSharesForIds(c, ids) + }) + return { coeffs, commitments, sharesByDealer } + } + + it('commitmentsDigest is deterministic for same commitments', () => { + const { commitments } = setupDealers(2) + const d1 = vss.commitmentsDigest(commitments) + const d2 = vss.commitmentsDigest(commitments) + assert.equal(d1, d2, 'digest should be deterministic') + }) + + it('verifyAllCommitmentsSubgroup passes for valid commitments', () => { + const { commitments } = setupDealers(2) + const ok = vss.verifyAllCommitmentsSubgroup(commitments) + assert.strictEqual(ok, true) + }) + + it('AES-GCM with AAD round-trip per dealer share (s_{k,i}), then aggregate per participant', () => { + const { commitments, sharesByDealer } = setupDealers(2) + // toy keys: deterministic 32B per participant id + const keyById: Record = {} + for (const p of participants) keyById[p.id] = Buffer.alloc(32, p.id) + + // encrypt per-dealer share maps and verify decrypt equals original per-dealer shares + const decryptedByDealer: Record> = {} + for (const dealerIdStr of Object.keys(sharesByDealer)) { + const dealerId = Number(dealerIdStr) + const sharesMap = sharesByDealer[dealerId]! + const encMap = vss.encryptSharesAESGCMWithAAD(sharesMap, keyById, commitments) + decryptedByDealer[dealerId] = {} + for (const p of participants) { + const dec = vss.decryptShareAESGCMWithAAD(encMap[p.id]!, keyById[p.id]!, p.id, commitments) + assert.equal(dec, sharesMap[p.id], `AAD decrypt mismatch for dealer ${dealerId}, id ${p.id}`) + decryptedByDealer[dealerId]![p.id] = dec + } + } + + // aggregate per participant after decrypt using library helper + for (const p of participants) { + const s_ki_byDealer_decrypted = Object.keys(decryptedByDealer) + .map((dealerIdStr) => ({ + dealerId: Number(dealerIdStr), + s_ki: decryptedByDealer[Number(dealerIdStr)!][p.id]! + })) + + const s_ki_byDealer_original = Object.keys(sharesByDealer) + .map((dealerIdStr) => ({ + dealerId: Number(dealerIdStr), + s_ki: sharesByDealer[Number(dealerIdStr)!][p.id]! + })) + + const resDecrypted = vss.finalizeVSSForParticipant(p.id, s_ki_byDealer_decrypted, commitments) + const resOriginal = vss.finalizeVSSForParticipant(p.id, s_ki_byDealer_original, commitments) + + assert.equal(resDecrypted.share.skShareDiv8, resOriginal.share.skShareDiv8, `Aggregated share mismatch for id ${p.id}`) + assert.equal(resDecrypted.share.skShare, resOriginal.share.skShare, `Aggregated share (x8) mismatch for id ${p.id}`) + // sanity: PK group must be identical regardless of share source + assert.equal( + vss.pointsEqual(resDecrypted.PKGroup, resOriginal.PKGroup), + true, + 'PKGroup mismatch' + ) + } + }) + + it('accepts non-consecutive but rejects non-increasing/invalid ids in computeDealerSharesForIds', () => { + const threshold = 2 + const coeffs = vss.makeDealerCoeffsDeterministic(participants[0] as any, threshold) + // non-consecutive is fine when self is excluded; should not throw + assert.doesNotThrow(() => vss.computeDealerSharesForIds(coeffs, [1, 3])) + // unsorted must throw + assert.throws(() => vss.computeDealerSharesForIds(coeffs, [2, 1]), /strictly increasing positive integers/i) + // non-positive must throw + assert.throws(() => vss.computeDealerSharesForIds(coeffs, [0, 2]), /strictly increasing positive integers/i) + }) + + it('AAD encryption allows non-consecutive ids; rejects invalid ids', () => { + const { commitments } = setupDealers(2) + const sharesOK: Record = { 2: 1n, 4: 2n } + const keyByIdOK: Record = { 2: Buffer.alloc(32, 2), 4: Buffer.alloc(32, 4) } + assert.doesNotThrow(() => vss.encryptSharesAESGCMWithAAD(sharesOK, keyByIdOK, commitments)) + const sharesBad: Record = { 0: 1n } + const keyByIdBad: Record = { 0: Buffer.alloc(32, 0) } + assert.throws(() => vss.encryptSharesAESGCMWithAAD(sharesBad, keyByIdBad, commitments), /strictly increasing positive integers/i) + }) + + it('Lagrange basis sums to 1 at 0, and reconstructs constant term', () => { + const a0 = vss.RandomScalar() + const a1 = vss.RandomScalar() + const ids = participants.map(p => BigInt(p.id)) + const sById: { id: number; s_i: bigint }[] = participants.map(p => ({ + id: p.id, + s_i: vss.modOrder(a0 + a1 * BigInt(p.id)) + })) + const lambdas = ids.map(id => vss.lagrangeBasisAtZero(ids as any, id)) + const sum = lambdas.reduce((acc, l) => vss.modOrder(acc + l), 0n) + assert.equal(sum, 1n, 'sum of λ_i(0) should be 1') + const rec = vss.reconstructConstantFromShares(sById.slice(0, 2)) + assert.equal(rec, a0, 'reconstructed a0 should equal original') + }) +}) diff --git a/test/vss.test.ts b/test/vss.test.ts index c12aae9..bf670e5 100644 --- a/test/vss.test.ts +++ b/test/vss.test.ts @@ -6,7 +6,8 @@ import { describe, it } from 'node:test' import type { Point } from '@zk-kit/baby-jubjub' import { eddsaBuild } from '../src/eddsa' -import BabyFROST, { type Commitment } from '../src/frost/babyfrost' +import type { Commitment } from '../src/frost/types' +import BabyFROST from '../src/frost/babyfrost' import type { ParticipantInput, Share } from '../src/frost/types' import BabyFrostVSSDKG from '../src/frost/vss-dkg' import { @@ -26,12 +27,7 @@ describe('VSS DKG: aggregator learns nothing; ≥t can reconstruct, Feldman Veri // Reconstruct master secret a0 (T(0)) from exactly `threshold` pairs (id, s_i) function reconstructA0FromShares (subset: { id: number; s_i: bigint }[], threshold: number): bigint { if (subset.length < threshold) throw new Error('insufficient shares to reconstruct') - const ids = subset.map(s => s.id) - let a0 = 0n - for (const { id, s_i } of subset) { - a0 = vss.modOrder(a0 + vss.modOrder(s_i) * frost.deriveInterpolatingValue( ids.map(BigInt), BigInt(id))) - } - return a0 // subgroup scalar + return vss.reconstructConstantFromShares(subset) } it('3 parties, t=2: verify shares, finalize, and demonstrate threshold property', () => { @@ -86,7 +82,7 @@ describe('VSS DKG: aggregator learns nothing; ≥t can reconstruct, Feldman Veri /insufficient shares/i, 'should not reconstruct with fewer than threshold shares') - // // any t shares can reconstruct the master secret a0 (threshold property) + // any t shares can reconstruct the master secret a0 (threshold property) const a0_12 = reconstructA0FromShares( [{ id: 1, s_i: map[1]!.skShareDiv8 }, { id: 2, s_i: map[2]!.skShareDiv8 }], threshold From 727233f8f517ea123638bb3aff206b45891060ec Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Thu, 16 Oct 2025 21:20:04 -0600 Subject: [PATCH 04/44] refactor: use test-vector inputs from trusted-dkg --- test/babyfrost.test.ts | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/test/babyfrost.test.ts b/test/babyfrost.test.ts index e0ed8c3..10c169e 100644 --- a/test/babyfrost.test.ts +++ b/test/babyfrost.test.ts @@ -4,42 +4,49 @@ import { describe, it } from 'node:test' import type { Commitment, Point } from '../src' import { eddsaBuild, poseidonHex } from '../src' import BabyFROST_RFC9591 from '../src/frost/babyfrost' +import { subOrder } from '@zk-kit/baby-jubjub' + +// can skip the mod purely for tests, it passes without. +function mod (x: bigint) { + const r = x % subOrder + return r < 0n ? r + subOrder : r +} const multiSigVector = [ { id: 1, share: { id: 1, - skShare: 9379043151059132348915826074748660163067085078692438895653970025055239912688n, - skShareDiv8: 1172380393882391543614478259343582520383385634836554861956746253131904989086n + skShare: mod(BigInt('0x1d4260025e6e520d8daab9c1f9923b0c94c6ee643f051ff2526517535133804') * 8n), + skShareDiv8: BigInt('0x1d4260025e6e520d8daab9c1f9923b0c94c6ee643f051ff2526517535133804') }, PKGroup: [ - 13572882520115239477456596963790473057624212656185529673136940543510856063202n, - 12515170913156463601814218712397355783209899258093010091034332015438200954857n + BigInt('0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6'), + BigInt('0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd') ] }, { id: 2, share: { id: 2, - skShare: 6288919410652533324466074240073604195793794349351903202901181227799867607360n, - skShareDiv8: 786114926331566665558259280009200524474224293668987900362647653474983450920n + skShare: mod(BigInt('0x1d85f8d8e472e07cf9fb432c0debae008ff241ea77f69c1d8403ffb36343cf0') * 8n), + skShareDiv8: BigInt('0x1d85f8d8e472e07cf9fb432c0debae008ff241ea77f69c1d8403ffb36343cf0') }, PKGroup: [ - 13572882520115239477456596963790473057624212656185529673136940543510856063202n, - 12515170913156463601814218712397355783209899258093010091034332015438200954857n + BigInt('0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6'), + BigInt('0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd') ] }, { id: 3, share: { id: 3, - skShare: 3198795670245934300016322405398548228520503620011367510148392430544495302032n, - skShareDiv8: 399849458780741787502040300674818528565062952501420938768549053818061912754n + skShare: mod(BigInt('0x442308b78d3cf348e735520c35d77dfb70167d82e334d911afbd7f02497c653') * 8n), + skShareDiv8: BigInt('0x442308b78d3cf348e735520c35d77dfb70167d82e334d911afbd7f02497c653') }, PKGroup: [ - 13572882520115239477456596963790473057624212656185529673136940543510856063202n, - 12515170913156463601814218712397355783209899258093010091034332015438200954857n + BigInt('0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6'), + BigInt('0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd') ] } ] From 78602d06822b3f78db5870e5eeedaa0527a0eb2b Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Thu, 16 Oct 2025 21:31:02 -0600 Subject: [PATCH 05/44] refactor: rename lagrangeBasisAtZero to deriveInterpolatingValue for clarity and update references --- src/frost/vss-dkg.ts | 28 +++++++++------------------- test/vss-extensions.test.ts | 2 +- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/frost/vss-dkg.ts b/src/frost/vss-dkg.ts index 9a31c51..e2477b8 100644 --- a/src/frost/vss-dkg.ts +++ b/src/frost/vss-dkg.ts @@ -350,28 +350,18 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { } } - private invMod (a: bigint, n: bigint): bigint { - let t = 0n; let newT = 1n - let r = n; let newR = this.modOrder(a) - while (newR !== 0n) { - const q = r / newR - ;[t, newT] = [newT, t - q * newT] - ;[r, newR] = [newR, r - q * newR] - } - if (r !== 1n) throw new Error('inverse does not exist') - return t < 0n ? t + n : t - } - - lagrangeBasisAtZero (ids: bigint[], x_i: bigint): bigint { - if (!ids.find((v) => v === x_i)) throw new Error('invalid parameters') + deriveInterpolatingValue (ids: bigint[], x_i: bigint): bigint { + const found = ids.find(a => { return a === x_i }) + if (!found) throw new Error('invalid parameters') let num = 1n - let den = 1n + let dom = 1n for (const x_j of ids) { if (x_j === x_i) continue - num = this.modOrder(num * this.modOrder(0n - x_j)) - den = this.modOrder(den * this.modOrder(x_i - x_j)) + num *= x_j + dom *= x_j - x_i } - return this.modOrder(num * this.invMod(den, this.order)) + const value = num / dom + return value } reconstructConstantFromShares (subset: { id: number; s_i: bigint }[]): bigint { @@ -379,7 +369,7 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { const ids = subset.map((s) => this.modOrder(BigInt(s.id))) let a0 = 0n for (const { id, s_i } of subset) { - const lambda = this.lagrangeBasisAtZero(ids, this.modOrder(BigInt(id))) + const lambda = this.deriveInterpolatingValue(ids, this.modOrder(BigInt(id))) a0 = this.modOrder(a0 + this.modOrder(s_i) * lambda) } return a0 diff --git a/test/vss-extensions.test.ts b/test/vss-extensions.test.ts index 0992ad4..a58798f 100644 --- a/test/vss-extensions.test.ts +++ b/test/vss-extensions.test.ts @@ -117,7 +117,7 @@ describe('VSS-DKG RFC-like extensions', () => { id: p.id, s_i: vss.modOrder(a0 + a1 * BigInt(p.id)) })) - const lambdas = ids.map(id => vss.lagrangeBasisAtZero(ids as any, id)) + const lambdas = ids.map(id => vss.deriveInterpolatingValue(ids as any, id)) const sum = lambdas.reduce((acc, l) => vss.modOrder(acc + l), 0n) assert.equal(sum, 1n, 'sum of λ_i(0) should be 1') const rec = vss.reconstructConstantFromShares(sById.slice(0, 2)) From e0dfca97c6382b7025382376604260f77164bfe4 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 17 Oct 2025 19:34:58 -0600 Subject: [PATCH 06/44] feat: add inverse modulo order function and enhance BabyFrostVSSDKG with new hashing methods and reconstruction tests --- src/curve.ts | 13 +++ src/frost/trusted-dkg.ts | 167 +++++++++++++++++++++++++++++++++++- src/frost/vss-dkg.ts | 16 ++-- src/hashing.ts | 8 ++ test/vss-extensions.test.ts | 16 ++++ 5 files changed, 212 insertions(+), 8 deletions(-) diff --git a/src/curve.ts b/src/curve.ts index 9bc8848..74fd86c 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -134,6 +134,19 @@ class RailJubCurvePoint { return r < 0n ? r + this.order : r } + invModOrder (a: bigint): bigint { + let t = 0n; let newT = 1n + let r = this.order; let newR = this.modOrder(a) + while (newR !== 0n) { + const q = r / newR + ;[t, newT] = [newT, t - q * newT] + ;[r, newR] = [newR, r - q * newR] + } + if (r !== 1n) throw new Error('inverse does not exist') + if (t < 0n) t += this.order + return t + } + pruneBuffer (buff: Uint8Array) { const out = new Uint8Array(buff) out[0]! &= 0xf8 diff --git a/src/frost/trusted-dkg.ts b/src/frost/trusted-dkg.ts index 00f416c..93525c5 100644 --- a/src/frost/trusted-dkg.ts +++ b/src/frost/trusted-dkg.ts @@ -1,11 +1,22 @@ /* eslint-disable camelcase */ /* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable jsdoc/require-param */ +/* eslint-disable jsdoc/require-returns */ import type { Point } from '@zk-kit/baby-jubjub' import { addPoint, mulPointEscalar } from '@zk-kit/baby-jubjub' import { RailJubCurvePoint } from '../curve' +import RFC9591Hasher from '../hashing' class TrustedDKG extends RailJubCurvePoint { + public readonly contextString = 'FROST-EDBABYJUJUB-BLAKE512-v1' + hasher: RFC9591Hasher + + constructor () { + super() + this.hasher = new RFC9591Hasher(this.contextString, this.order) + } + // Appendix C.1.1 polynomialEvaluate (x: bigint, coefficients: bigint[]) { let value = 0n @@ -80,7 +91,161 @@ class TrustedDKG extends RailJubCurvePoint { } participantPublicKeys.push(PK_i) } - return { PK, participantPublicKeys } + const viewingPrivateKey = new Uint8Array(this.deriveViewKeyFromPK([vssCommitment])) + return { PK, participantPublicKeys, viewingPrivateKey } + } + + // coordinator-less style helpers (similar to vss-dkg) + static assertSortedConsecutiveIds (ids: number[]) { + if (!ids.length) throw new Error('empty recipient id list') + if (ids.some((id) => !Number.isInteger(id) || id <= 0)) { + throw new Error('recipient ids must be strictly increasing positive integers (self may be excluded)') + } + for (let i = 1; i < ids.length; i++) { + if (ids[i]! <= ids[i - 1]!) { + throw new Error('recipient ids must be strictly increasing positive integers (self may be excluded)') + } + } + } + + private verifyCommitmentsShape (allDealerCommitments: Point[][]): number { + const lens = allDealerCommitments.map((c) => (c?.length ?? 0)) + if (lens.length === 0) throw new Error('no dealer commitments') + if (lens.some((l) => l <= 0)) throw new Error('dealer missing commitments') + const t = lens[0]! + for (const l of lens) { + if (l !== t) throw new Error('commitment degree mismatch across dealers') + } + return t + } + + computeSharesForIds (coefficients: bigint[], recipientIds: number[]): Record { + TrustedDKG.assertSortedConsecutiveIds(recipientIds) + const out: Record = {} + for (const id of recipientIds) { + if (!Number.isInteger(id) || id <= 0) throw new Error(`bad recipient id: ${id}`) + out[id] = this.polynomialEvaluate(BigInt(id), coefficients) + } + return out + } + + combineGroupPubkeyFromCommitments (allDealerCommitments: Point[][]): Point { + if (!allDealerCommitments.length) throw new Error('no dealer commitments') + this.verifyCommitmentsShape(allDealerCommitments) + let acc: Point = this.Identity() + for (const dealerCom of allDealerCommitments) { + if (!dealerCom?.length) throw new Error('dealer missing commitments') + const P0 = dealerCom[0]! + if (!this.pointsEqual(this.ScalarMult(P0, this.order), this.Identity())) { + throw new Error('C0 not in subgroup') + } + acc = addPoint(acc, P0) + } + return acc + } + + verifyAllCommitmentsSubgroup (allDealerCommitments: Point[][]): boolean { + if (!allDealerCommitments.length) return false + for (const dealerCom of allDealerCommitments) { + if (!dealerCom?.length) return false + for (const Cj of dealerCom) { + if (!this.pointsEqual(this.ScalarMult(Cj, this.order), this.Identity())) return false + } + } + return true + } + + verifyFeldmanShare ( + id: number, + sk_i: bigint, + commitments: Array> + ): boolean { + try { + if (!Number.isInteger(id) || id <= 0) return false + if (!commitments?.length) return false + + const LHS = this.ScalarBaseMult(this.modOrder(sk_i)) + + let RHS: Point = this.Identity() + let pow = 1n + const idL = this.modOrder(BigInt(id)) + for (const Cj of commitments) { + if (!this.pointsEqual(this.ScalarMult(Cj, this.order), this.Identity())) return false + RHS = addPoint(RHS, this.ScalarMult(Cj, pow)) + pow = this.modOrder(pow * idL) + } + return this.pointsEqual(LHS, RHS) + } catch { + return false + } + } + + deriveInterpolatingValue (ids: bigint[], x_i: bigint): bigint { + const found = ids.find(a => { return a === x_i }) + if (!found) throw new Error('invalid parameters') + let num = 1n + let dom = 1n + for (const x_j of ids) { + if (x_j === x_i) continue + num = this.modOrder(num * this.modOrder(x_j)) + dom = this.modOrder(dom * this.modOrder(x_j - x_i)) + } + const invDom = this.invModOrder(dom) + return this.modOrder(num * invDom) + } + + reconstructConstantFromShares (subset: { id: number; s_i: bigint }[]): bigint { + if (!subset.length) throw new Error('no shares') + const ids = subset.map((s) => this.modOrder(BigInt(s.id))) + let a0 = 0n + for (const { id, s_i } of subset) { + const lambda = this.deriveInterpolatingValue(ids, this.modOrder(BigInt(id))) + a0 = this.modOrder(a0 + this.modOrder(s_i) * lambda) + } + return a0 + } + + private sortKey = (P: Point) => + `${P[0].toString(16).padStart(64, '0')}:${P[1].toString(16).padStart(64, '0')}` + + deriveViewKeyFromPK (allDealerCommitments: Point[][]): Uint8Array { + const C0s: Point[] = [] + for (const dealerComms of allDealerCommitments) { + if (!dealerComms?.length) continue + C0s.push(dealerComms[0]!) + } + C0s.sort((a, b) => (this.sortKey(a) < this.sortKey(b) ? -1 : this.sortKey(a) > this.sortKey(b) ? 1 : 0)) + let acc = 0n + for (const C0 of C0s) { + // hash the affine coordinates + domain; interpret as scalar then accumulate in modL + const v = this.hasher.H7(this.SerializeElement(C0)) + const v_k = this.modOrder(v) + acc = this.modOrder(acc + v_k) + } + return this.toBytes(acc === 0n ? 1n : acc) // avoid zero key + } + + finalizeParticipant ( + participantId: number, + s_ki_byDealer: Array<{ dealerId: number; s_ki: bigint }>, + allDealerCommitments: Point[][] + ): { share: { id: number; skShare: bigint; skShareDiv8: bigint }; PKGroup: Point, viewingPrivateKey: Uint8Array } { + if (!Number.isInteger(participantId) || participantId <= 0) { + throw new Error('bad participantId') + } + if (!s_ki_byDealer.length || s_ki_byDealer.length !== allDealerCommitments.length) { + throw new Error('dealer/share count mismatch') + } + const dealerIds = s_ki_byDealer.map((d) => d.dealerId).slice().sort((a, b) => a - b) + TrustedDKG.assertSortedConsecutiveIds(dealerIds) + let s_i = 0n + for (const { s_ki } of s_ki_byDealer) s_i = this.modOrder(s_i + this.modOrder(s_ki)) + const skShareDiv8 = s_i + const skShare = this.modOrder(8n * skShareDiv8) + const share = { id: participantId, skShare, skShareDiv8 } + const viewingPrivateKey = this.deriveViewKeyFromPK(allDealerCommitments) + const PKGroup = this.combineGroupPubkeyFromCommitments(allDealerCommitments) + return { share, PKGroup, viewingPrivateKey } } } diff --git a/src/frost/vss-dkg.ts b/src/frost/vss-dkg.ts index e2477b8..4714a5c 100644 --- a/src/frost/vss-dkg.ts +++ b/src/frost/vss-dkg.ts @@ -58,7 +58,7 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { let acc = 0n for (const C0 of C0s) { // hash the affine coordinates + domain; interpret as scalar then accumulate in modL - const v = this.hasher.H3(this.SerializeElement(C0)) + const v = this.hasher.H7(this.SerializeElement(C0)) const v_k = this.modOrder(v) acc = this.modOrder(acc + v_k) } @@ -79,7 +79,7 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { p.seed, p.password ]) - const c0 = this.hasher.H1(input) + const c0 = this.hasher.H6(input) const a0 = this.modOrder(c0) if (a0 === 0n) throw new Error('a0 must be non-zero') a.push(a0) @@ -91,7 +91,7 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { p.seed, p.password ]) - const c = this.hasher.H1(input) + const c = this.hasher.H6(input) a.push(this.modOrder(c)) } if (a.length !== threshold) throw new Error('incorrect coefficient length') @@ -357,11 +357,11 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { let dom = 1n for (const x_j of ids) { if (x_j === x_i) continue - num *= x_j - dom *= x_j - x_i + num = this.modOrder(num * this.modOrder(x_j)) + dom = this.modOrder(dom * this.modOrder(x_j - x_i)) } - const value = num / dom - return value + const invDom = this.invModOrder(dom) + return this.modOrder(num * invDom) } reconstructConstantFromShares (subset: { id: number; s_i: bigint }[]): bigint { @@ -374,6 +374,8 @@ class BabyFrostVSSDKG extends RailJubCurvePoint { } return a0 } + + } const vss = new BabyFrostVSSDKG() diff --git a/src/hashing.ts b/src/hashing.ts index 08abf62..b353399 100644 --- a/src/hashing.ts +++ b/src/hashing.ts @@ -67,6 +67,14 @@ class RFC9591Hasher { H5 (m: Uint8Array): bigint { return this.deriveHashed('com', m) } + + H6 (m: Uint8Array): bigint { + return this.deriveHashed('coeff', m) + } + + H7 (m: Uint8Array): bigint { + return this.deriveHashed('view', m) + } } export default RFC9591Hasher diff --git a/test/vss-extensions.test.ts b/test/vss-extensions.test.ts index a58798f..8d7f585 100644 --- a/test/vss-extensions.test.ts +++ b/test/vss-extensions.test.ts @@ -123,4 +123,20 @@ describe('VSS-DKG RFC-like extensions', () => { const rec = vss.reconstructConstantFromShares(sById.slice(0, 2)) assert.equal(rec, a0, 'reconstructed a0 should equal original') }) + + it('Lagrange reconstruction works for non-consecutive ids', () => { + // use ids [1, 3] which previously surfaced integer-division bug + const a0 = vss.RandomScalar() + const a1 = vss.RandomScalar() + const ids = [1n, 3n] + const sById: { id: number; s_i: bigint }[] = ids.map(id => ({ + id: Number(id), + s_i: vss.modOrder(a0 + a1 * id) + })) + const lambdas = ids.map(id => vss.deriveInterpolatingValue(ids as any, id)) + const sum = lambdas.reduce((acc, l) => vss.modOrder(acc + l), 0n) + assert.equal(sum, 1n, 'sum of λ_i(0) should be 1') + const rec = vss.reconstructConstantFromShares(sById) + assert.equal(rec, a0, 'reconstructed a0 should equal original for non-consecutive ids') + }) }) From 5c55cec99e93af91bdcc9947e101325f16eb9149 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 17 Oct 2025 19:37:19 -0600 Subject: [PATCH 07/44] feat: add end-to-end tests for TrustedDKG and extensions with comprehensive validation --- test/trusted-dkg-coordinatorless-e2e.test.ts | 90 ++++++++++++++ test/trusted-dkg-e2e.test.ts | 78 ++++++++++++ test/trusted-dkg-extensions.test.ts | 118 +++++++++++++++++++ 3 files changed, 286 insertions(+) create mode 100644 test/trusted-dkg-coordinatorless-e2e.test.ts create mode 100644 test/trusted-dkg-e2e.test.ts create mode 100644 test/trusted-dkg-extensions.test.ts diff --git a/test/trusted-dkg-coordinatorless-e2e.test.ts b/test/trusted-dkg-coordinatorless-e2e.test.ts new file mode 100644 index 0000000..94b1ecb --- /dev/null +++ b/test/trusted-dkg-coordinatorless-e2e.test.ts @@ -0,0 +1,90 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import assert from 'node:assert' +import { describe, it } from 'node:test' + +import type { Point } from '@zk-kit/baby-jubjub' + +import TrustedDKG from '../src/frost/trusted-dkg' +import BabyFROST from '../src/frost/babyfrost' +import { eddsaBuild } from '../src/eddsa' +import type { Commitment } from '../src/frost/types' +import { poseidonHex } from '../src/index' + +describe('TrustedDKG coordinator-less end-to-end', () => { + const dkg = new TrustedDKG() + + it('multi-dealer 3-of-5: per-dealer shares -> finalize -> reconstruct', () => { + const threshold = 3 + const ids = [1, 2, 3, 4, 5] + + + const dealers: Array<{ id: number; coeffs: bigint[] }> = [ + { id: 1, coeffs: dkg.trustedDealerKeygen(0x11n, 5, threshold).coefficients }, + { id: 2, coeffs: dkg.trustedDealerKeygen(0x22n, 5, threshold).coefficients }, + { id: 3, coeffs: dkg.trustedDealerKeygen(0x33n, 5, threshold).coefficients }, + { id: 4, coeffs: dkg.trustedDealerKeygen(0x44n, 5, threshold).coefficients }, + { id: 5, coeffs: dkg.trustedDealerKeygen(0x55n, 5, threshold).coefficients }, + ] + + // Commitments and per-dealer shares + const commitmentsByDealer: Record[]> = {} + const sharesByDealer: Record> = {} + for (const d of dealers) { + const C = dkg.vssCommit(d.coeffs) + assert.equal(C.length, threshold, 'each dealer must produce t commitments') + commitmentsByDealer[d.id] = C + sharesByDealer[d.id] = dkg.computeSharesForIds(d.coeffs, ids) + } + + // Aggregate commitments for group PK + const allDealerCommitments = dealers.map(d => commitmentsByDealer[d.id]!) + assert.strictEqual(dkg.verifyAllCommitmentsSubgroup(allDealerCommitments), true, 'commitments not in subgroup') + const PKGroup = dkg.combineGroupPubkeyFromCommitments(allDealerCommitments) + + // Check group PK equals Base*(sum of dealers' a0) + const a0_total = dealers.reduce((acc, d) => dkg.modOrder(acc + dkg.modOrder(d.coeffs[0]!)), 0n) + const PKDirect = dkg.ScalarBaseMult(a0_total) + assert.strictEqual(dkg.pointsEqual(PKGroup, PKDirect), true, 'group PK does not match sum of a0') + + // Feldman verify each per-dealer share, then finalize per participant by summing s_{k}(i) + const finalized: Array<{ id: number; skShare: bigint }> = [] + for (const id of ids) { + const s_ki_byDealer = dealers.map(d => ({ dealerId: d.id, s_ki: sharesByDealer[d.id]![id]! })) + // verify each dealer share for this participant id + for (const d of dealers) { + const ok = dkg.vssVerify({ i: BigInt(id), sk_i: sharesByDealer[d.id]![id]! }, commitmentsByDealer[d.id]!, threshold) + assert.strictEqual(ok, true, `Feldman verify failed for dealer ${d.id}, id ${id}`) + } + const res = dkg.finalizeParticipant(id, s_ki_byDealer, allDealerCommitments) + const expectedDiv8 = s_ki_byDealer.reduce((acc, s) => dkg.modOrder(acc + dkg.modOrder(s.s_ki)), 0n) + assert.equal(res.share.skShareDiv8, expectedDiv8, 'finalized share mismatch') + assert.strictEqual(dkg.pointsEqual(res.PKGroup, PKGroup), true, 'PKGroup mismatch in finalization') + finalized.push({ id, skShare: res.share.skShare }) + } + + // Reconstruct total a0 from aggregated shares of any 3 participants (e.g., ids 1,2,5) + const recIds = [1, 2, 5] + const aggregatedShares = recIds.map((id) => ({ + id, + s_i: dealers.reduce((acc, d) => dkg.modOrder(acc + dkg.modOrder(sharesByDealer[d.id]![id]!)), 0n) + })) + const rec = dkg.reconstructConstantFromShares(aggregatedShares) + assert.equal(rec, a0_total, 'reconstructed total a0 should equal sum of dealer a0') + + // FROST signing with 3 finalized participants; verify aggregate + const frost = new BabyFROST() + const groupPublicKey = PKGroup + const signingSubset = finalized.slice(0, threshold) + const msgHash = BigInt('0x' + poseidonHex(['0x' + 99999n.toString(16)], true)) + const p1 = frost.commit(signingSubset[0]!.skShare, BigInt(signingSubset[0]!.id)) + const p2 = frost.commit(signingSubset[1]!.skShare, BigInt(signingSubset[1]!.id)) + const p3 = frost.commit(signingSubset[2]!.skShare, BigInt(signingSubset[2]!.id)) + const commitmentList: Commitment[] = [p1, p2, p3].map((a) => ({ ...a.commitments })) + const s1 = frost.sign(p1.commitments.identifier, signingSubset[0]!.skShare, groupPublicKey, p1.nonces, msgHash, commitmentList) + const s2 = frost.sign(p2.commitments.identifier, signingSubset[1]!.skShare, groupPublicKey, p2.nonces, msgHash, commitmentList) + const s3 = frost.sign(p3.commitments.identifier, signingSubset[2]!.skShare, groupPublicKey, p3.nonces, msgHash, commitmentList) + const sig = frost.aggregate(commitmentList, msgHash, groupPublicKey, [s1, s2, s3]) + const okAgg = eddsaBuild.verifyPoseidon(frost.toBytes(msgHash).toReversed(), sig, groupPublicKey) + assert.strictEqual(okAgg, true, 'FROST aggregate failed verification') + }) +}) diff --git a/test/trusted-dkg-e2e.test.ts b/test/trusted-dkg-e2e.test.ts new file mode 100644 index 0000000..0c74781 --- /dev/null +++ b/test/trusted-dkg-e2e.test.ts @@ -0,0 +1,78 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import assert from 'node:assert' +import { describe, it } from 'node:test' + +import type { Point } from '@zk-kit/baby-jubjub' + +import TrustedDKG from '../src/frost/trusted-dkg' +import BabyFROST from '../src/frost/babyfrost' +import { eddsaBuild } from '../src/eddsa' +import type { Commitment } from '../src/frost/types' +import { poseidonHex } from '../src/index' + +describe('TrustedDKG end-to-end', () => { + const dkg = new TrustedDKG() + + it('3-of-5 flow: dealer keygen -> Feldman verify -> finalize -> secret reconstruction', () => { + const threshold = 3 + const n = 5 + + // 1) Trusted dealer keygen (deterministic secret for reproducibility) + const secret = 0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn + const { participantPrivateKeys: shares, coefficients, vssCommitment } = dkg.trustedDealerKeygen(secret, n, threshold) + assert.equal(coefficients[0], secret, 'constant term must equal secret') + assert.equal(vssCommitment.length, threshold, 'commitments length = threshold') + + // 2) Feldman verification for each participant share + const finalized: Array<{ id: number; skShareDiv8: bigint; skShare: bigint }> = [] + for (let i = 0; i < n; i++) { + const { x_i, y_i } = shares[i]! + const ok = dkg.vssVerify({ i: BigInt(x_i), sk_i: y_i }, vssCommitment, threshold) + assert.strictEqual(ok, true, `vssVerify failed for id ${x_i}`) + // coordinator-less finalize for this single-dealer case + const res = dkg.finalizeParticipant(x_i, [{ dealerId: 1, s_ki: y_i }], [vssCommitment]) + finalized.push({ id: x_i, skShareDiv8: res.share.skShareDiv8, skShare: res.share.skShare }) + } + + // 3) Derive group info and cross-check with direct Base*secret + const group = dkg.deriveGroupInfo(n, threshold, vssCommitment) + const directPK = dkg.ScalarBaseMult(secret) + assert.strictEqual(dkg.pointsEqual(group.PK!, directPK), true, 'group PK mismatch') + + // 4) Coordinator-less finalize per participant using “single dealer” view + // Build input in the multi-dealer shape: one dealer with our commitments, per-participant s_ki + const allCommitments: Point[][] = [vssCommitment] + for (let i = 0; i < n; i++) { + const { x_i, y_i } = shares[i]! + const res = dkg.finalizeParticipant(x_i, [{ dealerId: 1, s_ki: y_i }], allCommitments) + // skShareDiv8 equals underlying s_i + assert.equal(res.share.skShareDiv8, y_i) + assert.strictEqual(dkg.pointsEqual(res.PKGroup, group.PK!), true, 'finalized PKGroup mismatch') + } + + // 5) Reconstruct secret from any threshold subset (ids 1,2,3) + const subsetShares = shares.slice(0, threshold).map(({ x_i, y_i }) => ({ id: x_i, s_i: y_i })) + const rec = dkg.reconstructConstantFromShares(subsetShares) + assert.equal(rec, secret, 'reconstructed secret should equal original') + + // 6) Negative: Feldman verify should fail for tampered share + const bad = dkg.modOrder(shares[0]!.y_i + 1n) + const okBad1 = dkg.vssVerify({ i: BigInt(shares[0]!.x_i), sk_i: bad }, vssCommitment, threshold) + assert.strictEqual(okBad1, false) + // 7) Perform a FROST signing round with 3 participants; verify aggregate with EDDSA-Poseidon + const frost = new BabyFROST() + const groupPublicKey = group.PK! + const subset = finalized.slice(0, threshold) + const msgHash = BigInt('0x' + poseidonHex(['0x' + 12345n.toString(16)], true)) + const p1 = frost.commit(subset[0]!.skShare, BigInt(subset[0]!.id)) + const p2 = frost.commit(subset[1]!.skShare, BigInt(subset[1]!.id)) + const p3 = frost.commit(subset[2]!.skShare, BigInt(subset[2]!.id)) + const commitmentList: Commitment[] = [p1, p2, p3].map((a) => ({ ...a.commitments })) + const s1 = frost.sign(p1.commitments.identifier, subset[0]!.skShare, groupPublicKey, p1.nonces, msgHash, commitmentList) + const s2 = frost.sign(p2.commitments.identifier, subset[1]!.skShare, groupPublicKey, p2.nonces, msgHash, commitmentList) + const s3 = frost.sign(p3.commitments.identifier, subset[2]!.skShare, groupPublicKey, p3.nonces, msgHash, commitmentList) + const sig = frost.aggregate(commitmentList, msgHash, groupPublicKey, [s1, s2, s3]) + const okAgg = eddsaBuild.verifyPoseidon(frost.toBytes(msgHash).toReversed(), sig, groupPublicKey) + assert.strictEqual(okAgg, true, 'FROST aggregate failed verification') + }) +}) diff --git a/test/trusted-dkg-extensions.test.ts b/test/trusted-dkg-extensions.test.ts new file mode 100644 index 0000000..768232b --- /dev/null +++ b/test/trusted-dkg-extensions.test.ts @@ -0,0 +1,118 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import assert from 'node:assert' +import { describe, it } from 'node:test' + +import type { Point } from '@zk-kit/baby-jubjub' + +import TrustedDKG from '../src/frost/trusted-dkg' + +describe('TrustedDKG coordinator-less extensions', () => { + const dkg = new TrustedDKG() + + // fixed dealer coefficients for determinism (include constant term a0 first) + const dealer1Coeffs = [ + 0x11n, // a0 + 0x22n, // a1 + 0x33n, // a2 (threshold = 3) + ] + const dealer2Coeffs = [ + 0x55n, + 0x66n, + 0x77n, + ] + + function commitmentsFromCoeffs (coeffs: bigint[]): Point[] { + return dkg.vssCommit(coeffs) + } + + it('computeSharesForIds matches polynomialEvaluate()', () => { + const ids = [1, 2, 4] + const shares = dkg.computeSharesForIds(dealer1Coeffs, ids) + for (const id of ids) { + const p = dkg.polynomialEvaluate(BigInt(id), dealer1Coeffs) + assert.equal(shares[id], p, `share mismatch for id ${id}`) + } + }) + + it('vssVerify succeeds for valid shares and fails for tampered', () => { + const commitments = commitmentsFromCoeffs(dealer1Coeffs) + const ids = [1, 2, 3] + const shares = dkg.computeSharesForIds(dealer1Coeffs, ids) + for (const id of ids) { + const ok = dkg.vssVerify({ i: BigInt(id), sk_i: shares[id]!}, commitments, commitments.length) + assert.strictEqual(ok, true, `Feldman verify failed for id ${id}`) + const bad = dkg.vssVerify({i: BigInt(id), sk_i: dkg.modOrder(shares[id]! + 1n)}, commitments, commitments.length) + assert.strictEqual(bad, false, `Feldman verify should fail for tampered share id ${id}`) + } + }) + + it('combineGroupPubkeyFromCommitments equals Base*(sum of a0)', () => { + const c1 = commitmentsFromCoeffs(dealer1Coeffs) + const c2 = commitmentsFromCoeffs(dealer2Coeffs) + const group = dkg.combineGroupPubkeyFromCommitments([c1, c2]) + const sumA0 = dkg.modOrder(dealer1Coeffs[0]! + dealer2Coeffs[0]!) + const direct = dkg.ScalarBaseMult(sumA0) + assert.strictEqual(dkg.pointsEqual(group, direct), true, 'group PK mismatch with a0 sum') + }) + + it('verifyAllCommitmentsSubgroup returns true for valid commitments', () => { + const c1 = commitmentsFromCoeffs(dealer1Coeffs) + const c2 = commitmentsFromCoeffs(dealer2Coeffs) + const ok = dkg.verifyAllCommitmentsSubgroup([c1, c2]) + assert.strictEqual(ok, true) + }) + + it('finalizeParticipant aggregates s_ki and derives PKGroup', () => { + const c1 = commitmentsFromCoeffs(dealer1Coeffs) + const c2 = commitmentsFromCoeffs(dealer2Coeffs) + const allComm = [c1, c2] + const id = 2 + const s1 = dkg.polynomialEvaluate(BigInt(id), dealer1Coeffs) + const s2 = dkg.polynomialEvaluate(BigInt(id), dealer2Coeffs) + const res = dkg.finalizeParticipant(id, [ + { dealerId: 1, s_ki: s1 }, + { dealerId: 2, s_ki: s2 }, + ], allComm) + const expectedDiv8 = dkg.modOrder(s1 + s2) + assert.equal(res.share.skShareDiv8, expectedDiv8, 'skShareDiv8 should be sum of dealer shares') + assert.equal(res.share.skShare, dkg.modOrder(8n * expectedDiv8), 'skShare should be x8 mod L') + const group = dkg.combineGroupPubkeyFromCommitments(allComm) + assert.strictEqual(dkg.pointsEqual(res.PKGroup, group), true, 'PKGroup mismatch') + }) + + it('assertSortedConsecutiveIds: accepts non-consecutive, rejects invalid/unsorted', () => { + // non-consecutive is allowed + assert.doesNotThrow(() => dkg.computeSharesForIds(dealer1Coeffs, [1, 3])) + // unsorted must throw + assert.throws(() => dkg.computeSharesForIds(dealer1Coeffs, [3, 1]), /strictly increasing positive integers/i) + // invalid must throw + assert.throws(() => dkg.computeSharesForIds(dealer1Coeffs, [0, 2]), /strictly increasing positive integers/i) + }) + + it('Lagrange basis sums to 1 and reconstructs constant term', () => { + // simple linear polynomial a0 + a1*x + const a0 = 0x1234n + const a1 = 0x9n + const coeffs = [a0, a1] + const ids = [1n, 3n] + const shares = ids.map((id) => ({ id: Number(id), s_i: dkg.polynomialEvaluate(id, coeffs) })) + const lambdas = ids.map((id) => dkg.deriveInterpolatingValue(ids, id)) + const sum = lambdas.reduce((acc, l) => dkg.modOrder(acc + l), 0n) + assert.equal(sum, 1n, 'sum of λ_i(0) should be 1') + const rec = dkg.reconstructConstantFromShares(shares) + assert.equal(rec, a0, 'reconstructed a0 should equal original') + }) + + it('verifyFeldmanShare rejects tampered/non-subgroup commitment', () => { + const coeffs = [0x21n, 0x31n, 0x41n] + const commits = dkg.vssCommit(coeffs) + const id = 2 + const s = dkg.polynomialEvaluate(BigInt(id), coeffs) + // tamper C0 with a bogus point (synthetic non-subgroup/invalid) + const tampered = commits.slice() + // Intentionally use an arbitrary pair; strict verifier should return false or gracefully handle + tampered[0] = [1n, 1n] as any + const ok = dkg.verifyFeldmanShare(id, s, tampered) + assert.strictEqual(ok, false, 'tampered/non-subgroup commitment should fail Feldman verification') + }) +}) From b1b9db75c8e368da18279cac197cfeb695770b1a Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Sat, 18 Oct 2025 11:14:37 -0600 Subject: [PATCH 08/44] feat: implement FROSTSigningManager with signing session management and testing --- src/manager/signing.ts | 161 +++++++++++++++++++++++++++++++++++ test/signing-manager.test.ts | 143 +++++++++++++++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 src/manager/signing.ts create mode 100644 test/signing-manager.test.ts diff --git a/src/manager/signing.ts b/src/manager/signing.ts new file mode 100644 index 0000000..5e12baa --- /dev/null +++ b/src/manager/signing.ts @@ -0,0 +1,161 @@ +import type { Point } from '@zk-kit/baby-jubjub' + +import { BabyFROST } from '../frost' +import type { Bindings, Commitment } from '../frost/types' + +type SignerShare = { id: number; skShare: bigint } +type SessionBinding = { bindings: Bindings, share: SignerShare } +type PartialSignature = { identifier: number, partial: bigint } +type SigningSession = { + msgHash: bigint, + signers: SignerShare[], + groupPublicKey: Point, + remoteSigners: Commitment[], + partials: bigint[] +} +/* eslint-disable jsdoc/require-jsdoc */ +class FROSTSigningManager { + frost: BabyFROST + + signers: SignerShare[] = [] // map by id + // todo: make these session based... + localBindings: SessionBinding[] = [] // map by id? + + remoteSigners: Commitment[] = [] + groupPublicKey: Point + partialsById: Map = new Map() + sessions: SigningSession[] = [] + + constructor (publicKey: Point) { + this.frost = new BabyFROST() + this.groupPublicKey = publicKey + } + + hasId (identifier: bigint) { + for (const s of this.signers) { + if (s.id === Number(identifier.toString(10))) return true + } + return false + } + // create signing session + // msgHash, signers, remotecommitments... + + createSession (msgHash: bigint, signers: SignerShare[], publicKey: Point) { + const session = { + msgHash, + signers, + groupPublicKey: publicKey, + remoteSigners: [], + partials: [] + } + this.sessions.push(session) + return session + } + + addSigner (signer: SignerShare) { + this.signers.push(signer) + } + + addRemoteSigner (commitment: Commitment) { + // avoid duplicates by identifier + const exists = this.remoteSigners.find(c => c.identifier === commitment.identifier) + if (!exists) this.remoteSigners.push(commitment) + } + + // round 1 participants generate commitments, local will addRemoteSigners when commitments recieved + round1 () { + // reset local bindings for a fresh round + this.localBindings = [] + + for (const signer of this.signers) { + const bindings = this.frost.commit(signer.skShare, BigInt(signer.id)) + const sb = { + bindings, + share: signer + } + this.localBindings.push(sb) + } + } + + exportRound1 () { + const commitments: Commitment[] = [] + for (const sb of this.localBindings) commitments.push({ ...sb.bindings.commitments }) + return commitments + } + + getCommitmentList () { + // add local signers commitments first, then add unique remotes + const byId = new Map() + for (const c of this.exportRound1()) byId.set(String(c.identifier), c) + for (const c of this.remoteSigners) if (!byId.has(String(c.identifier))) byId.set(String(c.identifier), c) + const list = Array.from(byId.values()) + // sort by participant identifier to ensure consistent binding factors + list.sort((a, b) => (a.identifier < b.identifier ? -1 : a.identifier > b.identifier ? 1 : 0)) + return list + } + + sign (msgHash: bigint) { + const partials = [] + const commitmentList = this.getCommitmentList() + if (commitmentList.length === 0) throw new Error('No commitments available; run round1 and collect remotes first') + for (const signer of this.localBindings) { + const partial = this.frost.sign( + signer.bindings.commitments.identifier, + signer.share.skShare, + this.groupPublicKey, + signer.bindings.nonces, + msgHash, + commitmentList + ) + // return this based on identifier as well + const complete = { identifier: signer.share.id, partial } + // this.partials.push(complete) + partials.push(complete) + } + // todo: should pass along the commitmentlist signed against, so out of sync + // orchestration can still succeed with valid # of any threshold participants + return partials + } + + recievePartials (partials: PartialSignature[]) { + for (const p of partials) { + const id = Number(p.identifier) + if (!this.partialsById.has(id)) this.partialsById.set(id, p.partial) + } + } + + finalize (msgHash: bigint) { + // todo: check for threshold partials + const commitmentList = this.getCommitmentList() + if (commitmentList.length === 0) throw new Error('No commitments available; cannot finalize') + const expectedIds = this.expectedParticipantIds() + const missing = this.getMissingPartials() + if (missing.length > 0) { + throw new Error(`Missing partials for identifiers: ${missing.join(', ')}`) + } + const sigShares: bigint[] = expectedIds.map(id => this.partialsById.get(id)!) + const sig = this.frost.aggregate(commitmentList, msgHash, this.groupPublicKey, sigShares) + return sig // check for validity after + } + + expectedParticipantIds () { + return this.getCommitmentList().map(c => Number(c.identifier)) + } + + getMissingPartials () { + const expected = this.expectedParticipantIds() + return expected.filter(id => !this.partialsById.has(id)) + } + + readyToFinalize () { + return this.getMissingPartials().length === 0 + } + + resetRoundState () { + this.localBindings = [] + this.remoteSigners = [] + this.partialsById.clear() + } +} + +export default FROSTSigningManager diff --git a/test/signing-manager.test.ts b/test/signing-manager.test.ts new file mode 100644 index 0000000..2f90aa6 --- /dev/null +++ b/test/signing-manager.test.ts @@ -0,0 +1,143 @@ +import { subOrder } from '@zk-kit/baby-jubjub' +import assert from 'node:assert' + +import { describe, it } from 'node:test' + +import FROSTSigningManager from '../src/manager/signing' +import { eddsaBuild } from '../src' + +function mod (x: bigint) { + const r = x % subOrder + return r < 0n ? r + subOrder : r +} + + +const multiSigVector = [ + { + id: 1, + share: { + id: 1, + skShare: mod(BigInt('0x1d4260025e6e520d8daab9c1f9923b0c94c6ee643f051ff2526517535133804') * 8n), + skShareDiv8: BigInt('0x1d4260025e6e520d8daab9c1f9923b0c94c6ee643f051ff2526517535133804') + }, + PKGroup: [ + BigInt('0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6'), + BigInt('0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd') + ] + }, + { + id: 2, + share: { + id: 2, + skShare: mod(BigInt('0x1d85f8d8e472e07cf9fb432c0debae008ff241ea77f69c1d8403ffb36343cf0') * 8n), + skShareDiv8: BigInt('0x1d85f8d8e472e07cf9fb432c0debae008ff241ea77f69c1d8403ffb36343cf0') + }, + PKGroup: [ + BigInt('0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6'), + BigInt('0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd') + ] + }, + { + id: 3, + share: { + id: 3, + skShare: mod(BigInt('0x442308b78d3cf348e735520c35d77dfb70167d82e334d911afbd7f02497c653') * 8n), + skShareDiv8: BigInt('0x442308b78d3cf348e735520c35d77dfb70167d82e334d911afbd7f02497c653') + }, + PKGroup: [ + BigInt('0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6'), + BigInt('0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd') + ] + } +] + +describe('BabyFrost Signing Manager', () => { + it('should run e2e signing manager flow', () => { + const signers = [] + for (const v of multiSigVector) { + const signer = new FROSTSigningManager(v.PKGroup as [bigint, bigint]) + signer.addSigner({ id: v.id, skShare: v.share.skShare }) + signers.push(signer) + } + + const message = 12345n + const commitments = [] + for (const signer of signers) { + signer.round1() + const c = signer.exportRound1() + commitments.push(c) + for(const s of c ) { + for(const s2 of signers){ + if(!s2.hasId(s.identifier)) { + s2.addRemoteSigner(s) + } + } + } + } + const partials = [] + for (const signer of signers) { + partials.push(signer.sign(message)) + } + for (const signer of signers) { + for(const p of partials) { + signer.recievePartials(p) + } + } + const sig = signers[0].finalize(message) + const ok = eddsaBuild.verifyPoseidon(signers[0].frost.toBytes(message).toReversed(), sig, signers[0].groupPublicKey) + assert(ok, 'validation failed') + + }) + + it('exposes participant/partials helper methods', () => { + // create managers for each participant + const signers: FROSTSigningManager[] = [] + for (const v of multiSigVector) { + const signer = new FROSTSigningManager(v.PKGroup as [bigint, bigint]) + signer.addSigner({ id: v.id, skShare: v.share.skShare }) + signers.push(signer) + } + + const message = 12345n + + // round 1 and exchange commitments + for (const signer of signers) { + signer.round1() + const c = signer.exportRound1() + for (const s of c) { + for (const s2 of signers) { + if (!s2.hasId(s.identifier)) { + s2.addRemoteSigner(s) + } + } + } + } + + // helpers should reflect all participants and no partials yet + for (const signer of signers) { + assert.deepStrictEqual(signer.expectedParticipantIds(), [1, 2, 3]) + assert.deepStrictEqual(signer.getMissingPartials(), [1, 2, 3]) + assert.equal(signer.readyToFinalize(), false) + // cannot finalize yet + assert.throws(() => signer.finalize(message)) + } + + // produce and exchange partials + const partials: { identifier: number, partial: bigint }[][] = [] + for (const signer of signers) { + partials.push(signer.sign(message)) + } + for (const signer of signers) { + for (const p of partials) signer.recievePartials(p) + assert.deepStrictEqual(signer.getMissingPartials(), []) + assert.equal(signer.readyToFinalize(), true) + } + + // reset and ensure state is cleared + for (const signer of signers) { + signer.resetRoundState() + assert.deepStrictEqual(signer.expectedParticipantIds(), []) + assert.throws(() => signer.finalize(message)) + } + }) +}) \ No newline at end of file From ee91b5ac81a6705a100dfd7c05072e14167c83d3 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Sat, 18 Oct 2025 11:29:33 -0600 Subject: [PATCH 09/44] feat: enhance FROSTSigningManager to support threshold-based signing sessions and update tests accordingly --- src/manager/signing.ts | 45 ++++++++++++++++++++++++++++++------ test/signing-manager.test.ts | 6 ++--- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/manager/signing.ts b/src/manager/signing.ts index 5e12baa..236a957 100644 --- a/src/manager/signing.ts +++ b/src/manager/signing.ts @@ -25,9 +25,11 @@ class FROSTSigningManager { groupPublicKey: Point partialsById: Map = new Map() sessions: SigningSession[] = [] + threshold: number - constructor (publicKey: Point) { + constructor (publicKey: Point, threshold: number) { this.frost = new BabyFROST() + this.threshold = threshold this.groupPublicKey = publicKey } @@ -91,6 +93,10 @@ class FROSTSigningManager { const list = Array.from(byId.values()) // sort by participant identifier to ensure consistent binding factors list.sort((a, b) => (a.identifier < b.identifier ? -1 : a.identifier > b.identifier ? 1 : 0)) + const required = this.threshold + if (required != null && list.length < required) { + throw new Error(`Insufficient commitments: have ${list.length}, need >= ${required}`) + } return list } @@ -125,14 +131,35 @@ class FROSTSigningManager { } finalize (msgHash: bigint) { - // todo: check for threshold partials const commitmentList = this.getCommitmentList() if (commitmentList.length === 0) throw new Error('No commitments available; cannot finalize') - const expectedIds = this.expectedParticipantIds() - const missing = this.getMissingPartials() - if (missing.length > 0) { - throw new Error(`Missing partials for identifiers: ${missing.join(', ')}`) + const expectedIds = commitmentList.map(c => Number(c.identifier)) + // require all expected shares for correctness under current flow + const missing = expectedIds.filter(id => !this.partialsById.has(id)) + if (missing.length > 0) throw new Error(`Missing partials for identifiers: ${missing.join(', ')}`) + + // verify locally known shares before aggregation + const byId = new Map() + for (const c of commitmentList) byId.set(Number(c.identifier), c) + for (const { share } of this.localBindings) { + const id = share.id + const sigShare = this.partialsById.get(id) + if (sigShare == null) continue // nothing to verify locally yet + const commitmentLocal = byId.get(id) + if (!commitmentLocal) throw new Error(`Missing commitment for local id ${id}`) + const ok = this.frost.verifySignatureShare( + BigInt(id), + share.skShare, + commitmentLocal, + sigShare, + commitmentList, + this.groupPublicKey, + msgHash + ) + if (!ok) throw new Error(`Local signature share failed verification for local signer ${id}`) } + + // build partials in order by identifier const sigShares: bigint[] = expectedIds.map(id => this.partialsById.get(id)!) const sig = this.frost.aggregate(commitmentList, msgHash, this.groupPublicKey, sigShares) return sig // check for validity after @@ -148,7 +175,11 @@ class FROSTSigningManager { } readyToFinalize () { - return this.getMissingPartials().length === 0 + const commitmentList = this.getCommitmentList() + const expectedIds = commitmentList.map(c => Number(c.identifier)) + const available = expectedIds.filter(id => this.partialsById.has(id)).length + const required = this.threshold + return available >= required } resetRoundState () { diff --git a/test/signing-manager.test.ts b/test/signing-manager.test.ts index 2f90aa6..c5ebd6d 100644 --- a/test/signing-manager.test.ts +++ b/test/signing-manager.test.ts @@ -55,7 +55,7 @@ describe('BabyFrost Signing Manager', () => { it('should run e2e signing manager flow', () => { const signers = [] for (const v of multiSigVector) { - const signer = new FROSTSigningManager(v.PKGroup as [bigint, bigint]) + const signer = new FROSTSigningManager(v.PKGroup as [bigint, bigint], 3) signer.addSigner({ id: v.id, skShare: v.share.skShare }) signers.push(signer) } @@ -93,7 +93,7 @@ describe('BabyFrost Signing Manager', () => { // create managers for each participant const signers: FROSTSigningManager[] = [] for (const v of multiSigVector) { - const signer = new FROSTSigningManager(v.PKGroup as [bigint, bigint]) + const signer = new FROSTSigningManager(v.PKGroup as [bigint, bigint], 3) signer.addSigner({ id: v.id, skShare: v.share.skShare }) signers.push(signer) } @@ -136,7 +136,7 @@ describe('BabyFrost Signing Manager', () => { // reset and ensure state is cleared for (const signer of signers) { signer.resetRoundState() - assert.deepStrictEqual(signer.expectedParticipantIds(), []) + assert.throws(() => signer.expectedParticipantIds()) assert.throws(() => signer.finalize(message)) } }) From f19f5f05121ec634dfc60bade8eef213eed84fdf Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Mon, 20 Oct 2025 09:49:37 -0600 Subject: [PATCH 10/44] feat: add DKGManager class with trusted key generation and corresponding tests --- src/manager/dkg.ts | 23 +++++++++++++++++++++++ test/dkg-manager.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/manager/dkg.ts create mode 100644 test/dkg-manager.test.ts diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts new file mode 100644 index 0000000..228b476 --- /dev/null +++ b/src/manager/dkg.ts @@ -0,0 +1,23 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import TrustedDKG from '../frost/trusted-dkg' + +class DKGManager { + dkg: TrustedDKG + + constructor () { + this.dkg = new TrustedDKG() + } + + runTrustedKeygen (privateKey: bigint, desiredShares: number, threshold: number) { + const result = this.dkg.trustedDealerKeygen(privateKey, desiredShares, threshold) + const groupInfo = this.dkg.deriveGroupInfo(desiredShares, threshold, result.vssCommitment) + + const output = { + shares: result.participantPrivateKeys.map(a => { return { identifier: a.x_i, skShare: '0x' + a.y_i.toString(16) } }), + groupPublicKey: groupInfo.PK!.map(a => '0x' + a.toString(16)) + } + return output + } +} + +export default DKGManager diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts new file mode 100644 index 0000000..3bb6faa --- /dev/null +++ b/test/dkg-manager.test.ts @@ -0,0 +1,22 @@ + +import assert from 'node:assert' + +import { describe, it } from 'node:test' +import DKGManager from '../src/manager/dkg' + +describe('DKGManager e2e flow test', ()=>{ + + + it('runs trusted keygen',() => { + const dkgManager = new DKGManager() + const secret = BigInt(0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn) + + const keys = dkgManager.runTrustedKeygen(secret, 5, 3) + const expectedGroupPK = [ + '0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6', + '0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd' + ] + assert.deepStrictEqual(keys.groupPublicKey, expectedGroupPK, 'derived group PK does not match.') + + }) +}) \ No newline at end of file From 91c3139a97a404990d35f5d344879d07ec120b4b Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Mon, 20 Oct 2025 09:49:45 -0600 Subject: [PATCH 11/44] feat: enhance TrustedDKG with AES-GCM encryption for shares and update DKGManager test description --- src/frost/trusted-dkg.ts | 68 ++++++++++++++++++++++++++++++++++++++-- test/dkg-manager.test.ts | 2 +- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/frost/trusted-dkg.ts b/src/frost/trusted-dkg.ts index 93525c5..f58a9ac 100644 --- a/src/frost/trusted-dkg.ts +++ b/src/frost/trusted-dkg.ts @@ -1,13 +1,16 @@ /* eslint-disable camelcase */ /* eslint-disable jsdoc/require-jsdoc */ -/* eslint-disable jsdoc/require-param */ -/* eslint-disable jsdoc/require-returns */ + +import { gcm } from '@noble/ciphers/aes.js' +import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' import { addPoint, mulPointEscalar } from '@zk-kit/baby-jubjub' import { RailJubCurvePoint } from '../curve' import RFC9591Hasher from '../hashing' +import type { EncryptedShare } from './types' + class TrustedDKG extends RailJubCurvePoint { public readonly contextString = 'FROST-EDBABYJUJUB-BLAKE512-v1' hasher: RFC9591Hasher @@ -247,6 +250,67 @@ class TrustedDKG extends RailJubCurvePoint { const PKGroup = this.combineGroupPubkeyFromCommitments(allDealerCommitments) return { share, PKGroup, viewingPrivateKey } } + + commitmentsDigest (allDealerCommitments: Point[][]): bigint { + const enc = this.encodeCommitmentsBytes(allDealerCommitments) + return this.hasher.H5(enc) + } + + private encodeCommitmentsBytes (allDealerCommitments: Point[][]): Uint8Array { + const dealers = allDealerCommitments.slice().filter(a => a?.length) + dealers.sort((a, b) => (this.sortKey(a[0]!) < this.sortKey(b[0]!) ? -1 : 1)) + let out = new Uint8Array() + for (const dealer of dealers) { + for (const Cj of dealer) { + const enc = this.SerializeElement(Cj) + out = Buffer.concat([out, enc]) + } + } + return out + } + + encryptSharesAESGCMWithAAD ( + shares: Record, + keyById: Record, + allDealerCommitments: Point[][] + ): Record { + const ids = Object.keys(shares).map(Number).sort((a, b) => a - b) + TrustedDKG.assertSortedConsecutiveIds(ids) + const out: Record = {} + const digest = this.commitmentsDigest(allDealerCommitments) + const digestBytes = this.toBytes(digest) + for (const [idStr, s] of Object.entries(shares)) { + const id = Number(idStr) + const key = keyById[id] + if (!key || key.length !== 32) throw new Error(`bad AES key for id ${id}`) + const nonce = randomBytes(12) + const pt = this.toBytes(this.modOrder(s)) + const aad = Buffer.concat([this.SerializeScalar(BigInt(id)), digestBytes]) + const aead = gcm(key, nonce, aad) + const ciphertext = aead.encrypt(pt) + out[id] = { nonce, ciphertext } + } + return out + } + + decryptShareAESGCMWithAAD ( + enc: EncryptedShare, + keyBytes: Uint8Array, + participantId: number, + allDealerCommitments: Point[][] + ): bigint { + if (keyBytes.length !== 32) throw new Error('bad AES key length') + if (!Number.isInteger(participantId) || participantId <= 0) throw new Error('bad participant id') + const nonce = enc.nonce + const ct = enc.ciphertext + if (nonce.length !== 12) throw new Error('bad nonce length (expected 12)') + if (ct.length < 16) throw new Error('bad ciphertext (must include 16B tag)') + const digest = this.commitmentsDigest(allDealerCommitments) + const aad = Buffer.concat([this.SerializeScalar(BigInt(participantId)), this.toBytes(digest)]) + const aead = gcm(keyBytes, nonce, aad) + const pt = aead.decrypt(ct) + return this.fromBytes(pt) + } } export default TrustedDKG diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index 3bb6faa..8cbc712 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -7,7 +7,7 @@ import DKGManager from '../src/manager/dkg' describe('DKGManager e2e flow test', ()=>{ - it('runs trusted keygen',() => { + it('should run trusted keygen and get expected group publickey',() => { const dkgManager = new DKGManager() const secret = BigInt(0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn) From e48cf8262a532489fbbdb978e493811bc3b35750 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Mon, 20 Oct 2025 21:13:18 -0600 Subject: [PATCH 12/44] feat: add @noble/curves dependency and update DKGManager for share encryption --- package.json | 1 + src/frost/trusted-dkg.ts | 2 +- src/manager/dkg.ts | 45 ++++++++++++++++++++++++++++++++++++++++ test/dkg-manager.test.ts | 45 +++++++++++++++++++++++++++++++++++++++- yarn.lock | 9 +++++++- 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2b70880..23b3260 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ }, "dependencies": { "@noble/ciphers": "^2.0.1", + "@noble/curves": "^2.0.1", "@noble/ed25519": "^3.0.0", "@noble/hashes": "^2.0.1", "@zk-kit/baby-jubjub": "^1.0.3", diff --git a/src/frost/trusted-dkg.ts b/src/frost/trusted-dkg.ts index f58a9ac..b1d0ce1 100644 --- a/src/frost/trusted-dkg.ts +++ b/src/frost/trusted-dkg.ts @@ -31,7 +31,7 @@ class TrustedDKG extends RailJubCurvePoint { } trustedDealerKeygen (secretKey: bigint, MAX_PARTICIPANTS: number, MIN_PARTICIPANTS: number) { - const coefficients = [] + const coefficients: bigint[] = [] for (let i = 0; i < MIN_PARTICIPANTS - 1; i++) { coefficients.push(this.RandomScalar()) } diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index 228b476..37943bd 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -1,11 +1,23 @@ /* eslint-disable jsdoc/require-jsdoc */ +import { + x25519 +} from '@noble/curves/ed25519.js' +import { bigIntToBuffer, bufferToBigInt } from '@zk-kit/utils' + import TrustedDKG from '../frost/trusted-dkg' class DKGManager { dkg: TrustedDKG + participantID: number | undefined + secretComKey: bigint + pubComKey: bigint + coefficients: any constructor () { this.dkg = new TrustedDKG() + this.secretComKey = this.dkg.RandomScalar() + // this will be announced with public commitments + this.pubComKey = this.getPublicKey(this.secretComKey) } runTrustedKeygen (privateKey: bigint, desiredShares: number, threshold: number) { @@ -18,6 +30,39 @@ class DKGManager { } return output } + + // coordinatorless + // make coeff + // make commitments + // announce commitments + // compute dealer shares from commitments & coeff & encrypt + // decrypt & combine + + // assignment of personal participant id by 'organizer' + // need to handle incoming encrypted shares, + + commitmentRound (secret: bigint, desiredShares: number, threshold: number) { + const { coefficients } = this.dkg.trustedDealerKeygen(secret, desiredShares, threshold) + const commitments = this.dkg.vssCommit(coefficients) + const recipientIds = [] + for (let id = 1; id <= desiredShares; id++) { + recipientIds.push(id) + } + this.coefficients = coefficients + const shares = this.dkg.computeSharesForIds(coefficients, recipientIds) + return { shares, commitments } + } + + // used for encrypting/decrypting shares. + getPublicKey (secretKey: bigint) { + const key = x25519.getPublicKey(bigIntToBuffer(secretKey)) + return bufferToBigInt(key) + } + + getSharedSecret (theirPub: bigint) { + const shared = x25519.getSharedSecret(bigIntToBuffer(this.secretComKey), bigIntToBuffer(theirPub)) + return bufferToBigInt(shared) + } } export default DKGManager diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index 8cbc712..e03297d 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -3,11 +3,13 @@ import assert from 'node:assert' import { describe, it } from 'node:test' import DKGManager from '../src/manager/dkg' +import type { Point } from '@zk-kit/baby-jubjub' +import type { EncryptedShare } from '../src' describe('DKGManager e2e flow test', ()=>{ - it('should run trusted keygen and get expected group publickey',() => { + it('should run trusted keygen and get expected group publickey', () => { const dkgManager = new DKGManager() const secret = BigInt(0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn) @@ -18,5 +20,46 @@ describe('DKGManager e2e flow test', ()=>{ ] assert.deepStrictEqual(keys.groupPublicKey, expectedGroupPK, 'derived group PK does not match.') + const secrets = [ + 0x11n, + 0x22n, + 0x33n, + 0x44n, + 0x55n, + ] + const commitments: Point[][] = [] + const dealers: DKGManager[] = [] + const dealerShares: Record> = {} + secrets.forEach((secret, idx)=> { + const dealer = new DKGManager() + dealers.push(dealer) + const c = dealer.commitmentRound(secret, 5, 3) + commitments.push(c.commitments) + dealerShares[idx+1] = c.shares + }) + + const keyById: Record = {} + for (const p in dealerShares) keyById[p] = Buffer.alloc(32, p) + const encs: Record[] = [] + dealers.forEach((dealer, idx)=>{ + const encrypted = dealer.dkg.encryptSharesAESGCMWithAAD(dealerShares[idx+1], keyById, commitments) + encs.push(encrypted) + for(const id in dealerShares){ + const s_ki_byDealer = secrets.map((d, idx2) => ({ dealerId: idx2+1, s_ki: dealerShares[idx2+1]![id]! })) + const res = dealer.dkg.finalizeParticipant(idx+1, s_ki_byDealer, commitments) + // console.log('ski', id, s_ki_byDealer) + } + }) + let t = 1 + for(const e of encs){ + for(const id in e) { + try{ + const decrypted = dealers[0].dkg.decryptShareAESGCMWithAAD(e[id], keyById[id], Number(id), commitments) + console.log('decrypted', t, id, decrypted) + } catch (err){ + } + } + t++ + } }) }) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 849a62c..f387f35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -339,6 +339,13 @@ dependencies: "@noble/hashes" "1.3.2" +"@noble/curves@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-2.0.1.tgz#64ba8bd5e8564a02942655602515646df1cdb3ad" + integrity sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw== + dependencies: + "@noble/hashes" "2.0.1" + "@noble/ed25519@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-3.0.0.tgz#720d4cdb6b5f632e29164a7e9d5cdfeb82a7ac86" @@ -349,7 +356,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== -"@noble/hashes@^2.0.1": +"@noble/hashes@2.0.1", "@noble/hashes@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e" integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== From 7cee1fd2eb78aba3f483d8d2fd81c7270bc712df Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 21 Oct 2025 00:46:31 -0600 Subject: [PATCH 13/44] feat: enhance DKGManager with share encryption and participant management features --- src/manager/dkg.ts | 111 ++++++++++++++++++++++++++++++++++- test/dkg-manager.test.ts | 121 +++++++++++++++++++++++++++------------ 2 files changed, 192 insertions(+), 40 deletions(-) diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index 37943bd..17e0441 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -2,9 +2,12 @@ import { x25519 } from '@noble/curves/ed25519.js' +import { randomBytes } from '@noble/hashes/utils.js' +import type { Point } from '@zk-kit/baby-jubjub' import { bigIntToBuffer, bufferToBigInt } from '@zk-kit/utils' import TrustedDKG from '../frost/trusted-dkg' +import type { EncryptedShare } from '../frost/types' class DKGManager { dkg: TrustedDKG @@ -12,12 +15,21 @@ class DKGManager { secretComKey: bigint pubComKey: bigint coefficients: any + roster: Record + keysByID: Record = {} + shares: Record = {} + recipientIds: number[] = [] + encryptedShares: Record> = {} + commitments: Point[][] = [] constructor () { this.dkg = new TrustedDKG() - this.secretComKey = this.dkg.RandomScalar() + // this.secretComKey = this.dkg.RandomScalar() + this.secretComKey = bufferToBigInt(randomBytes(32)) + // this will be announced with public commitments this.pubComKey = this.getPublicKey(this.secretComKey) + this.roster = {} } runTrustedKeygen (privateKey: bigint, desiredShares: number, threshold: number) { @@ -41,18 +53,113 @@ class DKGManager { // assignment of personal participant id by 'organizer' // need to handle incoming encrypted shares, + getAnnouncement () { + return { pubKey: this.pubComKey } + } + + assignIdentifier (identifier: number) { + this.participantID = identifier + } + + // id and pubkey + assignRoster (roster: Record) { + this.roster = roster + for (const id in roster) { + const key = roster[id]! + if (this.pubComKey === key) { + this.assignIdentifier(Number(id)) + // its our key, keybyid should be our secret + // this.keysByID[id] = new Uint8Array(leBigIntToBuffer(this.secretComKey, 32)) + } + // else { + // } + const shared = this.getSharedSecret(key) + this.keysByID[id] = shared + } + } + commitmentRound (secret: bigint, desiredShares: number, threshold: number) { + // should have id before this + if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') const { coefficients } = this.dkg.trustedDealerKeygen(secret, desiredShares, threshold) const commitments = this.dkg.vssCommit(coefficients) const recipientIds = [] for (let id = 1; id <= desiredShares; id++) { recipientIds.push(id) } + this.recipientIds = recipientIds this.coefficients = coefficients const shares = this.dkg.computeSharesForIds(coefficients, recipientIds) + this.shares = shares + // console.log("shares", this.participantID, this.shares) + // encrypt shares for ids return { shares, commitments } } + addEncryptedShares (particpantID: number, shares: Record) { + this.encryptedShares[particpantID] = shares + } + + getEncryptedShares () { + // make sure the commitments are not empty, and same length as 'max participants' + + // const shares = this.dkg.computeSharesForIds(this.coefficients, this.recipientIds) + // this.shares = shares + return this.dkg.encryptSharesAESGCMWithAAD(this.shares, this.keysByID, this.commitments) + } + + getDecryptedShares ( + // encryptedBundlesByDealer: Record>, + // commitments: Point[][] + ) { + if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') + const decryptedByDealer: Record = {} + for (const dealerIdStr in this.encryptedShares) { + // console.log(dealerIdStr, encryptedBundlesByDealer) + const dealerId = Number(dealerIdStr) + const bundle = this.encryptedShares[dealerId] + const enc = bundle?.[this.participantID] + if (!enc) { + console.error(`No encrypted share for our id ${this.participantID} from dealer ${dealerId}`) + continue + } + const key = this.keysByID[dealerId] + if (!key) { + console.error(`Missing shared key for dealer ${dealerId}`) + continue + } + try { + const decrypted = this.dkg.decryptShareAESGCMWithAAD( + enc, + key, + this.participantID, + this.commitments + ) + decryptedByDealer[dealerId] = decrypted + } catch (error) { + console.error(`Failed to decrypt our share from dealer ${dealerId}:`, error) + } + } + return decryptedByDealer + } + + addCommitments (commitments: Point[][]) { + this.commitments = commitments + } + + finalize () { + if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') + const dec = this.getDecryptedShares() + // console.log("dealer",idx + 1, dec) + const shares = [] + for (const d in dec) { + const share = { dealerId: Number(d), s_ki: dec[d]! } + shares.push(share) + // console.log("SKI", s_ki_byDealer) + } + return this.dkg.finalizeParticipant(this.participantID, shares, this.commitments) + } + // used for encrypting/decrypting shares. getPublicKey (secretKey: bigint) { const key = x25519.getPublicKey(bigIntToBuffer(secretKey)) @@ -61,7 +168,7 @@ class DKGManager { getSharedSecret (theirPub: bigint) { const shared = x25519.getSharedSecret(bigIntToBuffer(this.secretComKey), bigIntToBuffer(theirPub)) - return bufferToBigInt(shared) + return shared } } diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index e03297d..a5db3cc 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -6,7 +6,7 @@ import DKGManager from '../src/manager/dkg' import type { Point } from '@zk-kit/baby-jubjub' import type { EncryptedShare } from '../src' -describe('DKGManager e2e flow test', ()=>{ +describe('DKGManager e2e flow test', () => { it('should run trusted keygen and get expected group publickey', () => { @@ -15,51 +15,96 @@ describe('DKGManager e2e flow test', ()=>{ const keys = dkgManager.runTrustedKeygen(secret, 5, 3) const expectedGroupPK = [ - '0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6', - '0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd' - ] - assert.deepStrictEqual(keys.groupPublicKey, expectedGroupPK, 'derived group PK does not match.') + '0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6', + '0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd' + ] + assert.deepStrictEqual(keys.groupPublicKey, expectedGroupPK, 'derived group PK does not match.') - const secrets = [ + const secrets = [ 0x11n, 0x22n, 0x33n, 0x44n, 0x55n, - ] - const commitments: Point[][] = [] - const dealers: DKGManager[] = [] - const dealerShares: Record> = {} - secrets.forEach((secret, idx)=> { - const dealer = new DKGManager() - dealers.push(dealer) - const c = dealer.commitmentRound(secret, 5, 3) - commitments.push(c.commitments) - dealerShares[idx+1] = c.shares - }) + ] + const commitments: Point[][] = [] + const dealers: DKGManager[] = [] + const dealerShares: Record> = {} + + const announcements: bigint[] = [] + secrets.forEach((secret, idx) => { + const dealer = new DKGManager() + dealers.push(dealer) + const announce = dealer.getAnnouncement() + announcements.push(announce.pubKey) + // const c = dealer.commitmentRound(secret, 5, 3) + // commitments.push(c.commitments) + // dealerShares[idx+1] = c.shares + }) + + // create roster + const roster: Record = {} + announcements.forEach((a, idx) => { + roster[idx+1] = a + }) + + + + dealers.forEach((dealer, idx) => { + dealer.assignRoster(roster) + // const dealer = new DKGManager() + const c = dealer.commitmentRound(secrets[idx], 5, 3) + commitments.push(c.commitments) + dealerShares[idx + 1] = c.shares + }) + dealers.forEach((dealer)=>{ + dealer.addCommitments(commitments) + }) const keyById: Record = {} for (const p in dealerShares) keyById[p] = Buffer.alloc(32, p) - const encs: Record[] = [] - dealers.forEach((dealer, idx)=>{ - const encrypted = dealer.dkg.encryptSharesAESGCMWithAAD(dealerShares[idx+1], keyById, commitments) - encs.push(encrypted) - for(const id in dealerShares){ - const s_ki_byDealer = secrets.map((d, idx2) => ({ dealerId: idx2+1, s_ki: dealerShares[idx2+1]![id]! })) - const res = dealer.dkg.finalizeParticipant(idx+1, s_ki_byDealer, commitments) - // console.log('ski', id, s_ki_byDealer) - } - }) - let t = 1 - for(const e of encs){ - for(const id in e) { - try{ - const decrypted = dealers[0].dkg.decryptShareAESGCMWithAAD(e[id], keyById[id], Number(id), commitments) - console.log('decrypted', t, id, decrypted) - } catch (err){ - } - } - t++ - } + const encs: Record[] = [] + dealers.forEach((dealer, idx) => { + const encrypted = dealer.getEncryptedShares() + encs.push(encrypted) + // for (const id in dealerShares) { + // const s_ki_byDealer = secrets.map((d, idx2) => ({ dealerId: idx2 + 1, s_ki: dealerShares[idx2 + 1]![id]! })) + // // const res = dealer.dkg.finalizeParticipant(idx + 1, s_ki_byDealer, commitments) + // // console.log('ski', id, s_ki_byDealer) + // } + }) + dealers.forEach((dealer) => { + encs.forEach((enc, _idx) => { + // this will be abstracted out to the encrypted shares, they will contain the 'particpant id which made' + dealer.addEncryptedShares(_idx + 1, enc) + }) + + }) + let t = 1 + dealers.forEach((dealer, idx)=>{ + const done = dealer.finalize() + // const dec = dealer.getDecryptedShares(commitments) + // // console.log("dealer",idx + 1, dec) + // const skdealer = [] + // for(const d in dec){ + // const s_ki_byDealer = { dealerId: Number(d), s_ki: dec[d] } + // skdealer.push(s_ki_byDealer) + // // console.log("SKI", s_ki_byDealer) + // } + // const res = dealer.dkg.finalizeParticipant(idx + 1, skdealer, commitments) + console.log("SKd", idx + 1, done) + + + }) + // for (const e of encs) { + // for (const id in e) { + // try { + // const decrypted = dealers[0].dkg.decryptShareAESGCMWithAAD(e[id], keyById[id], Number(id), commitments) + // console.log('decrypted', t, id, decrypted) + // } catch (err) { + // } + // } + // t++ + // } }) }) \ No newline at end of file From 5a31cd4cd020be940f254d2c2dad5f09e5d83fbf Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 21 Oct 2025 00:46:36 -0600 Subject: [PATCH 14/44] refactor: clean up DKGManager test by removing commented-out code and improving readability --- test/dkg-manager.test.ts | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index a5db3cc..724b970 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -38,9 +38,6 @@ describe('DKGManager e2e flow test', () => { dealers.push(dealer) const announce = dealer.getAnnouncement() announcements.push(announce.pubKey) - // const c = dealer.commitmentRound(secret, 5, 3) - // commitments.push(c.commitments) - // dealerShares[idx+1] = c.shares }) // create roster @@ -49,11 +46,8 @@ describe('DKGManager e2e flow test', () => { roster[idx+1] = a }) - - dealers.forEach((dealer, idx) => { dealer.assignRoster(roster) - // const dealer = new DKGManager() const c = dealer.commitmentRound(secrets[idx], 5, 3) commitments.push(c.commitments) dealerShares[idx + 1] = c.shares @@ -67,11 +61,6 @@ describe('DKGManager e2e flow test', () => { dealers.forEach((dealer, idx) => { const encrypted = dealer.getEncryptedShares() encs.push(encrypted) - // for (const id in dealerShares) { - // const s_ki_byDealer = secrets.map((d, idx2) => ({ dealerId: idx2 + 1, s_ki: dealerShares[idx2 + 1]![id]! })) - // // const res = dealer.dkg.finalizeParticipant(idx + 1, s_ki_byDealer, commitments) - // // console.log('ski', id, s_ki_byDealer) - // } }) dealers.forEach((dealer) => { encs.forEach((enc, _idx) => { @@ -80,31 +69,9 @@ describe('DKGManager e2e flow test', () => { }) }) - let t = 1 dealers.forEach((dealer, idx)=>{ const done = dealer.finalize() - // const dec = dealer.getDecryptedShares(commitments) - // // console.log("dealer",idx + 1, dec) - // const skdealer = [] - // for(const d in dec){ - // const s_ki_byDealer = { dealerId: Number(d), s_ki: dec[d] } - // skdealer.push(s_ki_byDealer) - // // console.log("SKI", s_ki_byDealer) - // } - // const res = dealer.dkg.finalizeParticipant(idx + 1, skdealer, commitments) - console.log("SKd", idx + 1, done) - - + console.log("finalized share", idx + 1, done) }) - // for (const e of encs) { - // for (const id in e) { - // try { - // const decrypted = dealers[0].dkg.decryptShareAESGCMWithAAD(e[id], keyById[id], Number(id), commitments) - // console.log('decrypted', t, id, decrypted) - // } catch (err) { - // } - // } - // t++ - // } }) }) \ No newline at end of file From 3dc69d6dcf523af3970ca96ae495a8277d11ff1e Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 21 Oct 2025 14:13:04 -0600 Subject: [PATCH 15/44] feat: implement commitment tracking and retrieval in DKGManager --- src/manager/dkg.ts | 54 ++++++++---- test/dkg-manager.test.ts | 176 +++++++++++++++++++++++++++++++++------ 2 files changed, 192 insertions(+), 38 deletions(-) diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index 17e0441..4bd44e4 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -20,7 +20,22 @@ class DKGManager { shares: Record = {} recipientIds: number[] = [] encryptedShares: Record> = {} - commitments: Point[][] = [] + private commitmentsByDealerId: Record[]> = {} + + private getAllDealerCommitments (): Point[][] { + const ids = Object.keys(this.commitmentsByDealerId).map(Number).sort((a, b) => a - b) + if (ids.length === 0) throw new Error('no dealer commitments have been added') + // If we have a roster, ensure we have commitments from every dealer id in the roster + const rosterIds = Object.keys(this.roster).map(Number).sort((a, b) => a - b) + if (rosterIds.length > 0) { + if (ids.length !== rosterIds.length || ids.some((id, i) => id !== rosterIds[i])) { + throw new Error('missing commitments for one or more dealers') + } + } + const ordered: Point[][] = [] + for (const id of ids) ordered.push(this.commitmentsByDealerId[id]!) + return ordered + } constructor () { this.dkg = new TrustedDKG() @@ -37,8 +52,15 @@ class DKGManager { const groupInfo = this.dkg.deriveGroupInfo(desiredShares, threshold, result.vssCommitment) const output = { - shares: result.participantPrivateKeys.map(a => { return { identifier: a.x_i, skShare: '0x' + a.y_i.toString(16) } }), - groupPublicKey: groupInfo.PK!.map(a => '0x' + a.toString(16)) + shares: result.participantPrivateKeys.map(a => { + return { + identifier: a.x_i, + skShare: '0x' + this.dkg.modOrder(a.y_i * 8n).toString(16), + skShareDiv8: '0x' + a.y_i.toString(16) + } + }), + groupPublicKey: groupInfo.PK!.map(a => '0x' + a.toString(16)), + viewingPrivateKey: groupInfo.viewingPrivateKey, } return output } @@ -91,11 +113,19 @@ class DKGManager { this.coefficients = coefficients const shares = this.dkg.computeSharesForIds(coefficients, recipientIds) this.shares = shares + // store our own commitments like we do for encrypted shares + this.addParticipantCommitments(this.participantID!, commitments) // console.log("shares", this.participantID, this.shares) // encrypt shares for ids return { shares, commitments } } + addParticipantCommitments (particpantID: number, participantCommitments: Point[]) { + if (!Number.isInteger(particpantID) || particpantID <= 0) throw new Error('bad participant id for commitments') + if (!participantCommitments?.length) throw new Error('empty commitments from participant') + this.commitmentsByDealerId[particpantID] = participantCommitments + } + addEncryptedShares (particpantID: number, shares: Record) { this.encryptedShares[particpantID] = shares } @@ -105,15 +135,14 @@ class DKGManager { // const shares = this.dkg.computeSharesForIds(this.coefficients, this.recipientIds) // this.shares = shares - return this.dkg.encryptSharesAESGCMWithAAD(this.shares, this.keysByID, this.commitments) + const allDealerCommitments = this.getAllDealerCommitments() + return this.dkg.encryptSharesAESGCMWithAAD(this.shares, this.keysByID, allDealerCommitments) } - getDecryptedShares ( - // encryptedBundlesByDealer: Record>, - // commitments: Point[][] - ) { + getDecryptedShares () { if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') const decryptedByDealer: Record = {} + const allDealerCommitments = this.getAllDealerCommitments() for (const dealerIdStr in this.encryptedShares) { // console.log(dealerIdStr, encryptedBundlesByDealer) const dealerId = Number(dealerIdStr) @@ -133,7 +162,7 @@ class DKGManager { enc, key, this.participantID, - this.commitments + allDealerCommitments ) decryptedByDealer[dealerId] = decrypted } catch (error) { @@ -143,10 +172,6 @@ class DKGManager { return decryptedByDealer } - addCommitments (commitments: Point[][]) { - this.commitments = commitments - } - finalize () { if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') const dec = this.getDecryptedShares() @@ -157,7 +182,8 @@ class DKGManager { shares.push(share) // console.log("SKI", s_ki_byDealer) } - return this.dkg.finalizeParticipant(this.participantID, shares, this.commitments) + const allDealerCommitments = this.getAllDealerCommitments() + return this.dkg.finalizeParticipant(this.participantID, shares, allDealerCommitments) } // used for encrypting/decrypting shares. diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index 724b970..8c659df 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -3,12 +3,10 @@ import assert from 'node:assert' import { describe, it } from 'node:test' import DKGManager from '../src/manager/dkg' -import type { Point } from '@zk-kit/baby-jubjub' -import type { EncryptedShare } from '../src' +import FROSTSigningManager from '../src/manager/signing' +import { eddsaBuild, type Point } from '../src' describe('DKGManager e2e flow test', () => { - - it('should run trusted keygen and get expected group publickey', () => { const dkgManager = new DKGManager() const secret = BigInt(0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn) @@ -27,7 +25,6 @@ describe('DKGManager e2e flow test', () => { 0x44n, 0x55n, ] - const commitments: Point[][] = [] const dealers: DKGManager[] = [] const dealerShares: Record> = {} @@ -43,35 +40,166 @@ describe('DKGManager e2e flow test', () => { // create roster const roster: Record = {} announcements.forEach((a, idx) => { - roster[idx+1] = a + roster[idx + 1] = a }) + // Each dealer runs commitment round and publishes commitments and shares + const commitmentsByDealer: Record['commitments']> = {} dealers.forEach((dealer, idx) => { dealer.assignRoster(roster) - const c = dealer.commitmentRound(secrets[idx], 5, 3) - commitments.push(c.commitments) - dealerShares[idx + 1] = c.shares + const { commitments: comms, shares } = dealer.commitmentRound(secrets[idx], 5, 3) + commitmentsByDealer[idx + 1] = comms + dealerShares[idx + 1] = shares + }) + + // Distribute commitments to every dealer + dealers.forEach((_dealer) => { + for (const idStr in commitmentsByDealer) { + const id = Number(idStr) + _dealer.addParticipantCommitments(id, commitmentsByDealer[id]!) + } + }) + + // Each dealer encrypts their shares using the full commitments set + const encryptedShares: Record> = {} + dealers.forEach((dealer, idx) => { + encryptedShares[idx + 1] = dealer.getEncryptedShares() + }) + + // Distribute encrypted shares to each recipient + dealers.forEach((dealer) => { + for (const idStr in encryptedShares) { + const id = Number(idStr) + dealer.addEncryptedShares(id, encryptedShares[id]!) + } + }) + + // Finalize: each dealer should be able to decrypt, combine and produce their share + const results = dealers.map((dealer) => dealer.finalize()) + results.forEach((done, idx) => { + assert.ok(done.share.skShare !== 0n) + assert.ok(done.share.skShareDiv8 !== 0n) + assert.strictEqual(done.share.id, idx + 1) }) - dealers.forEach((dealer)=>{ - dealer.addCommitments(commitments) + // All should derive the same group public key and viewing key + const refPK = results[0].PKGroup + const refVK = Buffer.from(results[0].viewingPrivateKey) + results.forEach((r) => { + assert.deepStrictEqual(r.PKGroup, refPK) + assert.equal(Buffer.compare(Buffer.from(r.viewingPrivateKey), refVK), 0) }) - const keyById: Record = {} - for (const p in dealerShares) keyById[p] = Buffer.alloc(32, p) - const encs: Record[] = [] + }) + + it('trusted method: keygen -> signing end-to-end (3-of-5)', () => { + const threshold = 3 + const n = 5 + + const dkgManager = new DKGManager() + const secret = BigInt(0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn) + + const { groupPublicKey, shares, } = dkgManager.runTrustedKeygen(secret, n, threshold) + + const groupPK = groupPublicKey.map(p => BigInt(p)) as Point + const expectedGroupPK = [ + '0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6', + '0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefd' + ] + assert.deepStrictEqual(groupPublicKey, expectedGroupPK, 'derived group PK does not match.') + + const subset = shares.slice(0, threshold) + + // build signing managers + const signers: FROSTSigningManager[] = [] + for (const f of subset) { + const sm = new FROSTSigningManager(groupPK, threshold) + sm.addSigner({ id: f.identifier, skShare: BigInt(f.skShare)}) + // sm.addSigner({ id: f.identifier, skShare: sm.frost.modOrder(BigInt(f.skShare) * 8n) }) + signers.push(sm) + } + + // round 1 exchange + for (const sm of signers) { + sm.round1() + const c = sm.exportRound1() + for (const s of c) for (const sm2 of signers) if (!sm2.hasId(s.identifier)) sm2.addRemoteSigner(s) + } + + // round 2 + const msg = 42069n + const partials: { identifier: number, partial: bigint }[][] = [] + for (const sm of signers) partials.push(sm.sign(msg)) + for (const sm of signers) for (const p of partials) sm.recievePartials(p) + + const sig = signers[0]!.finalize(msg) + const ok = eddsaBuild.verifyPoseidon(signers[0]!.frost.toBytes(msg).toReversed(), sig, groupPK) + assert.strictEqual(ok, true, 'coordinator-less flow signing verification failed') + + }) + + it('new coordinator-less flow: finalize -> signing end-to-end (3-of-5)', () => { + const threshold = 3 + const secrets = [0x11n, 0x22n, 0x33n, 0x44n, 0x55n] + const dealers: DKGManager[] = [] + + // announcements and roster + const announcements: bigint[] = [] + for (let i = 0; i < secrets.length; i++) { + const d = new DKGManager() + dealers.push(d) + announcements.push(d.getAnnouncement().pubKey) + } + const roster: Record = {} + announcements.forEach((a, idx) => { roster[idx + 1] = a }) + + // commitment rounds and distribution + const commitmentsByDealer: Record['commitments']> = {} dealers.forEach((dealer, idx) => { - const encrypted = dealer.getEncryptedShares() - encs.push(encrypted) + dealer.assignRoster(roster) + const { commitments: comms } = dealer.commitmentRound(secrets[idx]!, secrets.length, threshold) + commitmentsByDealer[idx + 1] = comms }) dealers.forEach((dealer) => { - encs.forEach((enc, _idx) => { - // this will be abstracted out to the encrypted shares, they will contain the 'particpant id which made' - dealer.addEncryptedShares(_idx + 1, enc) - }) - + for (const idStr in commitmentsByDealer) { + const id = Number(idStr) + dealer.addParticipantCommitments(id, commitmentsByDealer[id]!) + } }) - dealers.forEach((dealer, idx)=>{ - const done = dealer.finalize() - console.log("finalized share", idx + 1, done) + + // encrypt shares and distribute + const encryptedByDealerId: Record> = {} + dealers.forEach((dealer, idx) => { encryptedByDealerId[idx + 1] = dealer.getEncryptedShares() }) + dealers.forEach((dealer) => { + for (const idStr in encryptedByDealerId) dealer.addEncryptedShares(Number(idStr), encryptedByDealerId[Number(idStr)]!) }) + + // finalize and take first 3 signers + const finalized = dealers.map((d) => d.finalize()) + const groupPublicKey = finalized[0]!.PKGroup + const subset = finalized.slice(0, threshold) + + // build signing managers + const signers: FROSTSigningManager[] = [] + for (const f of subset) { + const sm = new FROSTSigningManager(groupPublicKey, threshold) + sm.addSigner({ id: f.share.id, skShare: f.share.skShare }) + signers.push(sm) + } + + // round 1 exchange + for (const sm of signers) { + sm.round1() + const c = sm.exportRound1() + for (const s of c) for (const sm2 of signers) if (!sm2.hasId(s.identifier)) sm2.addRemoteSigner(s) + } + + // round 2 + const msg = 42069n + const partials: { identifier: number, partial: bigint }[][] = [] + for (const sm of signers) partials.push(sm.sign(msg)) + for (const sm of signers) for (const p of partials) sm.recievePartials(p) + + const sig = signers[0]!.finalize(msg) + const ok = eddsaBuild.verifyPoseidon(signers[0]!.frost.toBytes(msg).toReversed(), sig, groupPublicKey) + assert.strictEqual(ok, true, 'coordinator-less flow signing verification failed') }) }) \ No newline at end of file From b8813b77f69fbba52d35db57e213e88b2a3cf723 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 21 Oct 2025 16:45:45 -0600 Subject: [PATCH 16/44] refactor: remove comments --- src/manager/dkg.ts | 33 +++------------------------------ test/dkg-manager.test.ts | 2 +- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index 4bd44e4..29f70f5 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -12,12 +12,11 @@ import type { EncryptedShare } from '../frost/types' class DKGManager { dkg: TrustedDKG participantID: number | undefined - secretComKey: bigint + private secretComKey: bigint pubComKey: bigint - coefficients: any roster: Record keysByID: Record = {} - shares: Record = {} + private shares: Record = {} recipientIds: number[] = [] encryptedShares: Record> = {} private commitmentsByDealerId: Record[]> = {} @@ -65,16 +64,6 @@ class DKGManager { return output } - // coordinatorless - // make coeff - // make commitments - // announce commitments - // compute dealer shares from commitments & coeff & encrypt - // decrypt & combine - - // assignment of personal participant id by 'organizer' - // need to handle incoming encrypted shares, - getAnnouncement () { return { pubKey: this.pubComKey } } @@ -90,33 +79,23 @@ class DKGManager { const key = roster[id]! if (this.pubComKey === key) { this.assignIdentifier(Number(id)) - // its our key, keybyid should be our secret - // this.keysByID[id] = new Uint8Array(leBigIntToBuffer(this.secretComKey, 32)) } - // else { - // } const shared = this.getSharedSecret(key) this.keysByID[id] = shared } } commitmentRound (secret: bigint, desiredShares: number, threshold: number) { - // should have id before this if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') - const { coefficients } = this.dkg.trustedDealerKeygen(secret, desiredShares, threshold) - const commitments = this.dkg.vssCommit(coefficients) + const { coefficients, vssCommitment: commitments } = this.dkg.trustedDealerKeygen(secret, desiredShares, threshold) const recipientIds = [] for (let id = 1; id <= desiredShares; id++) { recipientIds.push(id) } this.recipientIds = recipientIds - this.coefficients = coefficients const shares = this.dkg.computeSharesForIds(coefficients, recipientIds) this.shares = shares - // store our own commitments like we do for encrypted shares this.addParticipantCommitments(this.participantID!, commitments) - // console.log("shares", this.participantID, this.shares) - // encrypt shares for ids return { shares, commitments } } @@ -132,9 +111,6 @@ class DKGManager { getEncryptedShares () { // make sure the commitments are not empty, and same length as 'max participants' - - // const shares = this.dkg.computeSharesForIds(this.coefficients, this.recipientIds) - // this.shares = shares const allDealerCommitments = this.getAllDealerCommitments() return this.dkg.encryptSharesAESGCMWithAAD(this.shares, this.keysByID, allDealerCommitments) } @@ -144,7 +120,6 @@ class DKGManager { const decryptedByDealer: Record = {} const allDealerCommitments = this.getAllDealerCommitments() for (const dealerIdStr in this.encryptedShares) { - // console.log(dealerIdStr, encryptedBundlesByDealer) const dealerId = Number(dealerIdStr) const bundle = this.encryptedShares[dealerId] const enc = bundle?.[this.participantID] @@ -175,12 +150,10 @@ class DKGManager { finalize () { if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') const dec = this.getDecryptedShares() - // console.log("dealer",idx + 1, dec) const shares = [] for (const d in dec) { const share = { dealerId: Number(d), s_ki: dec[d]! } shares.push(share) - // console.log("SKI", s_ki_byDealer) } const allDealerCommitments = this.getAllDealerCommitments() return this.dkg.finalizeParticipant(this.participantID, shares, allDealerCommitments) diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index 8c659df..3651822 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -136,7 +136,7 @@ describe('DKGManager e2e flow test', () => { }) - it('new coordinator-less flow: finalize -> signing end-to-end (3-of-5)', () => { + it('coordinator-less flow: finalize -> signing end-to-end (3-of-5)', () => { const threshold = 3 const secrets = [0x11n, 0x22n, 0x33n, 0x44n, 0x55n] const dealers: DKGManager[] = [] From 037b315bc9bb127577668ade9f014afc692e4fe2 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 21 Oct 2025 19:53:55 -0600 Subject: [PATCH 17/44] refactor: update DKGManager to use Uint8Array for keys and shares --- src/manager/dkg.ts | 27 +++++++++++++++------------ test/dkg-manager.test.ts | 8 ++++---- test/trusted-dkg-extensions.test.ts | 18 +++++++----------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index 29f70f5..06e136f 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -1,10 +1,11 @@ /* eslint-disable jsdoc/require-jsdoc */ +// import assert from 'node:assert' import { x25519 } from '@noble/curves/ed25519.js' import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' -import { bigIntToBuffer, bufferToBigInt } from '@zk-kit/utils' +import { bufferToBigInt } from '@zk-kit/utils' import TrustedDKG from '../frost/trusted-dkg' import type { EncryptedShare } from '../frost/types' @@ -12,9 +13,9 @@ import type { EncryptedShare } from '../frost/types' class DKGManager { dkg: TrustedDKG participantID: number | undefined - private secretComKey: bigint - pubComKey: bigint - roster: Record + private secretComKey: Uint8Array + pubComKey: Uint8Array + roster: Record keysByID: Record = {} private shares: Record = {} recipientIds: number[] = [] @@ -39,7 +40,7 @@ class DKGManager { constructor () { this.dkg = new TrustedDKG() // this.secretComKey = this.dkg.RandomScalar() - this.secretComKey = bufferToBigInt(randomBytes(32)) + this.secretComKey = randomBytes(32) // this will be announced with public commitments this.pubComKey = this.getPublicKey(this.secretComKey) @@ -73,13 +74,15 @@ class DKGManager { } // id and pubkey - assignRoster (roster: Record) { + assignRoster (roster: Record) { this.roster = roster for (const id in roster) { const key = roster[id]! - if (this.pubComKey === key) { + // assert.deepStrictEqual(this.pubComKey, key, '') + if (bufferToBigInt(this.pubComKey) === bufferToBigInt(key)) { this.assignIdentifier(Number(id)) } + // pad key here. const shared = this.getSharedSecret(key) this.keysByID[id] = shared } @@ -160,13 +163,13 @@ class DKGManager { } // used for encrypting/decrypting shares. - getPublicKey (secretKey: bigint) { - const key = x25519.getPublicKey(bigIntToBuffer(secretKey)) - return bufferToBigInt(key) + getPublicKey (secretKey: Uint8Array) { + const key = x25519.getPublicKey(secretKey) + return key } - getSharedSecret (theirPub: bigint) { - const shared = x25519.getSharedSecret(bigIntToBuffer(this.secretComKey), bigIntToBuffer(theirPub)) + getSharedSecret (theirPub: Uint8Array) { + const shared = x25519.getSharedSecret(this.secretComKey, theirPub) return shared } } diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index 3651822..2db900a 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -29,7 +29,7 @@ describe('DKGManager e2e flow test', () => { const dealerShares: Record> = {} - const announcements: bigint[] = [] + const announcements: Uint8Array[] = [] secrets.forEach((secret, idx) => { const dealer = new DKGManager() dealers.push(dealer) @@ -38,7 +38,7 @@ describe('DKGManager e2e flow test', () => { }) // create roster - const roster: Record = {} + const roster: Record = {} announcements.forEach((a, idx) => { roster[idx + 1] = a }) @@ -142,13 +142,13 @@ describe('DKGManager e2e flow test', () => { const dealers: DKGManager[] = [] // announcements and roster - const announcements: bigint[] = [] + const announcements: Uint8Array[] = [] for (let i = 0; i < secrets.length; i++) { const d = new DKGManager() dealers.push(d) announcements.push(d.getAnnouncement().pubKey) } - const roster: Record = {} + const roster: Record = {} announcements.forEach((a, idx) => { roster[idx + 1] = a }) // commitment rounds and distribution diff --git a/test/trusted-dkg-extensions.test.ts b/test/trusted-dkg-extensions.test.ts index 768232b..7feb68c 100644 --- a/test/trusted-dkg-extensions.test.ts +++ b/test/trusted-dkg-extensions.test.ts @@ -21,10 +21,6 @@ describe('TrustedDKG coordinator-less extensions', () => { 0x77n, ] - function commitmentsFromCoeffs (coeffs: bigint[]): Point[] { - return dkg.vssCommit(coeffs) - } - it('computeSharesForIds matches polynomialEvaluate()', () => { const ids = [1, 2, 4] const shares = dkg.computeSharesForIds(dealer1Coeffs, ids) @@ -35,7 +31,7 @@ describe('TrustedDKG coordinator-less extensions', () => { }) it('vssVerify succeeds for valid shares and fails for tampered', () => { - const commitments = commitmentsFromCoeffs(dealer1Coeffs) + const commitments = dkg.vssCommit(dealer1Coeffs) const ids = [1, 2, 3] const shares = dkg.computeSharesForIds(dealer1Coeffs, ids) for (const id of ids) { @@ -47,8 +43,8 @@ describe('TrustedDKG coordinator-less extensions', () => { }) it('combineGroupPubkeyFromCommitments equals Base*(sum of a0)', () => { - const c1 = commitmentsFromCoeffs(dealer1Coeffs) - const c2 = commitmentsFromCoeffs(dealer2Coeffs) + const c1 = dkg.vssCommit(dealer1Coeffs) + const c2 = dkg.vssCommit(dealer2Coeffs) const group = dkg.combineGroupPubkeyFromCommitments([c1, c2]) const sumA0 = dkg.modOrder(dealer1Coeffs[0]! + dealer2Coeffs[0]!) const direct = dkg.ScalarBaseMult(sumA0) @@ -56,15 +52,15 @@ describe('TrustedDKG coordinator-less extensions', () => { }) it('verifyAllCommitmentsSubgroup returns true for valid commitments', () => { - const c1 = commitmentsFromCoeffs(dealer1Coeffs) - const c2 = commitmentsFromCoeffs(dealer2Coeffs) + const c1 = dkg.vssCommit(dealer1Coeffs) + const c2 = dkg.vssCommit(dealer2Coeffs) const ok = dkg.verifyAllCommitmentsSubgroup([c1, c2]) assert.strictEqual(ok, true) }) it('finalizeParticipant aggregates s_ki and derives PKGroup', () => { - const c1 = commitmentsFromCoeffs(dealer1Coeffs) - const c2 = commitmentsFromCoeffs(dealer2Coeffs) + const c1 = dkg.vssCommit(dealer1Coeffs) + const c2 = dkg.vssCommit(dealer2Coeffs) const allComm = [c1, c2] const id = 2 const s1 = dkg.polynomialEvaluate(BigInt(id), dealer1Coeffs) From 27acdbe2bf95da1f09bd3e64909c57df885fc927 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 21 Oct 2025 20:29:08 -0600 Subject: [PATCH 18/44] feat: implement DKG flow state management and enhance error handling in DKGManager --- docs/dkg-manager.md | 113 ++++++++++++++++++++++++ docs/signing-manager.md | 73 ++++++++++++++++ src/manager/dkg.ts | 179 ++++++++++++++++++++++++++++++--------- test/dkg-manager.test.ts | 5 +- 4 files changed, 329 insertions(+), 41 deletions(-) create mode 100644 docs/dkg-manager.md create mode 100644 docs/signing-manager.md diff --git a/docs/dkg-manager.md b/docs/dkg-manager.md new file mode 100644 index 0000000..5bf033e --- /dev/null +++ b/docs/dkg-manager.md @@ -0,0 +1,113 @@ +# DKGManager + +A small orchestration helper around the FROST DKG primitives that supports two flows: + +- Trusted-dealer keygen: one dealer produces all shares and the group public key. +- Coordinator-less multi-dealer: many dealers each contribute a polynomial; participants encrypt/decrypt shares and finalize their own share. + +This README covers the public API, step-by-step usage, and common errors. The code lives in `src/manager/dkg.ts`. + +## Public API (stable) + +- `new DKGManager()` +- `getAnnouncement(): { pubKey: Uint8Array }` — publish your X25519 public key for share encryption. +- `assignRoster(roster: Record): void` — map of participantId → X25519 public key. Your own key must be present. +- `runTrustedKeygen(secret: bigint, n: number, t: number)` → `{ shares, groupPublicKey, viewingPrivateKey }` +- `commitmentRound(secret: bigint, n: number, t: number)` → `{ shares: Record, commitments: Point[] }` +- `addParticipantCommitments(dealerId: number, commitments: Point[]): void` +- `getEncryptedShares()` → `Record` +- `addEncryptedShares(dealerId: number, shares: Record): void` +- `finalize()` → `{ share: { id, skShare, skShareDiv8 }, PKGroup, viewingPrivateKey }` + +Notes +- Participant IDs are 1..N positive integers. +- Shares and commitments must be provided for every dealer in the roster before finalizing. + +## Flow 1: Trusted-dealer keygen + +When a single dealer generates all shares and the group key. + +```ts +import DKGManager from '../src/manager/dkg' + +const dkg = new DKGManager() +const secret = 0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn +const n = 5 +const t = 3 + +const { groupPublicKey, shares, viewingPrivateKey } = dkg.runTrustedKeygen(secret, n, t) +// shares: [{ identifier, skShare, skShareDiv8 }] +// groupPublicKey: [xHex, yHex] +``` + +Validation +- `n > 0`, `t > 0`, `t <= n`. + +## Flow 2: Coordinator-less multi-dealer + +Every dealer contributes a polynomial. Each participant obtains an encrypted share from every dealer, decrypts locally, and finalizes their own share. + +High level steps per dealer: + +1. Announce and roster +```ts +const dealer = new DKGManager() +const announce = dealer.getAnnouncement() // { pubKey } +// Build a roster across all dealers +const roster: Record = { 1: pub1, 2: pub2, 3: pub3, 4: pub4, 5: pub5 } +dealer.assignRoster(roster) // assigns dealer.participantID implicitly based on its own pubKey +``` + +2. Commitment round (per dealer) +```ts +const { shares, commitments } = dealer.commitmentRound(secret_i, n, t) +// Broadcast `commitments` to everyone; keep `shares` to encrypt next +``` + +3. Collect commitments +```ts +for (const [dealerId, comms] of Object.entries(allCommitments)) { + dealer.addParticipantCommitments(Number(dealerId), comms) +} +``` + +4. Encrypt and distribute shares (per dealer) +```ts +const encryptedByRecipient = dealer.getEncryptedShares() +// Send encryptedByRecipient[participantId] to each participant +``` + +5. Collect encrypted shares (per participant) +```ts +for (const [dealerId, encBundle] of Object.entries(collectedEncrypted)) { + dealer.addEncryptedShares(Number(dealerId), encBundle) +} +``` + +6. Finalize local share (per participant) +```ts +const { share, PKGroup, viewingPrivateKey } = dealer.finalize() +// share.id === dealer.participantID +``` + +Validation and ordering +- `assignRoster` must run before `commitmentRound`. +- Each `addParticipantCommitments` must be called for all dealers listed in roster (same ID set). +- `getEncryptedShares` requires a complete commitment set and local shares from `commitmentRound`. +- `addEncryptedShares` must be called for all dealers in roster. +- `finalize` throws if any dealers are missing. + +## Error messages and causes + +- `roster not assigned` — call `assignRoster` first. +- `our announcement pubKey not present in roster` — include the current manager's pubKey in roster. +- `roster size does not match desiredShares` — ensure `n` equals roster size. +- `missing commitments for one or more dealers` — collect all dealer commitments before encrypting or finalizing. +- `missing encrypted shares or keys from dealers: ...` — not all encrypted shares were collected. +- `threshold cannot exceed desiredShares` — fix `t` vs `n`. + +## Tips + +- Participant IDs are 1..N consecutively. +- The `viewingPrivateKey` is derived deterministically from the set of dealers (C0 commitments) and is identical across participants. +- The returned `PKGroup` is the sum of the first commitments across dealers. diff --git a/docs/signing-manager.md b/docs/signing-manager.md new file mode 100644 index 0000000..9e90b30 --- /dev/null +++ b/docs/signing-manager.md @@ -0,0 +1,73 @@ +# FROSTSigningManager + +Orchestrates the 2-round FROST signing flow on top of `BabyFROST` primitives, given a finalized group public key and per-participant shares. + +This README documents the public API, a minimal E2E example, and helper methods for readiness and diagnostics. The code lives in `src/manager/signing.ts`. + +## Public API (stable) + +- `new FROSTSigningManager(groupPublicKey: Point, threshold: number)` +- `addSigner({ id: number, skShare: bigint }): void` — add a local signer/share owned by this manager instance. +- `round1(): void` — generate commitments for local signers and reset round state. +- `exportRound1(): Commitment[]` — commitments for local signers to share with others. +- `addRemoteSigner(commitment: Commitment): void` — add a commitment from another participant (id is unique). +- `sign(msgHash: bigint): { identifier: number, partial: bigint }[]` — produce signature shares for all local signers, bound to the combined commitment list. +- `recievePartials(partials: { identifier, partial }[]): void` — collect partials from others. +- `finalize(msgHash: bigint)` → aggregated signature (tuple `[R8x, R8y, s]` from `BabyFROST`). + +Helper methods +- `expectedParticipantIds(): number[]` — identifiers implied by the current commitment list. +- `getMissingPartials(): number[]` — identifiers without collected partials. +- `readyToFinalize(): boolean` — true when at least `threshold` partials are available among expected ids. +- `resetRoundState(): void` — clears commitments and partials. + +## Minimal E2E usage (t-of-n) + +```ts +import FROSTSigningManager from '../src/manager/signing' +import { eddsaBuild } from '../src' // provides verifyPoseidon +import { bigIntToBuffer } from '@zk-kit/utils' + +const t = 3 +const groupPublicKey = /* Point from DKG */ + +// choose any t participants that have finalized DKG shares +const signers: FROSTSigningManager[] = [] +for (const { id, skShare } of subsetShares) { + const sm = new FROSTSigningManager(groupPublicKey, t) + sm.addSigner({ id, skShare }) + signers.push(sm) +} + +// round 1: produce + exchange commitments +for (const sm of signers) { + sm.round1() + const local = sm.exportRound1() + for (const c of local) { + for (const peer of signers) if (!peer.hasId(c.identifier)) peer.addRemoteSigner(c) + } +} + +// round 2: sign and exchange partials +const msg = 42069n +const partialsFromAll: { identifier: number, partial: bigint }[][] = [] +for (const sm of signers) partialsFromAll.push(sm.sign(msg)) +for (const sm of signers) for (const batch of partialsFromAll) sm.recievePartials(batch) + +// finalize and verify +const sig = signers[0].finalize(msg) +const ok = eddsaBuild.verifyPoseidon(bigIntToBuffer(msg), sig, groupPublicKey) +``` + +## Validation and errors + +- `getCommitmentList` sorts commitments by identifier and enforces `list.length >= threshold`. +- `sign` and `finalize` throw when commitments are missing or inconsistent. +- `finalize` verifies local signature shares before aggregation and throws if any fail verification. +- Use `readyToFinalize` and `getMissingPartials` to monitor progress. + +## Tips + +- Keep one manager instance per participating device/process. Each instance can manage one or more local shares if needed. +- Always call `round1()` before `exportRound1()` and `sign()`. +- Identifiers must be positive integers and must match identifiers assigned during DKG. diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index 06e136f..cdd754f 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -10,6 +10,19 @@ import { bufferToBigInt } from '@zk-kit/utils' import TrustedDKG from '../frost/trusted-dkg' import type { EncryptedShare } from '../frost/types' +// Lightweight internal state to prevent wrong-order usage and surface clear errors +// also output state updates for the upstream clients. + +enum DKGFlowState { + Init = 'init', + RosterAssigned = 'roster-assigned', + CommitmentsCreated = 'commitments-created', + CommitmentsCollected = 'commitments-collected', + SharesEncrypted = 'shares-encrypted', + EncryptedSharesCollected = 'encrypted-shares-collected', + Finalized = 'finalized', +} + class DKGManager { dkg: TrustedDKG participantID: number | undefined @@ -22,15 +35,43 @@ class DKGManager { encryptedShares: Record> = {} private commitmentsByDealerId: Record[]> = {} + private state: DKGFlowState = DKGFlowState.Init + + private readonly stateOrder: Record = { + [DKGFlowState.Init]: 0, + [DKGFlowState.RosterAssigned]: 1, + [DKGFlowState.CommitmentsCreated]: 2, + [DKGFlowState.CommitmentsCollected]: 3, + [DKGFlowState.SharesEncrypted]: 4, + [DKGFlowState.EncryptedSharesCollected]: 5, + [DKGFlowState.Finalized]: 6, + } + + private ensureStateIn (where: string, allowed: DKGFlowState[]) { + if (!allowed.includes(this.state)) { + const allowedStr = allowed.join('|') + throw new Error(`${where} invalid state: ${this.state}; allowed: ${allowedStr}`) + } + } + + private ensureStateAtLeast (where: string, min: DKGFlowState) { + if (this.stateOrder[this.state] < this.stateOrder[min]) { + throw new Error(`${where} requires state >= ${min}, current=${this.state}`) + } + } + + getState () { return this.state } + private getAllDealerCommitments (): Point[][] { + // must have collected complete commitments set + this.ensureStateAtLeast('getAllDealerCommitments', DKGFlowState.CommitmentsCollected) + const rosterIds = Object.keys(this.roster || {}).map(Number).sort((a, b) => a - b) + if (!rosterIds.length) throw new Error('roster not assigned') + const ids = Object.keys(this.commitmentsByDealerId).map(Number).sort((a, b) => a - b) if (ids.length === 0) throw new Error('no dealer commitments have been added') - // If we have a roster, ensure we have commitments from every dealer id in the roster - const rosterIds = Object.keys(this.roster).map(Number).sort((a, b) => a - b) - if (rosterIds.length > 0) { - if (ids.length !== rosterIds.length || ids.some((id, i) => id !== rosterIds[i])) { - throw new Error('missing commitments for one or more dealers') - } + if (ids.length !== rosterIds.length || ids.some((id, i) => id !== rosterIds[i])) { + throw new Error('missing commitments for one or more dealers') } const ordered: Point[][] = [] for (const id of ids) ordered.push(this.commitmentsByDealerId[id]!) @@ -48,6 +89,9 @@ class DKGManager { } runTrustedKeygen (privateKey: bigint, desiredShares: number, threshold: number) { + if (!Number.isInteger(desiredShares) || desiredShares <= 0) throw new Error('desiredShares must be a positive integer') + if (!Number.isInteger(threshold) || threshold <= 0) throw new Error('threshold must be a positive integer') + if (threshold > desiredShares) throw new Error('threshold cannot exceed desiredShares') const result = this.dkg.trustedDealerKeygen(privateKey, desiredShares, threshold) const groupInfo = this.dkg.deriveGroupInfo(desiredShares, threshold, result.vssCommitment) @@ -70,26 +114,50 @@ class DKGManager { } assignIdentifier (identifier: number) { + if (!Number.isInteger(identifier) || identifier <= 0) throw new Error('bad participant identifier') this.participantID = identifier } // id and pubkey assignRoster (roster: Record) { - this.roster = roster - for (const id in roster) { + this.ensureStateIn('assignRoster', [DKGFlowState.Init]) + // Basic shape validation + if (!roster || typeof roster !== 'object') throw new Error('invalid roster') + const ids = Object.keys(roster).map(Number).sort((a, b) => a - b) + if (!ids.length) throw new Error('empty roster') + ids.forEach((id) => { if (!Number.isInteger(id) || id <= 0) throw new Error(`bad roster id: ${id}`) }) + + this.roster = {} + this.keysByID = {} + let matchedSelf = false + for (const idStr in roster) { + const id = Number(idStr) const key = roster[id]! - // assert.deepStrictEqual(this.pubComKey, key, '') + if (!(key instanceof Uint8Array) || key.length !== 32) throw new Error(`bad roster pubkey for id ${id}`) + this.roster[id] = key if (bufferToBigInt(this.pubComKey) === bufferToBigInt(key)) { - this.assignIdentifier(Number(id)) + this.assignIdentifier(id) + matchedSelf = true } - // pad key here. const shared = this.getSharedSecret(key) + if (!shared || shared.length !== 32) throw new Error(`failed to derive shared secret for id ${id}`) this.keysByID[id] = shared } + if (!matchedSelf) throw new Error('our announcement pubKey not present in roster') + this.state = DKGFlowState.RosterAssigned } commitmentRound (secret: bigint, desiredShares: number, threshold: number) { - if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') + if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier') + this.ensureStateIn('commitmentRound', [DKGFlowState.RosterAssigned]) + if (!Number.isInteger(desiredShares) || desiredShares <= 0) throw new Error('desiredShares must be a positive integer') + if (!Number.isInteger(threshold) || threshold <= 0) throw new Error('threshold must be a positive integer') + if (threshold > desiredShares) throw new Error('threshold cannot exceed desiredShares') + + // Ensure roster matches desiredShares + const rosterIds = Object.keys(this.roster).map(Number).sort((a, b) => a - b) + if (rosterIds.length !== desiredShares) throw new Error('roster size does not match desiredShares') + const { coefficients, vssCommitment: commitments } = this.dkg.trustedDealerKeygen(secret, desiredShares, threshold) const recipientIds = [] for (let id = 1; id <= desiredShares; id++) { @@ -98,68 +166,101 @@ class DKGManager { this.recipientIds = recipientIds const shares = this.dkg.computeSharesForIds(coefficients, recipientIds) this.shares = shares + this.state = DKGFlowState.CommitmentsCreated this.addParticipantCommitments(this.participantID!, commitments) return { shares, commitments } } addParticipantCommitments (particpantID: number, participantCommitments: Point[]) { + this.ensureStateIn('addParticipantCommitments', [DKGFlowState.CommitmentsCreated, DKGFlowState.CommitmentsCollected]) if (!Number.isInteger(particpantID) || particpantID <= 0) throw new Error('bad participant id for commitments') if (!participantCommitments?.length) throw new Error('empty commitments from participant') this.commitmentsByDealerId[particpantID] = participantCommitments + // If roster is known and we have a full set of commitments, advance state + const rosterIds = Object.keys(this.roster || {}).map(Number).sort((a, b) => a - b) + const commitIds = Object.keys(this.commitmentsByDealerId).map(Number).sort((a, b) => a - b) + if (rosterIds.length && rosterIds.length === commitIds.length && rosterIds.every((id, i) => id === commitIds[i])) { + this.state = DKGFlowState.CommitmentsCollected + } } addEncryptedShares (particpantID: number, shares: Record) { + this.ensureStateAtLeast('addEncryptedShares', DKGFlowState.CommitmentsCollected) + if (!Number.isInteger(particpantID) || particpantID <= 0) throw new Error('bad dealer id for encrypted shares') + if (!shares || typeof shares !== 'object') throw new Error('invalid encrypted shares bundle') + // Basic validation for our expected entry if we know our id + if (typeof this.participantID !== 'undefined') { + const mine = shares[this.participantID] + if (!mine || !(mine.nonce instanceof Uint8Array) || !(mine.ciphertext instanceof Uint8Array)) { + // Allow storing anyway, but surface a strong error when decrypting + } + } this.encryptedShares[particpantID] = shares + const rosterIds = Object.keys(this.roster || {}).map(Number).sort((a, b) => a - b) + const encIds = Object.keys(this.encryptedShares).map(Number).sort((a, b) => a - b) + if (rosterIds.length && rosterIds.length === encIds.length && rosterIds.every((id, i) => id === encIds[i])) { + this.state = DKGFlowState.EncryptedSharesCollected + } } getEncryptedShares () { // make sure the commitments are not empty, and same length as 'max participants' + this.ensureStateIn('getEncryptedShares', [DKGFlowState.CommitmentsCollected]) + if (!Object.keys(this.shares).length) throw new Error('no local shares computed; run commitmentRound first') const allDealerCommitments = this.getAllDealerCommitments() - return this.dkg.encryptSharesAESGCMWithAAD(this.shares, this.keysByID, allDealerCommitments) + // Ensure we have derived a shared key for every intended recipient + const rosterIds = Object.keys(this.roster).map(Number).sort((a, b) => a - b) + for (const id of rosterIds) { + const k = this.keysByID[id] + if (!k || k.length !== 32) throw new Error(`missing shared key for participant ${id}`) + } + const out = this.dkg.encryptSharesAESGCMWithAAD(this.shares, this.keysByID, allDealerCommitments) + this.state = DKGFlowState.SharesEncrypted + return out } getDecryptedShares () { - if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') - const decryptedByDealer: Record = {} + if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier') + this.ensureStateIn('getDecryptedShares', [DKGFlowState.EncryptedSharesCollected]) const allDealerCommitments = this.getAllDealerCommitments() - for (const dealerIdStr in this.encryptedShares) { - const dealerId = Number(dealerIdStr) + const decryptedByDealer: Record = {} + const rosterIds = Object.keys(this.roster).map(Number).sort((a, b) => a - b) + + const missing: number[] = [] + for (const dealerId of rosterIds) { const bundle = this.encryptedShares[dealerId] const enc = bundle?.[this.participantID] - if (!enc) { - console.error(`No encrypted share for our id ${this.participantID} from dealer ${dealerId}`) - continue - } const key = this.keysByID[dealerId] - if (!key) { - console.error(`Missing shared key for dealer ${dealerId}`) + if (!bundle || !enc || !key) { + missing.push(dealerId) continue } - try { - const decrypted = this.dkg.decryptShareAESGCMWithAAD( - enc, - key, - this.participantID, - allDealerCommitments - ) - decryptedByDealer[dealerId] = decrypted - } catch (error) { - console.error(`Failed to decrypt our share from dealer ${dealerId}:`, error) - } + const decrypted = this.dkg.decryptShareAESGCMWithAAD( + enc, + key, + this.participantID, + allDealerCommitments + ) + decryptedByDealer[dealerId] = decrypted + } + if (missing.length) { + throw new Error(`missing encrypted shares or keys from dealers: ${missing.join(', ')}`) } return decryptedByDealer } finalize () { - if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier.') + if (typeof this.participantID === 'undefined') throw new Error('missing participant identifier') + this.ensureStateIn('finalize', [DKGFlowState.EncryptedSharesCollected]) const dec = this.getDecryptedShares() - const shares = [] + const shares: Array<{ dealerId: number; s_ki: bigint }> = [] for (const d in dec) { - const share = { dealerId: Number(d), s_ki: dec[d]! } - shares.push(share) + shares.push({ dealerId: Number(d), s_ki: dec[d]! }) } const allDealerCommitments = this.getAllDealerCommitments() - return this.dkg.finalizeParticipant(this.participantID, shares, allDealerCommitments) + const res = this.dkg.finalizeParticipant(this.participantID, shares, allDealerCommitments) + this.state = DKGFlowState.Finalized + return res } // used for encrypting/decrypting shares. diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index 2db900a..b21a442 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -5,6 +5,7 @@ import { describe, it } from 'node:test' import DKGManager from '../src/manager/dkg' import FROSTSigningManager from '../src/manager/signing' import { eddsaBuild, type Point } from '../src' +import { bigIntToBuffer } from '@zk-kit/utils' describe('DKGManager e2e flow test', () => { it('should run trusted keygen and get expected group publickey', () => { @@ -131,7 +132,7 @@ describe('DKGManager e2e flow test', () => { for (const sm of signers) for (const p of partials) sm.recievePartials(p) const sig = signers[0]!.finalize(msg) - const ok = eddsaBuild.verifyPoseidon(signers[0]!.frost.toBytes(msg).toReversed(), sig, groupPK) + const ok = eddsaBuild.verifyPoseidon(bigIntToBuffer(msg), sig, groupPK) assert.strictEqual(ok, true, 'coordinator-less flow signing verification failed') }) @@ -199,7 +200,7 @@ describe('DKGManager e2e flow test', () => { for (const sm of signers) for (const p of partials) sm.recievePartials(p) const sig = signers[0]!.finalize(msg) - const ok = eddsaBuild.verifyPoseidon(signers[0]!.frost.toBytes(msg).toReversed(), sig, groupPublicKey) + const ok = eddsaBuild.verifyPoseidon(bigIntToBuffer(msg), sig, groupPublicKey) assert.strictEqual(ok, true, 'coordinator-less flow signing verification failed') }) }) \ No newline at end of file From 4bdddc69b562e14c79fb549a8a3ed5d597df5f5b Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Wed, 22 Oct 2025 13:29:05 -0600 Subject: [PATCH 19/44] feat: add coordinator-less end-to-end tests for TrustedDKG and extensions --- test/trusted-dkg-coordinatorless-e2e.test.ts | 90 --------- test/trusted-dkg-e2e.test.ts | 187 +++++++++++++++++++ test/trusted-dkg-extensions.test.ts | 114 ----------- 3 files changed, 187 insertions(+), 204 deletions(-) delete mode 100644 test/trusted-dkg-coordinatorless-e2e.test.ts delete mode 100644 test/trusted-dkg-extensions.test.ts diff --git a/test/trusted-dkg-coordinatorless-e2e.test.ts b/test/trusted-dkg-coordinatorless-e2e.test.ts deleted file mode 100644 index 94b1ecb..0000000 --- a/test/trusted-dkg-coordinatorless-e2e.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* eslint-disable jsdoc/require-jsdoc */ -import assert from 'node:assert' -import { describe, it } from 'node:test' - -import type { Point } from '@zk-kit/baby-jubjub' - -import TrustedDKG from '../src/frost/trusted-dkg' -import BabyFROST from '../src/frost/babyfrost' -import { eddsaBuild } from '../src/eddsa' -import type { Commitment } from '../src/frost/types' -import { poseidonHex } from '../src/index' - -describe('TrustedDKG coordinator-less end-to-end', () => { - const dkg = new TrustedDKG() - - it('multi-dealer 3-of-5: per-dealer shares -> finalize -> reconstruct', () => { - const threshold = 3 - const ids = [1, 2, 3, 4, 5] - - - const dealers: Array<{ id: number; coeffs: bigint[] }> = [ - { id: 1, coeffs: dkg.trustedDealerKeygen(0x11n, 5, threshold).coefficients }, - { id: 2, coeffs: dkg.trustedDealerKeygen(0x22n, 5, threshold).coefficients }, - { id: 3, coeffs: dkg.trustedDealerKeygen(0x33n, 5, threshold).coefficients }, - { id: 4, coeffs: dkg.trustedDealerKeygen(0x44n, 5, threshold).coefficients }, - { id: 5, coeffs: dkg.trustedDealerKeygen(0x55n, 5, threshold).coefficients }, - ] - - // Commitments and per-dealer shares - const commitmentsByDealer: Record[]> = {} - const sharesByDealer: Record> = {} - for (const d of dealers) { - const C = dkg.vssCommit(d.coeffs) - assert.equal(C.length, threshold, 'each dealer must produce t commitments') - commitmentsByDealer[d.id] = C - sharesByDealer[d.id] = dkg.computeSharesForIds(d.coeffs, ids) - } - - // Aggregate commitments for group PK - const allDealerCommitments = dealers.map(d => commitmentsByDealer[d.id]!) - assert.strictEqual(dkg.verifyAllCommitmentsSubgroup(allDealerCommitments), true, 'commitments not in subgroup') - const PKGroup = dkg.combineGroupPubkeyFromCommitments(allDealerCommitments) - - // Check group PK equals Base*(sum of dealers' a0) - const a0_total = dealers.reduce((acc, d) => dkg.modOrder(acc + dkg.modOrder(d.coeffs[0]!)), 0n) - const PKDirect = dkg.ScalarBaseMult(a0_total) - assert.strictEqual(dkg.pointsEqual(PKGroup, PKDirect), true, 'group PK does not match sum of a0') - - // Feldman verify each per-dealer share, then finalize per participant by summing s_{k}(i) - const finalized: Array<{ id: number; skShare: bigint }> = [] - for (const id of ids) { - const s_ki_byDealer = dealers.map(d => ({ dealerId: d.id, s_ki: sharesByDealer[d.id]![id]! })) - // verify each dealer share for this participant id - for (const d of dealers) { - const ok = dkg.vssVerify({ i: BigInt(id), sk_i: sharesByDealer[d.id]![id]! }, commitmentsByDealer[d.id]!, threshold) - assert.strictEqual(ok, true, `Feldman verify failed for dealer ${d.id}, id ${id}`) - } - const res = dkg.finalizeParticipant(id, s_ki_byDealer, allDealerCommitments) - const expectedDiv8 = s_ki_byDealer.reduce((acc, s) => dkg.modOrder(acc + dkg.modOrder(s.s_ki)), 0n) - assert.equal(res.share.skShareDiv8, expectedDiv8, 'finalized share mismatch') - assert.strictEqual(dkg.pointsEqual(res.PKGroup, PKGroup), true, 'PKGroup mismatch in finalization') - finalized.push({ id, skShare: res.share.skShare }) - } - - // Reconstruct total a0 from aggregated shares of any 3 participants (e.g., ids 1,2,5) - const recIds = [1, 2, 5] - const aggregatedShares = recIds.map((id) => ({ - id, - s_i: dealers.reduce((acc, d) => dkg.modOrder(acc + dkg.modOrder(sharesByDealer[d.id]![id]!)), 0n) - })) - const rec = dkg.reconstructConstantFromShares(aggregatedShares) - assert.equal(rec, a0_total, 'reconstructed total a0 should equal sum of dealer a0') - - // FROST signing with 3 finalized participants; verify aggregate - const frost = new BabyFROST() - const groupPublicKey = PKGroup - const signingSubset = finalized.slice(0, threshold) - const msgHash = BigInt('0x' + poseidonHex(['0x' + 99999n.toString(16)], true)) - const p1 = frost.commit(signingSubset[0]!.skShare, BigInt(signingSubset[0]!.id)) - const p2 = frost.commit(signingSubset[1]!.skShare, BigInt(signingSubset[1]!.id)) - const p3 = frost.commit(signingSubset[2]!.skShare, BigInt(signingSubset[2]!.id)) - const commitmentList: Commitment[] = [p1, p2, p3].map((a) => ({ ...a.commitments })) - const s1 = frost.sign(p1.commitments.identifier, signingSubset[0]!.skShare, groupPublicKey, p1.nonces, msgHash, commitmentList) - const s2 = frost.sign(p2.commitments.identifier, signingSubset[1]!.skShare, groupPublicKey, p2.nonces, msgHash, commitmentList) - const s3 = frost.sign(p3.commitments.identifier, signingSubset[2]!.skShare, groupPublicKey, p3.nonces, msgHash, commitmentList) - const sig = frost.aggregate(commitmentList, msgHash, groupPublicKey, [s1, s2, s3]) - const okAgg = eddsaBuild.verifyPoseidon(frost.toBytes(msgHash).toReversed(), sig, groupPublicKey) - assert.strictEqual(okAgg, true, 'FROST aggregate failed verification') - }) -}) diff --git a/test/trusted-dkg-e2e.test.ts b/test/trusted-dkg-e2e.test.ts index 0c74781..1471aac 100644 --- a/test/trusted-dkg-e2e.test.ts +++ b/test/trusted-dkg-e2e.test.ts @@ -76,3 +76,190 @@ describe('TrustedDKG end-to-end', () => { assert.strictEqual(okAgg, true, 'FROST aggregate failed verification') }) }) + +describe('TrustedDKG coordinator-less end-to-end', () => { + const dkg = new TrustedDKG() + + it('multi-dealer 3-of-5: per-dealer shares -> finalize -> reconstruct', () => { + const threshold = 3 + const ids = [1, 2, 3, 4, 5] + + + const dealers: Array<{ id: number; coeffs: bigint[] }> = [ + { id: 1, coeffs: dkg.trustedDealerKeygen(0x11n, 5, threshold).coefficients }, + { id: 2, coeffs: dkg.trustedDealerKeygen(0x22n, 5, threshold).coefficients }, + { id: 3, coeffs: dkg.trustedDealerKeygen(0x33n, 5, threshold).coefficients }, + { id: 4, coeffs: dkg.trustedDealerKeygen(0x44n, 5, threshold).coefficients }, + { id: 5, coeffs: dkg.trustedDealerKeygen(0x55n, 5, threshold).coefficients }, + ] + + // Commitments and per-dealer shares + const commitmentsByDealer: Record[]> = {} + const sharesByDealer: Record> = {} + for (const d of dealers) { + const C = dkg.vssCommit(d.coeffs) + assert.equal(C.length, threshold, 'each dealer must produce t commitments') + commitmentsByDealer[d.id] = C + sharesByDealer[d.id] = dkg.computeSharesForIds(d.coeffs, ids) + } + + // Aggregate commitments for group PK + const allDealerCommitments = dealers.map(d => commitmentsByDealer[d.id]!) + assert.strictEqual(dkg.verifyAllCommitmentsSubgroup(allDealerCommitments), true, 'commitments not in subgroup') + const PKGroup = dkg.combineGroupPubkeyFromCommitments(allDealerCommitments) + + // Check group PK equals Base*(sum of dealers' a0) + const a0_total = dealers.reduce((acc, d) => dkg.modOrder(acc + dkg.modOrder(d.coeffs[0]!)), 0n) + const PKDirect = dkg.ScalarBaseMult(a0_total) + assert.strictEqual(dkg.pointsEqual(PKGroup, PKDirect), true, 'group PK does not match sum of a0') + + // Feldman verify each per-dealer share, then finalize per participant by summing s_{k}(i) + const finalized: Array<{ id: number; skShare: bigint }> = [] + for (const id of ids) { + const s_ki_byDealer = dealers.map(d => ({ dealerId: d.id, s_ki: sharesByDealer[d.id]![id]! })) + // verify each dealer share for this participant id + for (const d of dealers) { + const ok = dkg.vssVerify({ i: BigInt(id), sk_i: sharesByDealer[d.id]![id]! }, commitmentsByDealer[d.id]!, threshold) + assert.strictEqual(ok, true, `Feldman verify failed for dealer ${d.id}, id ${id}`) + } + const res = dkg.finalizeParticipant(id, s_ki_byDealer, allDealerCommitments) + const expectedDiv8 = s_ki_byDealer.reduce((acc, s) => dkg.modOrder(acc + dkg.modOrder(s.s_ki)), 0n) + assert.equal(res.share.skShareDiv8, expectedDiv8, 'finalized share mismatch') + assert.strictEqual(dkg.pointsEqual(res.PKGroup, PKGroup), true, 'PKGroup mismatch in finalization') + finalized.push({ id, skShare: res.share.skShare }) + } + + // Reconstruct total a0 from aggregated shares of any 3 participants (e.g., ids 1,2,5) + const recIds = [1, 2, 5] + const aggregatedShares = recIds.map((id) => ({ + id, + s_i: dealers.reduce((acc, d) => dkg.modOrder(acc + dkg.modOrder(sharesByDealer[d.id]![id]!)), 0n) + })) + const rec = dkg.reconstructConstantFromShares(aggregatedShares) + assert.equal(rec, a0_total, 'reconstructed total a0 should equal sum of dealer a0') + + // FROST signing with 3 finalized participants; verify aggregate + const frost = new BabyFROST() + const groupPublicKey = PKGroup + const signingSubset = finalized.slice(0, threshold) + const msgHash = BigInt('0x' + poseidonHex(['0x' + 99999n.toString(16)], true)) + const p1 = frost.commit(signingSubset[0]!.skShare, BigInt(signingSubset[0]!.id)) + const p2 = frost.commit(signingSubset[1]!.skShare, BigInt(signingSubset[1]!.id)) + const p3 = frost.commit(signingSubset[2]!.skShare, BigInt(signingSubset[2]!.id)) + const commitmentList: Commitment[] = [p1, p2, p3].map((a) => ({ ...a.commitments })) + const s1 = frost.sign(p1.commitments.identifier, signingSubset[0]!.skShare, groupPublicKey, p1.nonces, msgHash, commitmentList) + const s2 = frost.sign(p2.commitments.identifier, signingSubset[1]!.skShare, groupPublicKey, p2.nonces, msgHash, commitmentList) + const s3 = frost.sign(p3.commitments.identifier, signingSubset[2]!.skShare, groupPublicKey, p3.nonces, msgHash, commitmentList) + const sig = frost.aggregate(commitmentList, msgHash, groupPublicKey, [s1, s2, s3]) + const okAgg = eddsaBuild.verifyPoseidon(frost.toBytes(msgHash).toReversed(), sig, groupPublicKey) + assert.strictEqual(okAgg, true, 'FROST aggregate failed verification') + }) +}) + + +describe('TrustedDKG coordinator-less extensions', () => { + const dkg = new TrustedDKG() + + // fixed dealer coefficients for determinism (include constant term a0 first) + const dealer1Coeffs = [ + 0x11n, // a0 + 0x22n, // a1 + 0x33n, // a2 (threshold = 3) + ] + const dealer2Coeffs = [ + 0x55n, + 0x66n, + 0x77n, + ] + + it('computeSharesForIds matches polynomialEvaluate()', () => { + const ids = [1, 2, 4] + const shares = dkg.computeSharesForIds(dealer1Coeffs, ids) + for (const id of ids) { + const p = dkg.polynomialEvaluate(BigInt(id), dealer1Coeffs) + assert.equal(shares[id], p, `share mismatch for id ${id}`) + } + }) + + it('vssVerify succeeds for valid shares and fails for tampered', () => { + const commitments = dkg.vssCommit(dealer1Coeffs) + const ids = [1, 2, 3] + const shares = dkg.computeSharesForIds(dealer1Coeffs, ids) + for (const id of ids) { + const ok = dkg.vssVerify({ i: BigInt(id), sk_i: shares[id]!}, commitments, commitments.length) + assert.strictEqual(ok, true, `Feldman verify failed for id ${id}`) + const bad = dkg.vssVerify({i: BigInt(id), sk_i: dkg.modOrder(shares[id]! + 1n)}, commitments, commitments.length) + assert.strictEqual(bad, false, `Feldman verify should fail for tampered share id ${id}`) + } + }) + + it('combineGroupPubkeyFromCommitments equals Base*(sum of a0)', () => { + const c1 = dkg.vssCommit(dealer1Coeffs) + const c2 = dkg.vssCommit(dealer2Coeffs) + const group = dkg.combineGroupPubkeyFromCommitments([c1, c2]) + const sumA0 = dkg.modOrder(dealer1Coeffs[0]! + dealer2Coeffs[0]!) + const direct = dkg.ScalarBaseMult(sumA0) + assert.strictEqual(dkg.pointsEqual(group, direct), true, 'group PK mismatch with a0 sum') + }) + + it('verifyAllCommitmentsSubgroup returns true for valid commitments', () => { + const c1 = dkg.vssCommit(dealer1Coeffs) + const c2 = dkg.vssCommit(dealer2Coeffs) + const ok = dkg.verifyAllCommitmentsSubgroup([c1, c2]) + assert.strictEqual(ok, true) + }) + + it('finalizeParticipant aggregates s_ki and derives PKGroup', () => { + const c1 = dkg.vssCommit(dealer1Coeffs) + const c2 = dkg.vssCommit(dealer2Coeffs) + const allComm = [c1, c2] + const id = 2 + const s1 = dkg.polynomialEvaluate(BigInt(id), dealer1Coeffs) + const s2 = dkg.polynomialEvaluate(BigInt(id), dealer2Coeffs) + const res = dkg.finalizeParticipant(id, [ + { dealerId: 1, s_ki: s1 }, + { dealerId: 2, s_ki: s2 }, + ], allComm) + const expectedDiv8 = dkg.modOrder(s1 + s2) + assert.equal(res.share.skShareDiv8, expectedDiv8, 'skShareDiv8 should be sum of dealer shares') + assert.equal(res.share.skShare, dkg.modOrder(8n * expectedDiv8), 'skShare should be x8 mod L') + const group = dkg.combineGroupPubkeyFromCommitments(allComm) + assert.strictEqual(dkg.pointsEqual(res.PKGroup, group), true, 'PKGroup mismatch') + }) + + it('assertSortedConsecutiveIds: accepts non-consecutive, rejects invalid/unsorted', () => { + // non-consecutive is allowed + assert.doesNotThrow(() => dkg.computeSharesForIds(dealer1Coeffs, [1, 3])) + // unsorted must throw + assert.throws(() => dkg.computeSharesForIds(dealer1Coeffs, [3, 1]), /strictly increasing positive integers/i) + // invalid must throw + assert.throws(() => dkg.computeSharesForIds(dealer1Coeffs, [0, 2]), /strictly increasing positive integers/i) + }) + + it('Lagrange basis sums to 1 and reconstructs constant term', () => { + // simple linear polynomial a0 + a1*x + const a0 = 0x1234n + const a1 = 0x9n + const coeffs = [a0, a1] + const ids = [1n, 3n] + const shares = ids.map((id) => ({ id: Number(id), s_i: dkg.polynomialEvaluate(id, coeffs) })) + const lambdas = ids.map((id) => dkg.deriveInterpolatingValue(ids, id)) + const sum = lambdas.reduce((acc, l) => dkg.modOrder(acc + l), 0n) + assert.equal(sum, 1n, 'sum of λ_i(0) should be 1') + const rec = dkg.reconstructConstantFromShares(shares) + assert.equal(rec, a0, 'reconstructed a0 should equal original') + }) + + it('verifyFeldmanShare rejects tampered/non-subgroup commitment', () => { + const coeffs = [0x21n, 0x31n, 0x41n] + const commits = dkg.vssCommit(coeffs) + const id = 2 + const s = dkg.polynomialEvaluate(BigInt(id), coeffs) + // tamper C0 with a bogus point (synthetic non-subgroup/invalid) + const tampered = commits.slice() + // Intentionally use an arbitrary pair; strict verifier should return false or gracefully handle + tampered[0] = [1n, 1n] as any + const ok = dkg.verifyFeldmanShare(id, s, tampered) + assert.strictEqual(ok, false, 'tampered/non-subgroup commitment should fail Feldman verification') + }) +}) diff --git a/test/trusted-dkg-extensions.test.ts b/test/trusted-dkg-extensions.test.ts deleted file mode 100644 index 7feb68c..0000000 --- a/test/trusted-dkg-extensions.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable jsdoc/require-jsdoc */ -import assert from 'node:assert' -import { describe, it } from 'node:test' - -import type { Point } from '@zk-kit/baby-jubjub' - -import TrustedDKG from '../src/frost/trusted-dkg' - -describe('TrustedDKG coordinator-less extensions', () => { - const dkg = new TrustedDKG() - - // fixed dealer coefficients for determinism (include constant term a0 first) - const dealer1Coeffs = [ - 0x11n, // a0 - 0x22n, // a1 - 0x33n, // a2 (threshold = 3) - ] - const dealer2Coeffs = [ - 0x55n, - 0x66n, - 0x77n, - ] - - it('computeSharesForIds matches polynomialEvaluate()', () => { - const ids = [1, 2, 4] - const shares = dkg.computeSharesForIds(dealer1Coeffs, ids) - for (const id of ids) { - const p = dkg.polynomialEvaluate(BigInt(id), dealer1Coeffs) - assert.equal(shares[id], p, `share mismatch for id ${id}`) - } - }) - - it('vssVerify succeeds for valid shares and fails for tampered', () => { - const commitments = dkg.vssCommit(dealer1Coeffs) - const ids = [1, 2, 3] - const shares = dkg.computeSharesForIds(dealer1Coeffs, ids) - for (const id of ids) { - const ok = dkg.vssVerify({ i: BigInt(id), sk_i: shares[id]!}, commitments, commitments.length) - assert.strictEqual(ok, true, `Feldman verify failed for id ${id}`) - const bad = dkg.vssVerify({i: BigInt(id), sk_i: dkg.modOrder(shares[id]! + 1n)}, commitments, commitments.length) - assert.strictEqual(bad, false, `Feldman verify should fail for tampered share id ${id}`) - } - }) - - it('combineGroupPubkeyFromCommitments equals Base*(sum of a0)', () => { - const c1 = dkg.vssCommit(dealer1Coeffs) - const c2 = dkg.vssCommit(dealer2Coeffs) - const group = dkg.combineGroupPubkeyFromCommitments([c1, c2]) - const sumA0 = dkg.modOrder(dealer1Coeffs[0]! + dealer2Coeffs[0]!) - const direct = dkg.ScalarBaseMult(sumA0) - assert.strictEqual(dkg.pointsEqual(group, direct), true, 'group PK mismatch with a0 sum') - }) - - it('verifyAllCommitmentsSubgroup returns true for valid commitments', () => { - const c1 = dkg.vssCommit(dealer1Coeffs) - const c2 = dkg.vssCommit(dealer2Coeffs) - const ok = dkg.verifyAllCommitmentsSubgroup([c1, c2]) - assert.strictEqual(ok, true) - }) - - it('finalizeParticipant aggregates s_ki and derives PKGroup', () => { - const c1 = dkg.vssCommit(dealer1Coeffs) - const c2 = dkg.vssCommit(dealer2Coeffs) - const allComm = [c1, c2] - const id = 2 - const s1 = dkg.polynomialEvaluate(BigInt(id), dealer1Coeffs) - const s2 = dkg.polynomialEvaluate(BigInt(id), dealer2Coeffs) - const res = dkg.finalizeParticipant(id, [ - { dealerId: 1, s_ki: s1 }, - { dealerId: 2, s_ki: s2 }, - ], allComm) - const expectedDiv8 = dkg.modOrder(s1 + s2) - assert.equal(res.share.skShareDiv8, expectedDiv8, 'skShareDiv8 should be sum of dealer shares') - assert.equal(res.share.skShare, dkg.modOrder(8n * expectedDiv8), 'skShare should be x8 mod L') - const group = dkg.combineGroupPubkeyFromCommitments(allComm) - assert.strictEqual(dkg.pointsEqual(res.PKGroup, group), true, 'PKGroup mismatch') - }) - - it('assertSortedConsecutiveIds: accepts non-consecutive, rejects invalid/unsorted', () => { - // non-consecutive is allowed - assert.doesNotThrow(() => dkg.computeSharesForIds(dealer1Coeffs, [1, 3])) - // unsorted must throw - assert.throws(() => dkg.computeSharesForIds(dealer1Coeffs, [3, 1]), /strictly increasing positive integers/i) - // invalid must throw - assert.throws(() => dkg.computeSharesForIds(dealer1Coeffs, [0, 2]), /strictly increasing positive integers/i) - }) - - it('Lagrange basis sums to 1 and reconstructs constant term', () => { - // simple linear polynomial a0 + a1*x - const a0 = 0x1234n - const a1 = 0x9n - const coeffs = [a0, a1] - const ids = [1n, 3n] - const shares = ids.map((id) => ({ id: Number(id), s_i: dkg.polynomialEvaluate(id, coeffs) })) - const lambdas = ids.map((id) => dkg.deriveInterpolatingValue(ids, id)) - const sum = lambdas.reduce((acc, l) => dkg.modOrder(acc + l), 0n) - assert.equal(sum, 1n, 'sum of λ_i(0) should be 1') - const rec = dkg.reconstructConstantFromShares(shares) - assert.equal(rec, a0, 'reconstructed a0 should equal original') - }) - - it('verifyFeldmanShare rejects tampered/non-subgroup commitment', () => { - const coeffs = [0x21n, 0x31n, 0x41n] - const commits = dkg.vssCommit(coeffs) - const id = 2 - const s = dkg.polynomialEvaluate(BigInt(id), coeffs) - // tamper C0 with a bogus point (synthetic non-subgroup/invalid) - const tampered = commits.slice() - // Intentionally use an arbitrary pair; strict verifier should return false or gracefully handle - tampered[0] = [1n, 1n] as any - const ok = dkg.verifyFeldmanShare(id, s, tampered) - assert.strictEqual(ok, false, 'tampered/non-subgroup commitment should fail Feldman verification') - }) -}) From 845fee313cc57a2ec186333e67afd09cb2ece994 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Wed, 22 Oct 2025 13:30:52 -0600 Subject: [PATCH 20/44] feat: add detailed test cases for TrustedDKG and remove deprecated test file --- test/trusted-dkg-e2e.test.ts | 113 +++++++++++++++++++++++++++++++++ test/trusted-dkg.test.ts | 119 ----------------------------------- test/vss-extensions.test.ts | 2 +- test/vss.test.ts | 2 +- 4 files changed, 115 insertions(+), 121 deletions(-) delete mode 100644 test/trusted-dkg.test.ts diff --git a/test/trusted-dkg-e2e.test.ts b/test/trusted-dkg-e2e.test.ts index 1471aac..aa6f46e 100644 --- a/test/trusted-dkg-e2e.test.ts +++ b/test/trusted-dkg-e2e.test.ts @@ -10,6 +10,119 @@ import { eddsaBuild } from '../src/eddsa' import type { Commitment } from '../src/frost/types' import { poseidonHex } from '../src/index' +describe('TrustedDKG', () => { + const dkg = new TrustedDKG() + + describe('Test Vector Validation', () => { + it('should match FROST test vectors for 3-of-5 threshold scheme', () => { + const secret = 0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn + const a1 = 0x57ea51a2f5712861d3e3125c7b81f081b19c40b04894abf76d35429f2eef8a3n + const a2 = 0x439109f6f2946257f8c508718b4a86e0cc73e1cae22dd0b7b8a20a59331d5b4n + + const expectedShares = [ + 0x1d4260025e6e520d8daab9c1f9923b0c94c6ee643f051ff2526517535133804n, // Participant 1 + 0x1d85f8d8e472e07cf9fb432c0debae008ff241ea77f69c1d8403ffb36343cf0n, // Participant 2 + 0x442308b78d3cf348e735520c35d77dfb70167d82e334d911afbd7f02497c653n, // Participant 3 + 0x3050f2b896694a1de4b85af56e52fa428144c5a9eeb0f6285e68177c71cad3cn, // Participant 4 + 0x42d853c1c25b254f6324e954ba60d390776bf5e32c79d408072d46e56e4189cn // Participant 5 + ] + + const expectedPubKeys = [ + [ + 0x2ebc885681e45848f34ee1050a827eca10e8fb24cbb64a091901d96911480cc2n, + 0x482f7210ba3665fbe23bc0eab393f7266a36008186bb7c6781b1017612e49b4n + ], + [ + 0x26dd7c7370505a2496012e97bae1a23e9439661f90c0f15aac26ed1cd606535fn, + 0x2a770f7fc5a26df9427cde7456ba495a2b2eb89c6a1be3273d9d982d5417bd01n + ], + [ + 0x1136b9efcaf9dc8b3b20bf463151645d49a2b33be95a4a36c3833e7a66453e65n, + 0x28146163f1cf5182296a46ed9523b521e37f8eb312ac3d209f4d0a629b474445n + ], + [ + 0x1a78e7b5302f95764b22448764ca2ecac418116b52332882d77868f2db086c33n, + 0x178597d7aea8a7e82b223cb8a0626d776a217caf7f1988bfa775d73d49e26f0bn + ], + [ + 0x2d91b9147ee0817f26f33eb7b759b4702577e429f80097aa07c7deb69dd6f562n, + 0x71e39bb200f3b4c3e6c54134c5c1382590dbdf8500303525f3b585647cbe8edn + ], + [ + 0x190260ab0b8674b7b20c22d37d9838cd5194d348745c7d4eba69feb1a5bfc749n, + 0x1c51721613b027faee6b242b213d8f534578c115d70746347d5590e10e2ce7f8n + ] + ] + + const expectedGroupPK = { + x: 0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6n, + y: 0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefdn + } + + const coefficients = [a1, a2] + const { secretKeyShares, coefficients: coeffs } = dkg.secretShareShard(secret, coefficients, 5) + + assert.strictEqual(coeffs[0], secret, 'First coefficient should be the secret') + assert.strictEqual(coeffs[1], a1, 'Second coefficient should match a1') + assert.strictEqual(coeffs[2], a2, 'Third coefficient should match a2') + + for (let i = 0; i < 5; i++) { + const share = secretKeyShares[i]! + assert.strictEqual(share.x_i, i + 1, `Participant ${i + 1} x-coordinate should be ${i + 1}`) + assert.strictEqual(share.y_i, expectedShares[i], `Participant ${i + 1} secret share should match test vector`) + } + + const vssCommitment = dkg.vssCommit(coeffs) + + for (let i = 1; i <= 5; i++) { + const participantKey = secretKeyShares[i - 1]! + const share = { + i: BigInt(participantKey.x_i), + sk_i: participantKey.y_i + } + const isValid = dkg.vssVerify(share, vssCommitment, 3) + assert.strictEqual(isValid, true, `Participant ${i} share should be valid`) + } + + const groupInfo = dkg.deriveGroupInfo(5, 3, vssCommitment) + groupInfo.participantPublicKeys.forEach((p, idx)=>{ + console.log(`Participant ${idx + 1}:`) + console.log(` Secret share: 0x${secretKeyShares[idx].y_i.toString(16)!}`) + console.log(` pubKey.x: 0x${p[0].toString(16)!}`) + console.log(` pubKey.y: 0x${p[1].toString(16)!}`) + assert.equal(p[0], expectedPubKeys[idx][0], 'Participant public key x-coordinate should match') + assert.equal(p[1], expectedPubKeys[idx][1], 'Participant public key y-coordinate should match') + }) + assert.strictEqual(groupInfo.PK![0], expectedGroupPK.x, 'Group public key x-coordinate should match') + assert.strictEqual(groupInfo.PK![1], expectedGroupPK.y, 'Group public key y-coordinate should match') + + const directGroupPK = dkg.ScalarBaseMult(secret) + assert(dkg.pointsEqual(groupInfo.PK!, directGroupPK), 'Group PK should equal secret * Base') + }) + + it('should correctly evaluate polynomial with test vector coefficients', () => { + const secret = 0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn + const a1 = 0x57ea51a2f5712861d3e3125c7b81f081b19c40b04894abf76d35429f2eef8a3n + const a2 = 0x439109f6f2946257f8c508718b4a86e0cc73e1cae22dd0b7b8a20a59331d5b4n + + const coefficients = [secret, a1, a2] + + const p1 = dkg.polynomialEvaluate(1n, coefficients) + + assert.strictEqual(p1, 0x1d4260025e6e520d8daab9c1f9923b0c94c6ee643f051ff2526517535133804n) + + const p2 = dkg.polynomialEvaluate(2n, coefficients) + assert.strictEqual(p2, 0x1d85f8d8e472e07cf9fb432c0debae008ff241ea77f69c1d8403ffb36343cf0n) + + const p3 = dkg.polynomialEvaluate(3n, coefficients) + assert.strictEqual(p3, 0x442308b78d3cf348e735520c35d77dfb70167d82e334d911afbd7f02497c653n) + + const p0 = dkg.polynomialEvaluate(0n, coefficients) + assert.strictEqual(p0, secret, 'Polynomial at x=0 should equal the secret') + }) + }) +}) + describe('TrustedDKG end-to-end', () => { const dkg = new TrustedDKG() diff --git a/test/trusted-dkg.test.ts b/test/trusted-dkg.test.ts deleted file mode 100644 index ef7f70e..0000000 --- a/test/trusted-dkg.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-disable jsdoc/require-jsdoc */ -/* eslint-disable camelcase */ -import assert from 'node:assert' -import { describe, it } from 'node:test' - -import TrustedDKG from '../src/frost/trusted-dkg' - -describe('TrustedDKG', () => { - const dkg = new TrustedDKG() - - describe('Test Vector Validation', () => { - it('should match FROST test vectors for 3-of-5 threshold scheme', () => { - const secret = 0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn - const a1 = 0x57ea51a2f5712861d3e3125c7b81f081b19c40b04894abf76d35429f2eef8a3n - const a2 = 0x439109f6f2946257f8c508718b4a86e0cc73e1cae22dd0b7b8a20a59331d5b4n - - const expectedShares = [ - 0x1d4260025e6e520d8daab9c1f9923b0c94c6ee643f051ff2526517535133804n, // Participant 1 - 0x1d85f8d8e472e07cf9fb432c0debae008ff241ea77f69c1d8403ffb36343cf0n, // Participant 2 - 0x442308b78d3cf348e735520c35d77dfb70167d82e334d911afbd7f02497c653n, // Participant 3 - 0x3050f2b896694a1de4b85af56e52fa428144c5a9eeb0f6285e68177c71cad3cn, // Participant 4 - 0x42d853c1c25b254f6324e954ba60d390776bf5e32c79d408072d46e56e4189cn // Participant 5 - ] - - const expectedPubKeys = [ - [ - 0x2ebc885681e45848f34ee1050a827eca10e8fb24cbb64a091901d96911480cc2n, - 0x482f7210ba3665fbe23bc0eab393f7266a36008186bb7c6781b1017612e49b4n - ], - [ - 0x26dd7c7370505a2496012e97bae1a23e9439661f90c0f15aac26ed1cd606535fn, - 0x2a770f7fc5a26df9427cde7456ba495a2b2eb89c6a1be3273d9d982d5417bd01n - ], - [ - 0x1136b9efcaf9dc8b3b20bf463151645d49a2b33be95a4a36c3833e7a66453e65n, - 0x28146163f1cf5182296a46ed9523b521e37f8eb312ac3d209f4d0a629b474445n - ], - [ - 0x1a78e7b5302f95764b22448764ca2ecac418116b52332882d77868f2db086c33n, - 0x178597d7aea8a7e82b223cb8a0626d776a217caf7f1988bfa775d73d49e26f0bn - ], - [ - 0x2d91b9147ee0817f26f33eb7b759b4702577e429f80097aa07c7deb69dd6f562n, - 0x71e39bb200f3b4c3e6c54134c5c1382590dbdf8500303525f3b585647cbe8edn - ], - [ - 0x190260ab0b8674b7b20c22d37d9838cd5194d348745c7d4eba69feb1a5bfc749n, - 0x1c51721613b027faee6b242b213d8f534578c115d70746347d5590e10e2ce7f8n - ] - ] - - const expectedGroupPK = { - x: 0x1e0762d6610a0b47f3b5e3f23f5f748fde5abb8843f33cf084c0dabd8dc813e6n, - y: 0xb82b739e78dda57e75ac680ef689df1158fe3eed8095c6d4bd1b2c7c166eefdn - } - - const coefficients = [a1, a2] - const { secretKeyShares, coefficients: coeffs } = dkg.secretShareShard(secret, coefficients, 5) - - assert.strictEqual(coeffs[0], secret, 'First coefficient should be the secret') - assert.strictEqual(coeffs[1], a1, 'Second coefficient should match a1') - assert.strictEqual(coeffs[2], a2, 'Third coefficient should match a2') - - for (let i = 0; i < 5; i++) { - const share = secretKeyShares[i]! - assert.strictEqual(share.x_i, i + 1, `Participant ${i + 1} x-coordinate should be ${i + 1}`) - assert.strictEqual(share.y_i, expectedShares[i], `Participant ${i + 1} secret share should match test vector`) - } - - const vssCommitment = dkg.vssCommit(coeffs) - - for (let i = 1; i <= 5; i++) { - const participantKey = secretKeyShares[i - 1]! - const share = { - i: BigInt(participantKey.x_i), - sk_i: participantKey.y_i - } - const isValid = dkg.vssVerify(share, vssCommitment, 3) - assert.strictEqual(isValid, true, `Participant ${i} share should be valid`) - } - - const groupInfo = dkg.deriveGroupInfo(5, 3, vssCommitment) - groupInfo.participantPublicKeys.forEach((p, idx)=>{ - console.log(`Participant ${idx + 1}:`) - console.log(` Secret share: 0x${secretKeyShares[idx].y_i.toString(16)!}`) - console.log(` pubKey.x: 0x${p[0].toString(16)!}`) - console.log(` pubKey.y: 0x${p[1].toString(16)!}`) - assert.equal(p[0], expectedPubKeys[idx][0], 'Participant public key x-coordinate should match') - assert.equal(p[1], expectedPubKeys[idx][1], 'Participant public key y-coordinate should match') - }) - assert.strictEqual(groupInfo.PK![0], expectedGroupPK.x, 'Group public key x-coordinate should match') - assert.strictEqual(groupInfo.PK![1], expectedGroupPK.y, 'Group public key y-coordinate should match') - - const directGroupPK = dkg.ScalarBaseMult(secret) - assert(dkg.pointsEqual(groupInfo.PK!, directGroupPK), 'Group PK should equal secret * Base') - }) - - it('should correctly evaluate polynomial with test vector coefficients', () => { - const secret = 0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn - const a1 = 0x57ea51a2f5712861d3e3125c7b81f081b19c40b04894abf76d35429f2eef8a3n - const a2 = 0x439109f6f2946257f8c508718b4a86e0cc73e1cae22dd0b7b8a20a59331d5b4n - - const coefficients = [secret, a1, a2] - - const p1 = dkg.polynomialEvaluate(1n, coefficients) - - assert.strictEqual(p1, 0x1d4260025e6e520d8daab9c1f9923b0c94c6ee643f051ff2526517535133804n) - - const p2 = dkg.polynomialEvaluate(2n, coefficients) - assert.strictEqual(p2, 0x1d85f8d8e472e07cf9fb432c0debae008ff241ea77f69c1d8403ffb36343cf0n) - - const p3 = dkg.polynomialEvaluate(3n, coefficients) - assert.strictEqual(p3, 0x442308b78d3cf348e735520c35d77dfb70167d82e334d911afbd7f02497c653n) - - const p0 = dkg.polynomialEvaluate(0n, coefficients) - assert.strictEqual(p0, secret, 'Polynomial at x=0 should equal the secret') - }) - }) -}) \ No newline at end of file diff --git a/test/vss-extensions.test.ts b/test/vss-extensions.test.ts index 8d7f585..27a9ddd 100644 --- a/test/vss-extensions.test.ts +++ b/test/vss-extensions.test.ts @@ -6,7 +6,7 @@ import type { Point } from '@zk-kit/baby-jubjub' import BabyFrostVSSDKG from '../src/frost/vss-dkg' -describe('VSS-DKG RFC-like extensions', () => { +describe.skip('VSS-DKG RFC-like extensions', () => { const vss = new BabyFrostVSSDKG() // simple deterministic participants diff --git a/test/vss.test.ts b/test/vss.test.ts index bf670e5..076320e 100644 --- a/test/vss.test.ts +++ b/test/vss.test.ts @@ -20,7 +20,7 @@ function sharesById (shs: Share[]): Record { return m } -describe('VSS DKG: aggregator learns nothing; ≥t can reconstruct, Feldman Verify', () => { +describe.skip('VSS DKG: aggregator learns nothing; ≥t can reconstruct, Feldman Verify', () => { const vss = new BabyFrostVSSDKG() const frost = new BabyFROST() From df811d06cdc385511428e58f9a4d085c515bbc18 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Wed, 22 Oct 2025 13:38:51 -0600 Subject: [PATCH 21/44] feat: refactor DKG and FROST imports by removing BabyFrostVSSDKG and vss-dkg, and updating index exports --- src/frost/index.ts | 4 +- src/frost/vss-dkg.ts | 384 ------------------------------------ src/index.ts | 9 +- src/manager/index.ts | 3 + test/vss-extensions.test.ts | 142 ------------- test/vss.test.ts | 156 --------------- 6 files changed, 8 insertions(+), 690 deletions(-) delete mode 100644 src/frost/vss-dkg.ts create mode 100644 src/manager/index.ts delete mode 100644 test/vss-extensions.test.ts delete mode 100644 test/vss.test.ts diff --git a/src/frost/index.ts b/src/frost/index.ts index 202bebe..d4053d3 100644 --- a/src/frost/index.ts +++ b/src/frost/index.ts @@ -1,4 +1,4 @@ import BabyFROST, { frost } from './babyfrost.js' -import BabyFrostVSSDKG, { vss } from './vss-dkg.js' +import TrustedDKG from './trusted-dkg.js' -export { BabyFROST, frost, BabyFrostVSSDKG, vss } +export { BabyFROST, frost, TrustedDKG } diff --git a/src/frost/vss-dkg.ts b/src/frost/vss-dkg.ts deleted file mode 100644 index 4714a5c..0000000 --- a/src/frost/vss-dkg.ts +++ /dev/null @@ -1,384 +0,0 @@ -/* eslint-disable jsdoc/require-returns */ -/* eslint-disable jsdoc/require-param */ -/* eslint-disable camelcase */ -/* eslint-disable jsdoc/require-jsdoc */ - -import { gcm } from '@noble/ciphers/aes.js' -import { randomBytes } from '@noble/hashes/utils.js' -import type { Point } from '@zk-kit/baby-jubjub' -import { addPoint } from '@zk-kit/baby-jubjub' - -import type { EncryptedShare, ParticipantInput, Share } from './types.js' -import { RailJubCurvePoint } from '../curve.js' -import RFC9591Hasher from '../hashing.js' - -function objToU8 (o: Record | Uint8Array): Uint8Array { - if (o instanceof Uint8Array) return o - const out = new Uint8Array(Object.keys(o).length) - for (const [k, v] of Object.entries(o)) out[Number(k)] = Number(v) - return out -} - -class BabyFrostVSSDKG extends RailJubCurvePoint { - public readonly contextString = 'FROST-EDBABYJUJUB-BLAKE512-v1' - hasher: RFC9591Hasher - - constructor () { - super() - this.hasher = new RFC9591Hasher(this.contextString, this.order) - } - - // ---------- math ---------- - /** Evaluate polynomial (coeffs in mod L) at x=id in mod L. */ - evalPolySubgroup (coeffsL: bigint[], id: bigint): bigint { - let y = 0n - let pow = 1n - for (const aj of coeffsL) { - y = this.modOrder(y + this.modOrder(aj) * pow) - pow = this.modOrder(pow * id) - } - return y - } - - // ---------- misc ---------- - private sortKey = (P: Point) => - `${P[0].toString(16).padStart(64, '0')}:${P[1].toString(16).padStart(64, '0')}` - - /** - * Deterministically derive a "view key" from dealers' constant-term commitments, for read-only uses. - * If you will use this as an AES key, it's fine as a 32-byte secret; for scalar uses modL first. - */ - deriveViewKeyFromPK (allDealerCommitments: Point[][]): Uint8Array { - const C0s: Point[] = [] - for (const dealerComms of allDealerCommitments) { - if (!dealerComms?.length) continue - C0s.push(dealerComms[0]!) - } - C0s.sort((a, b) => (this.sortKey(a) < this.sortKey(b) ? -1 : this.sortKey(a) > this.sortKey(b) ? 1 : 0)) - let acc = 0n - for (const C0 of C0s) { - // hash the affine coordinates + domain; interpret as scalar then accumulate in modL - const v = this.hasher.H7(this.SerializeElement(C0)) - const v_k = this.modOrder(v) - acc = this.modOrder(acc + v_k) - } - return this.toBytes(acc === 0n ? 1n : acc) // avoid zero key - } - - /** - * Deterministic dealer coefficients. If `initialState` doesn't include a0, derive a0 with j=0. - * Returns coeffs in mod L: [a0, a1, ..., a_{t-1}] - */ - makeDealerCoeffsDeterministic (p: ParticipantInput, threshold: number, initialState: bigint[] = []): bigint[] { - if (threshold <= 0) throw new Error('threshold must be > 0') - const a: bigint[] = initialState.slice() - if (a.length === 0) { - const input = Buffer.concat([ - this.toBytes(BigInt(p.id)), - this.toBytes(0n), - p.seed, - p.password - ]) - const c0 = this.hasher.H6(input) - const a0 = this.modOrder(c0) - if (a0 === 0n) throw new Error('a0 must be non-zero') - a.push(a0) - } - for (let j = a.length; j < threshold; j++) { - const input = Buffer.concat([ - this.toBytes(BigInt(p.id)), - this.toBytes(BigInt(j)), - p.seed, - p.password - ]) - const c = this.hasher.H6(input) - a.push(this.modOrder(c)) - } - if (a.length !== threshold) throw new Error('incorrect coefficient length') - return a - } - - /** Commitments Cj = Base8^{a_j} for j in [0..t-1]. */ - makeDealerCommitments (coeffsL: bigint[]): Point[] { - return coeffsL.map((aj) => { - const s = this.modOrder(aj) - // Base8 * s; stays in prime-order subgroup - return this.ScalarBaseMult(s) - }) - } - - /** Compute evaluations s_k(i) for recipients i. */ - computeDealerSharesForIds (coeffsL: bigint[], recipientIds: number[]): Record { - BabyFrostVSSDKG.assertSortedConsecutiveIds(recipientIds) - const out: Record = {} - for (const id of recipientIds) { - if (!Number.isInteger(id) || id <= 0) throw new Error(`bad recipient id: ${id}`) - out[id] = this.evalPolySubgroup(coeffsL, this.modOrder(BigInt(id))) - } - return out - } - - /** Check each dealer's C0 ∈ subgroup and aggregate the group public key A = ∑_k C_{k,0}. */ - combineGroupPubkeyFromCommitments (allDealerCommitments: Point[][]): Point { - if (!allDealerCommitments.length) throw new Error('no dealer commitments') - this.verifyCommitmentsShape(allDealerCommitments) - let acc: Point = this.Identity() - for (const dealerCom of allDealerCommitments) { - if (!dealerCom?.length) throw new Error('dealer missing commitments') - const P0 = dealerCom[0]! - // subgroup check: SUBORDER * P0 == 0 - if (!this.pointsEqual(this.ScalarMult(P0, this.order), this.Identity())) { - throw new Error('C0 not in subgroup') - } - acc = addPoint(acc, P0) - } - return acc - } - - private verifyCommitmentsShape (allDealerCommitments: Point[][]): number { - const lens = allDealerCommitments.map((c) => (c?.length ?? 0)) - if (lens.length === 0) throw new Error('no dealer commitments') - if (lens.some((l) => l <= 0)) throw new Error('dealer missing commitments') - const t = lens[0]! - for (const l of lens) { - if (l !== t) throw new Error('commitment degree mismatch across dealers') - } - return t - } - - verifyAllCommitmentsSubgroup (allDealerCommitments: Point[][]): boolean { - if (!allDealerCommitments.length) return false - for (const dealerCom of allDealerCommitments) { - if (!dealerCom?.length) return false - for (const Cj of dealerCom) { - if (!this.pointsEqual(this.ScalarMult(Cj, this.order), this.Identity())) return false - } - } - return true - } - - commitmentsDigest (allDealerCommitments: Point[][]): bigint { - const enc = this.encodeCommitmentsBytes(allDealerCommitments) - return this.hasher.H5(enc) - } - - private encodeCommitmentsBytes (allDealerCommitments: Point[][]): Uint8Array { - const dealers = allDealerCommitments.slice().filter(a => a?.length) - dealers.sort((a, b) => (this.sortKey(a[0]!) < this.sortKey(b[0]!) ? -1 : 1)) - let out = new Uint8Array() - for (const dealer of dealers) { - for (const Cj of dealer) { - const enc = this.SerializeElement(Cj) - out = Buffer.concat([out, enc]) - } - } - return out - } - - /** Verify Feldman share sk_i against commitments. */ - verifyFeldmanShare ( - id: number, - sk_i: bigint, - commitments: Array> - ): boolean { - try { - if (!Number.isInteger(id) || id <= 0) return false - if (!commitments?.length) return false - - // LHS = Base8 * s_i - const LHS = this.ScalarBaseMult(this.modOrder(sk_i)) - - // RHS = Σ_j C_j * (id^j) - let RHS: Point = this.Identity() - let pow = 1n - const idL = this.modOrder(BigInt(id)) - for (const Cj of commitments) { - // Cj must be in subgroup - if (!this.pointsEqual(this.ScalarMult(Cj, this.order), this.Identity())) return false - RHS = addPoint(RHS, this.ScalarMult(Cj, pow)) - pow = this.modOrder(pow * idL) - } - return this.pointsEqual(LHS, RHS) - } catch { - return false - } - } - - /** - * Finalize participant i: aggregate all dealer evaluations s_{k}(i), - * produce (skShareDiv8, skShare = 8*skShareDiv8 mod L), group PK, and a view key. - */ - finalizeVSSForParticipant ( - participantId: number, - s_ki_byDealer: Array<{ dealerId: number; s_ki: bigint }>, - allDealerCommitments: Point[][] - ): { share: Share; PKGroup: Point; viewingPrivateKey: Uint8Array } { - if (!Number.isInteger(participantId) || participantId <= 0) { - throw new Error('bad participantId') - } - if (!s_ki_byDealer.length || s_ki_byDealer.length !== allDealerCommitments.length) { - throw new Error('dealer/share count mismatch') - } - // Normalize and validate dealerIds: strictly increasing positive integers (order of shares does not affect sum) - const dealerIds = s_ki_byDealer.map((d) => d.dealerId) - const sortedDealerIds = dealerIds.slice().sort((a, b) => a - b) - BabyFrostVSSDKG.assertSortedConsecutiveIds(sortedDealerIds) - - // s_i = Σ_k s_{k}(i) (mod L) - let s_i = 0n - for (const { s_ki } of s_ki_byDealer) s_i = this.modOrder(s_i + this.modOrder(s_ki)) - - const skShareDiv8 = s_i - const skShare = this.modOrder(8n * skShareDiv8) - const share: Share = { id: participantId, skShare, skShareDiv8 } - - const PKGroup = this.combineGroupPubkeyFromCommitments(allDealerCommitments) - const viewingPrivateKey = this.deriveViewKeyFromPK(allDealerCommitments) - - return { share, PKGroup, viewingPrivateKey } - } - - /** - * Encrypt per-recipient shares using AES-GCM with strong context binding. - * keys: map of recipient id -> 32B AES key (e.g., from ECDH) - */ - encryptSharesAESGCM ( - shares: Record, - keyById: Record - ): Record { - const ids = Object.keys(shares).map(Number).sort((a, b) => a - b) - BabyFrostVSSDKG.assertSortedConsecutiveIds(ids) - const out: Record = {} - for (const [idStr, s] of Object.entries(shares)) { - const id = Number(idStr) - const key = keyById[id] - if (!key || key.length !== 32) throw new Error(`bad AES key for id ${id}`) - - const nonce = randomBytes(12) - const pt = this.toBytes(this.modOrder(s)) - - const aead = gcm(key, nonce) - const ciphertext = aead.encrypt(pt) - - out[id] = { nonce, ciphertext } - } - return out - } - - /** - * AES-GCM encryption with AAD binding: participant id || commitmentsDigest. - */ - encryptSharesAESGCMWithAAD ( - shares: Record, - keyById: Record, - allDealerCommitments: Point[][] - ): Record { - const ids = Object.keys(shares).map(Number).sort((a, b) => a - b) - BabyFrostVSSDKG.assertSortedConsecutiveIds(ids) - const out: Record = {} - const digest = this.commitmentsDigest(allDealerCommitments) - const digestBytes = this.toBytes(digest) - for (const [idStr, s] of Object.entries(shares)) { - const id = Number(idStr) - const key = keyById[id] - if (!key || key.length !== 32) throw new Error(`bad AES key for id ${id}`) - const nonce = randomBytes(12) - const pt = this.toBytes(this.modOrder(s)) - const aad = Buffer.concat([this.SerializeScalar(BigInt(id)), digestBytes]) - const aead = gcm(key, nonce, aad) - const ciphertext = aead.encrypt(pt) - out[id] = { nonce, ciphertext } - } - return out - } - - decryptShareAESGCM ( - enc: EncryptedShare, - keyBytes: Uint8Array - ): bigint { - if (keyBytes.length !== 32) throw new Error('bad AES key length') - - const nonce = objToU8(enc.nonce as any) - const ct = objToU8(enc.ciphertext as any) - if (nonce.length !== 12) throw new Error('bad nonce length (expected 12)') - if (ct.length < 16) throw new Error('bad ciphertext (must include 16B tag)') - const aead = gcm(keyBytes, nonce) - const pt = aead.decrypt(ct) // throws on auth failure - return this.fromBytes(pt) - } - - /** Decrypt with AAD binding (must match the context used in encryptSharesAESGCMWithAAD). */ - decryptShareAESGCMWithAAD ( - enc: EncryptedShare, - keyBytes: Uint8Array, - participantId: number, - allDealerCommitments: Point[][] - ): bigint { - if (keyBytes.length !== 32) throw new Error('bad AES key length') - if (!Number.isInteger(participantId) || participantId <= 0) throw new Error('bad participant id') - const nonce = objToU8(enc.nonce as any) - const ct = objToU8(enc.ciphertext as any) - if (nonce.length !== 12) throw new Error('bad nonce length (expected 12)') - if (ct.length < 16) throw new Error('bad ciphertext (must include 16B tag)') - const digest = this.commitmentsDigest(allDealerCommitments) - const aad = Buffer.concat([this.SerializeScalar(BigInt(participantId)), this.toBytes(digest)]) - const aead = gcm(keyBytes, nonce, aad) - const pt = aead.decrypt(ct) - return this.fromBytes(pt) - } - - assertCommitmentDegree (commitments: Point[], threshold: number) { - if (commitments.length !== threshold) throw new Error('commitment degree mismatch') - } - - static assertUniqueRecipientIds (ids: number[]) { - const s = new Set(ids) - if (ids.some((i) => !Number.isInteger(i) || i <= 0) || s.size !== ids.length) { - throw new Error('bad or duplicate recipient ids') - } - } - - static assertSortedConsecutiveIds (ids: number[]) { - if (!ids.length) throw new Error('empty recipient id list') - if (ids.some((id) => !Number.isInteger(id) || id <= 0)) { - throw new Error('recipient ids must be strictly increasing positive integers (self may be excluded)') - } - for (let i = 1; i < ids.length; i++) { - if (ids[i]! <= ids[i - 1]!) { - throw new Error('recipient ids must be strictly increasing positive integers (self may be excluded)') - } - } - } - - deriveInterpolatingValue (ids: bigint[], x_i: bigint): bigint { - const found = ids.find(a => { return a === x_i }) - if (!found) throw new Error('invalid parameters') - let num = 1n - let dom = 1n - for (const x_j of ids) { - if (x_j === x_i) continue - num = this.modOrder(num * this.modOrder(x_j)) - dom = this.modOrder(dom * this.modOrder(x_j - x_i)) - } - const invDom = this.invModOrder(dom) - return this.modOrder(num * invDom) - } - - reconstructConstantFromShares (subset: { id: number; s_i: bigint }[]): bigint { - if (!subset.length) throw new Error('no shares') - const ids = subset.map((s) => this.modOrder(BigInt(s.id))) - let a0 = 0n - for (const { id, s_i } of subset) { - const lambda = this.deriveInterpolatingValue(ids, this.modOrder(BigInt(id))) - a0 = this.modOrder(a0 + this.modOrder(s_i) * lambda) - } - return a0 - } - - -} - -const vss = new BabyFrostVSSDKG() - -export { vss } -export default BabyFrostVSSDKG diff --git a/src/index.ts b/src/index.ts index 272ef9b..a2cea81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import msgpack from 'msgpack-lite' import type { Signature } from './eddsa/babyposeidon.js' import { EddsaPoseidon, eddsaBuild } from './eddsa/index.js' import { BabyFROST, frost } from './frost/index.js' +import { DKGManager, FROSTSigningManager } from './manager' import { poseidonFn } from './poseidon/poseidon-lite-wrapper.js' function poseidon (inputs: Uint8Array[]) { @@ -56,15 +57,11 @@ export type * from './frost/types.js' export type { Point } -// FROST -export { - BabyFrostVSSDKG, - vss, -} from './frost/index.js' - export { frost, BabyFROST, + FROSTSigningManager, + DKGManager, eddsaBuild, EddsaPoseidon, getPublicSpendingKey, diff --git a/src/manager/index.ts b/src/manager/index.ts new file mode 100644 index 0000000..d4396a6 --- /dev/null +++ b/src/manager/index.ts @@ -0,0 +1,3 @@ +import DKGManager from './dkg.js' +import FROSTSigningManager from './signing.js' +export { FROSTSigningManager, DKGManager } diff --git a/test/vss-extensions.test.ts b/test/vss-extensions.test.ts deleted file mode 100644 index 27a9ddd..0000000 --- a/test/vss-extensions.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* eslint-disable jsdoc/require-jsdoc */ -import assert from 'node:assert' -import { describe, it } from 'node:test' - -import type { Point } from '@zk-kit/baby-jubjub' - -import BabyFrostVSSDKG from '../src/frost/vss-dkg' - -describe.skip('VSS-DKG RFC-like extensions', () => { - const vss = new BabyFrostVSSDKG() - - // simple deterministic participants - const participants = [ - { id: 1, seed: Buffer.alloc(32, 0x11), password: Buffer.alloc(32, 0xaa) }, - { id: 2, seed: Buffer.alloc(32, 0x22), password: Buffer.alloc(32, 0xbb) }, - { id: 3, seed: Buffer.alloc(32, 0x33), password: Buffer.alloc(32, 0xcc) }, - ] - - function setupDealers (threshold = 2) { - const coeffs = participants.map(p => vss.makeDealerCoeffsDeterministic(p as any, threshold)) - const commitments: Point[][] = coeffs.map(cs => vss.makeDealerCommitments(cs)) - const ids = participants.map(p => p.id) - const sharesByDealer: Record> = {} - coeffs.forEach((c, idx) => { - sharesByDealer[participants[idx]!.id] = vss.computeDealerSharesForIds(c, ids) - }) - return { coeffs, commitments, sharesByDealer } - } - - it('commitmentsDigest is deterministic for same commitments', () => { - const { commitments } = setupDealers(2) - const d1 = vss.commitmentsDigest(commitments) - const d2 = vss.commitmentsDigest(commitments) - assert.equal(d1, d2, 'digest should be deterministic') - }) - - it('verifyAllCommitmentsSubgroup passes for valid commitments', () => { - const { commitments } = setupDealers(2) - const ok = vss.verifyAllCommitmentsSubgroup(commitments) - assert.strictEqual(ok, true) - }) - - it('AES-GCM with AAD round-trip per dealer share (s_{k,i}), then aggregate per participant', () => { - const { commitments, sharesByDealer } = setupDealers(2) - // toy keys: deterministic 32B per participant id - const keyById: Record = {} - for (const p of participants) keyById[p.id] = Buffer.alloc(32, p.id) - - // encrypt per-dealer share maps and verify decrypt equals original per-dealer shares - const decryptedByDealer: Record> = {} - for (const dealerIdStr of Object.keys(sharesByDealer)) { - const dealerId = Number(dealerIdStr) - const sharesMap = sharesByDealer[dealerId]! - const encMap = vss.encryptSharesAESGCMWithAAD(sharesMap, keyById, commitments) - decryptedByDealer[dealerId] = {} - for (const p of participants) { - const dec = vss.decryptShareAESGCMWithAAD(encMap[p.id]!, keyById[p.id]!, p.id, commitments) - assert.equal(dec, sharesMap[p.id], `AAD decrypt mismatch for dealer ${dealerId}, id ${p.id}`) - decryptedByDealer[dealerId]![p.id] = dec - } - } - - // aggregate per participant after decrypt using library helper - for (const p of participants) { - const s_ki_byDealer_decrypted = Object.keys(decryptedByDealer) - .map((dealerIdStr) => ({ - dealerId: Number(dealerIdStr), - s_ki: decryptedByDealer[Number(dealerIdStr)!][p.id]! - })) - - const s_ki_byDealer_original = Object.keys(sharesByDealer) - .map((dealerIdStr) => ({ - dealerId: Number(dealerIdStr), - s_ki: sharesByDealer[Number(dealerIdStr)!][p.id]! - })) - - const resDecrypted = vss.finalizeVSSForParticipant(p.id, s_ki_byDealer_decrypted, commitments) - const resOriginal = vss.finalizeVSSForParticipant(p.id, s_ki_byDealer_original, commitments) - - assert.equal(resDecrypted.share.skShareDiv8, resOriginal.share.skShareDiv8, `Aggregated share mismatch for id ${p.id}`) - assert.equal(resDecrypted.share.skShare, resOriginal.share.skShare, `Aggregated share (x8) mismatch for id ${p.id}`) - // sanity: PK group must be identical regardless of share source - assert.equal( - vss.pointsEqual(resDecrypted.PKGroup, resOriginal.PKGroup), - true, - 'PKGroup mismatch' - ) - } - }) - - it('accepts non-consecutive but rejects non-increasing/invalid ids in computeDealerSharesForIds', () => { - const threshold = 2 - const coeffs = vss.makeDealerCoeffsDeterministic(participants[0] as any, threshold) - // non-consecutive is fine when self is excluded; should not throw - assert.doesNotThrow(() => vss.computeDealerSharesForIds(coeffs, [1, 3])) - // unsorted must throw - assert.throws(() => vss.computeDealerSharesForIds(coeffs, [2, 1]), /strictly increasing positive integers/i) - // non-positive must throw - assert.throws(() => vss.computeDealerSharesForIds(coeffs, [0, 2]), /strictly increasing positive integers/i) - }) - - it('AAD encryption allows non-consecutive ids; rejects invalid ids', () => { - const { commitments } = setupDealers(2) - const sharesOK: Record = { 2: 1n, 4: 2n } - const keyByIdOK: Record = { 2: Buffer.alloc(32, 2), 4: Buffer.alloc(32, 4) } - assert.doesNotThrow(() => vss.encryptSharesAESGCMWithAAD(sharesOK, keyByIdOK, commitments)) - const sharesBad: Record = { 0: 1n } - const keyByIdBad: Record = { 0: Buffer.alloc(32, 0) } - assert.throws(() => vss.encryptSharesAESGCMWithAAD(sharesBad, keyByIdBad, commitments), /strictly increasing positive integers/i) - }) - - it('Lagrange basis sums to 1 at 0, and reconstructs constant term', () => { - const a0 = vss.RandomScalar() - const a1 = vss.RandomScalar() - const ids = participants.map(p => BigInt(p.id)) - const sById: { id: number; s_i: bigint }[] = participants.map(p => ({ - id: p.id, - s_i: vss.modOrder(a0 + a1 * BigInt(p.id)) - })) - const lambdas = ids.map(id => vss.deriveInterpolatingValue(ids as any, id)) - const sum = lambdas.reduce((acc, l) => vss.modOrder(acc + l), 0n) - assert.equal(sum, 1n, 'sum of λ_i(0) should be 1') - const rec = vss.reconstructConstantFromShares(sById.slice(0, 2)) - assert.equal(rec, a0, 'reconstructed a0 should equal original') - }) - - it('Lagrange reconstruction works for non-consecutive ids', () => { - // use ids [1, 3] which previously surfaced integer-division bug - const a0 = vss.RandomScalar() - const a1 = vss.RandomScalar() - const ids = [1n, 3n] - const sById: { id: number; s_i: bigint }[] = ids.map(id => ({ - id: Number(id), - s_i: vss.modOrder(a0 + a1 * id) - })) - const lambdas = ids.map(id => vss.deriveInterpolatingValue(ids as any, id)) - const sum = lambdas.reduce((acc, l) => vss.modOrder(acc + l), 0n) - assert.equal(sum, 1n, 'sum of λ_i(0) should be 1') - const rec = vss.reconstructConstantFromShares(sById) - assert.equal(rec, a0, 'reconstructed a0 should equal original for non-consecutive ids') - }) -}) diff --git a/test/vss.test.ts b/test/vss.test.ts deleted file mode 100644 index 076320e..0000000 --- a/test/vss.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* eslint-disable jsdoc/require-jsdoc */ -/* eslint-disable camelcase */ -import assert from 'node:assert' -import { describe, it } from 'node:test' - -import type { Point } from '@zk-kit/baby-jubjub' - -import { eddsaBuild } from '../src/eddsa' -import type { Commitment } from '../src/frost/types' -import BabyFROST from '../src/frost/babyfrost' -import type { ParticipantInput, Share } from '../src/frost/types' -import BabyFrostVSSDKG from '../src/frost/vss-dkg' -import { - poseidonHex -} from '../src/index' - -function sharesById (shs: Share[]): Record { - const m: Record = {} - for (const s of shs) m[s.id] = s - return m -} - -describe.skip('VSS DKG: aggregator learns nothing; ≥t can reconstruct, Feldman Verify', () => { - const vss = new BabyFrostVSSDKG() - const frost = new BabyFROST() - - // Reconstruct master secret a0 (T(0)) from exactly `threshold` pairs (id, s_i) - function reconstructA0FromShares (subset: { id: number; s_i: bigint }[], threshold: number): bigint { - if (subset.length < threshold) throw new Error('insufficient shares to reconstruct') - return vss.reconstructConstantFromShares(subset) - } - - it('3 parties, t=2: verify shares, finalize, and demonstrate threshold property', () => { - const participants: ParticipantInput[] = [ - { id: 1, seed: Buffer.alloc(32, 0x11), password: Buffer.alloc(32, 0xaa) }, - { id: 2, seed: Buffer.alloc(32, 0x22), password: Buffer.alloc(32, 0xbb) }, - { id: 3, seed: Buffer.alloc(32, 0x33), password: Buffer.alloc(32, 0xcc) }, - ] - const ids = participants.map(p => p.id) - const threshold = 2 - - // each party is a dealer: make coeffs & commitments, then per-recipient shares - const dealerCoeffs = participants.map(p => vss.makeDealerCoeffsDeterministic(p, threshold)) - const dealerComms: Point[][] = dealerCoeffs.map(cs => vss.makeDealerCommitments(cs)) - const dealerShares: Record> = {} - dealerCoeffs.forEach((coeffs, kIdx) => { - dealerShares[participants[kIdx]!.id] = vss.computeDealerSharesForIds(coeffs, ids) - }) - - // aggregator can compute PKGroup from commitments, but not the scalar a0 - const PK_from_comm = vss.combineGroupPubkeyFromCommitments(dealerComms) - - // each participant verifies each dealer's share and finalizes their own share - const finalized: { id: number; share: Share; PKGroup: Point }[] = [] - for (const p of participants) { - // verify Feldman shares from every dealer - for (let dk = 0; dk < participants.length; dk++) { - const dealerId = participants[dk]!.id - const s_ki = dealerShares[dealerId]![p.id] // private share sent to p - const ok = vss.verifyFeldmanShare(p.id, s_ki!, dealerComms[dk]!) - assert.strictEqual(ok, true, `share from dealer ${dealerId} to ${p.id} failed Feldman check`) - } - - // collect (dealerId, s_ki) for finalize - const s_ki_byDealer = participants.map((dealer) => ({ - dealerId: dealer.id, - s_ki: dealerShares[dealer.id]![p.id], - })) - - const { share, PKGroup } = vss.finalizeVSSForParticipant(p.id, s_ki_byDealer as any, dealerComms) - finalized.push({ id: p.id, share, PKGroup }) - } - // all participants agree on PKGroup (same as from public commitments) - for (const f of finalized) { - assert.deepStrictEqual(f.PKGroup[0], PK_from_comm[0], 'PKGroup.x mismatch') - assert.deepStrictEqual(f.PKGroup[1], PK_from_comm[1], 'PKGroup.y mismatch') - } - - // aggregator cannot reconstruct with < t shares - const map = sharesById(finalized.map(f => f.share)) - assert.throws(() => reconstructA0FromShares([{ id: 1, s_i: map[1]!.skShareDiv8 }], threshold), - /insufficient shares/i, - 'should not reconstruct with fewer than threshold shares') - - // any t shares can reconstruct the master secret a0 (threshold property) - const a0_12 = reconstructA0FromShares( - [{ id: 1, s_i: map[1]!.skShareDiv8 }, { id: 2, s_i: map[2]!.skShareDiv8 }], - threshold - ) - const A_12 = vss.multiplyUnsafe(a0_12) - - assert.deepStrictEqual(A_12[0], PK_from_comm[0], 'Reconstructed PK.x (1,2) mismatch') - assert.deepStrictEqual(A_12[1], PK_from_comm[1], 'Reconstructed PK.y (1,2) mismatch') - - const a0_23 = reconstructA0FromShares( - [{ id: 2, s_i: map[2]!.skShareDiv8 }, { id: 3, s_i: map[3]!.skShareDiv8 }], - threshold - ) - const A_23 = vss.multiplyUnsafe(a0_23) - - assert.deepStrictEqual(A_23[0], PK_from_comm[0], 'Reconstructed PK.x (2,3) mismatch') - assert.deepStrictEqual(A_23[1], PK_from_comm[1], 'Reconstructed PK.y (2,3) mismatch') - - // test signing & verify - const subset = [ - finalized[0], - finalized[1], - finalized[2] - ] - const signIds = subset.map(s => s!.id) - - const msgHash = BigInt('0x' + poseidonHex(['0x' + 12345n.toString(16)], true)) - - const groupPublicKey = finalized[0].PKGroup as Point - - // round 1 commitment - const p1 = frost.commit(subset[0].share.skShare, BigInt(subset[0].id)) - const p2 = frost.commit(subset[1].share.skShare, BigInt(subset[1].id)) - const p3 = frost.commit(subset[2].share.skShare, BigInt(subset[2].id)) - - const commitmentList: Commitment[] = [p1, p2, p3].map((a)=>{ - return { ...a.commitments } - }) - - const s1 = frost.sign( - p1.commitments.identifier, - subset[0].share.skShare, - groupPublicKey, - p1.nonces, - msgHash, - commitmentList - ) - - const s2 = frost.sign( - p2.commitments.identifier, - subset[1].share.skShare, - groupPublicKey, - p2.nonces, - msgHash, - commitmentList - ) - - const s3 = frost.sign( - p3.commitments.identifier, - subset[2].share.skShare, - groupPublicKey, - p3.nonces, - msgHash, - commitmentList - ) - - const sig = frost.aggregate(commitmentList, msgHash, groupPublicKey, [s1, s2, s3]) - const ok = eddsaBuild.verifyPoseidon(frost.toBytes(msgHash).toReversed(), sig, groupPublicKey) - assert.strictEqual(ok, true, 'FROST aggregate failed verification') - }) -}) From db5585143a1e368286aee59ad19fbdbead743f06 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 28 Oct 2025 14:09:53 -0400 Subject: [PATCH 22/44] recap old --- package.json | 24 ++++++++++++------------ src/index.ts | 6 +++++- src/manager/signing.ts | 8 ++++---- test/dkg-manager.test.ts | 4 ++-- test/signing-manager.test.ts | 4 ++-- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 23b3260..0ddab1e 100644 --- a/package.json +++ b/package.json @@ -11,20 +11,20 @@ "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" }, - "./frost/rfc9591": { - "types": "./dist/types/frost/rfc9591.d.ts", - "import": "./dist/esm/frost/rfc9591.js", - "require": "./dist/cjs/frost/rfc9591.js" + "./frost/babyfrost": { + "types": "./dist/types/frost/babyfrost.d.ts", + "import": "./dist/esm/frost/babyfrost.js", + "require": "./dist/cjs/frost/babyfrost.js" }, - "./frost/vss-dkg": { - "types": "./dist/types/frost/vss-dkg.d.ts", - "import": "./dist/esm/frost/vss-dkg.js", - "require": "./dist/cjs/frost/vss-dkg.js" + "./frost/trusted-dkg": { + "types": "./dist/types/frost/trusted-dkg.d.ts", + "import": "./dist/esm/frost/trusted-dkg.js", + "require": "./dist/cjs/frost/trusted-dkg.js" }, - "./vss": { - "types": "./dist/types/frost/vss-dkg.d.ts", - "import": "./dist/esm/frost/vss-dkg.js", - "require": "./dist/cjs/frost/vss-dkg.js" + "./manager": { + "types": "./dist/types/manager/index.d.ts", + "import": "./dist/esm/manager/index.js", + "require": "./dist/cjs/manager/index.js" } }, "scripts": { diff --git a/src/index.ts b/src/index.ts index a2cea81..73f38a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import msgpack from 'msgpack-lite' import type { Signature } from './eddsa/babyposeidon.js' import { EddsaPoseidon, eddsaBuild } from './eddsa/index.js' import { BabyFROST, frost } from './frost/index.js' -import { DKGManager, FROSTSigningManager } from './manager' +import { DKGManager, FROSTSigningManager } from './manager/index.js' import { poseidonFn } from './poseidon/poseidon-lite-wrapper.js' function poseidon (inputs: Uint8Array[]) { @@ -54,6 +54,10 @@ function getShareableViewingKey (spendingPublicKey: Point, viewingPrivat } export type * from './frost/types.js' +// export { +// FROSTSigningManager, +// DKGManager +// } from './manager/index.js' export type { Point } diff --git a/src/manager/signing.ts b/src/manager/signing.ts index 236a957..4c41af9 100644 --- a/src/manager/signing.ts +++ b/src/manager/signing.ts @@ -3,7 +3,7 @@ import type { Point } from '@zk-kit/baby-jubjub' import { BabyFROST } from '../frost' import type { Bindings, Commitment } from '../frost/types' -type SignerShare = { id: number; skShare: bigint } +type SignerShare = { id: number; skShareDiv8: bigint } type SessionBinding = { bindings: Bindings, share: SignerShare } type PartialSignature = { identifier: number, partial: bigint } type SigningSession = { @@ -70,7 +70,7 @@ class FROSTSigningManager { this.localBindings = [] for (const signer of this.signers) { - const bindings = this.frost.commit(signer.skShare, BigInt(signer.id)) + const bindings = this.frost.commit(signer.skShareDiv8, BigInt(signer.id)) const sb = { bindings, share: signer @@ -107,7 +107,7 @@ class FROSTSigningManager { for (const signer of this.localBindings) { const partial = this.frost.sign( signer.bindings.commitments.identifier, - signer.share.skShare, + signer.share.skShareDiv8, this.groupPublicKey, signer.bindings.nonces, msgHash, @@ -149,7 +149,7 @@ class FROSTSigningManager { if (!commitmentLocal) throw new Error(`Missing commitment for local id ${id}`) const ok = this.frost.verifySignatureShare( BigInt(id), - share.skShare, + share.skShareDiv8, commitmentLocal, sigShare, commitmentList, diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index b21a442..4466aa5 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -113,7 +113,7 @@ describe('DKGManager e2e flow test', () => { const signers: FROSTSigningManager[] = [] for (const f of subset) { const sm = new FROSTSigningManager(groupPK, threshold) - sm.addSigner({ id: f.identifier, skShare: BigInt(f.skShare)}) + sm.addSigner({ id: f.identifier, skShareDiv8: BigInt(f.skShare)}) // sm.addSigner({ id: f.identifier, skShare: sm.frost.modOrder(BigInt(f.skShare) * 8n) }) signers.push(sm) } @@ -182,7 +182,7 @@ describe('DKGManager e2e flow test', () => { const signers: FROSTSigningManager[] = [] for (const f of subset) { const sm = new FROSTSigningManager(groupPublicKey, threshold) - sm.addSigner({ id: f.share.id, skShare: f.share.skShare }) + sm.addSigner({ id: f.share.id, skShareDiv8: f.share.skShare }) signers.push(sm) } diff --git a/test/signing-manager.test.ts b/test/signing-manager.test.ts index c5ebd6d..e606bb5 100644 --- a/test/signing-manager.test.ts +++ b/test/signing-manager.test.ts @@ -56,7 +56,7 @@ describe('BabyFrost Signing Manager', () => { const signers = [] for (const v of multiSigVector) { const signer = new FROSTSigningManager(v.PKGroup as [bigint, bigint], 3) - signer.addSigner({ id: v.id, skShare: v.share.skShare }) + signer.addSigner({ id: v.id, skShareDiv8: v.share.skShare }) signers.push(signer) } @@ -94,7 +94,7 @@ describe('BabyFrost Signing Manager', () => { const signers: FROSTSigningManager[] = [] for (const v of multiSigVector) { const signer = new FROSTSigningManager(v.PKGroup as [bigint, bigint], 3) - signer.addSigner({ id: v.id, skShare: v.share.skShare }) + signer.addSigner({ id: v.id, skShareDiv8: v.share.skShare }) signers.push(signer) } From 519aeace581c2f3bed25b6e4fd4788fd02e7e657 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 28 Oct 2025 14:37:06 -0400 Subject: [PATCH 23/44] refactor: standardize LE encoding --- src/curve.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/curve.ts b/src/curve.ts index 74fd86c..b519183 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -1,6 +1,6 @@ /* eslint-disable jsdoc/require-jsdoc */ import { blake512 } from '@noble/hashes/blake1.js' -import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js' +import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' import { Base8, Fr as FrValue, mulPointEscalar, order, r, subOrder } from '@zk-kit/baby-jubjub' import { packPublicKey, unpackPublicKey } from '@zk-kit/eddsa-poseidon' @@ -73,15 +73,14 @@ class RailJubCurvePoint { SerializeElement (A: Point): Uint8Array { if (this.pointsEqual(A, this.identity)) throw new Error('SerializeElement: input is group identity') const packed = packPublicKey(A) - const hex = packed.toString(16).padStart(64, '0') - const be = hexToBytes(hex) - return be + const le = leBigIntToBuffer(packed) + return le } DeserializeElement (buf: Uint8Array): Point { if (buf.length !== 32) throw new Error('DeserializeElement: invalid length') - const formatted = BigInt('0x' + bytesToHex(buf)) - const P = unpackPublicKey(formatted) + const packed = leBufferToBigInt(buf) + const P = unpackPublicKey(packed) if (P === null) throw new Error('DeserializeElement: invalid point encoding') if (this.pointsEqual(P, this.identity)) throw new Error('DeserializeElement: point is identity') const check = mulPointEscalar(P, this.order) From f0a27d1cd2c29ac517113fc2419007c6c6875a50 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Tue, 28 Oct 2025 14:54:07 -0400 Subject: [PATCH 24/44] refactor: add preventative checks --- src/curve.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/curve.ts b/src/curve.ts index b519183..a802613 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -14,6 +14,9 @@ type AffinePoint = { y: bigint; } +// no magic numbers +const SCALAR_BITS = 256n + class RailJubCurvePoint { public readonly curveOrder = order public readonly generator = Base8 @@ -115,11 +118,15 @@ class RailJubCurvePoint { return result } - toBytes (a: bigint) { + toBytes (a: bigint): Uint8Array { + if (a < 0n) throw new Error('toBytes: negative bigint') + // reject values needing >32 bytes (purely defensive) + if (a >> (SCALAR_BITS) !== 0n) throw new Error('toBytes: value too large for 32 bytes') return leBigIntToBuffer(a, 32) } fromBytes (a: Uint8Array) { + if (a.length !== 32) throw new Error('fromBytes: must be 32 bytes') return leBufferToBigInt(a) } From f20cdb3aa2f24e82e47d14817fca01449a6900a1 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Wed, 29 Oct 2025 09:05:08 -0400 Subject: [PATCH 25/44] refactor: update serialization methods to use packPoint and unpackPoint instead of incorrect un/packPublicKey --- src/curve.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/curve.ts b/src/curve.ts index a802613..e0e0120 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -2,8 +2,7 @@ import { blake512 } from '@noble/hashes/blake1.js' import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' -import { Base8, Fr as FrValue, mulPointEscalar, order, r, subOrder } from '@zk-kit/baby-jubjub' -import { packPublicKey, unpackPublicKey } from '@zk-kit/eddsa-poseidon' +import { Base8, Fr as FrValue, mulPointEscalar, order, packPoint, r, subOrder, unpackPoint } from '@zk-kit/baby-jubjub' import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' type FrType = typeof FrValue @@ -75,7 +74,7 @@ class RailJubCurvePoint { SerializeElement (A: Point): Uint8Array { if (this.pointsEqual(A, this.identity)) throw new Error('SerializeElement: input is group identity') - const packed = packPublicKey(A) + const packed = packPoint(A) const le = leBigIntToBuffer(packed) return le } @@ -83,7 +82,7 @@ class RailJubCurvePoint { DeserializeElement (buf: Uint8Array): Point { if (buf.length !== 32) throw new Error('DeserializeElement: invalid length') const packed = leBufferToBigInt(buf) - const P = unpackPublicKey(packed) + const P = unpackPoint(packed) if (P === null) throw new Error('DeserializeElement: invalid point encoding') if (this.pointsEqual(P, this.identity)) throw new Error('DeserializeElement: point is identity') const check = mulPointEscalar(P, this.order) From dc392294b1fd63ee6c7d70a2e7579015411f6e0b Mon Sep 17 00:00:00 2001 From: Reno Date: Wed, 29 Oct 2025 18:42:19 +0100 Subject: [PATCH 26/44] replace pointpacking from BIP62 compatibility to RFC8032 --- src/curve.ts | 198 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 182 insertions(+), 16 deletions(-) diff --git a/src/curve.ts b/src/curve.ts index e0e0120..7ed6e62 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -1,8 +1,8 @@ /* eslint-disable jsdoc/require-jsdoc */ import { blake512 } from '@noble/hashes/blake1.js' -import { randomBytes } from '@noble/hashes/utils.js' +import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' -import { Base8, Fr as FrValue, mulPointEscalar, order, packPoint, r, subOrder, unpackPoint } from '@zk-kit/baby-jubjub' +import { Base8, Fr as FrValue, mulPointEscalar, order, r, subOrder } from '@zk-kit/baby-jubjub' import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' type FrType = typeof FrValue @@ -13,9 +13,6 @@ type AffinePoint = { y: bigint; } -// no magic numbers -const SCALAR_BITS = 256n - class RailJubCurvePoint { public readonly curveOrder = order public readonly generator = Base8 @@ -25,6 +22,171 @@ class RailJubCurvePoint { public readonly fieldPrime = r blake512 = blake512 + // RFC 8032 compliant point compression + // Encodes point as y-coordinate with sign bit for x in bit 255 + private pointCompress(P: Point): bigint { + const x = P[0] + const y = P[1] + + // Set bit 255 to the sign (least significant bit) of x + const sign = x & 1n + const encoded = y | (sign << 255n) + + return encoded + } + + // RFC 8032 compliant point decompression + // Recovers point from y-coordinate and x sign bit + private pointDecompress(s: bigint): Point | null { + // Extract sign bit from bit 255 + const sign = (s >> 255n) & 1n + // Mask out the sign bit to get y + const y = s & ((1n << 255n) - 1n) + + // Recover x from y and sign + const x = this.recoverX(y, sign) + if (x === null) return null + + return [x, y] + } + + // Recover x-coordinate from y-coordinate and sign bit + // Baby Jubjub curve equation: ax² + y² = 1 + dx²y² + // where a = 168700, d = 168696 + private recoverX(y: bigint, sign: bigint): bigint | null { + const a = 168700n + const d = 168696n + const p = this.fieldPrime + + // Compute x² from curve equation + // x² = (y² - 1) / (dy² - a) + const y2 = this.modP(y * y) + const u = this.modP(y2 - 1n) + const v = this.modP(d * y2 - a) + + const vInv = this.modPInv(v) + if (vInv === null) return null + + const x2 = this.modP(u * vInv) + + // Compute square root + let x = this.modPSqrt(x2) + if (x === null) return null + + // Choose the square root with the correct sign + if ((x & 1n) !== sign) { + x = this.modP(-x) + } + + return x + } + + // Modular arithmetic in the field + private modP(x: bigint): bigint { + const r = x % this.fieldPrime + return r < 0n ? r + this.fieldPrime : r + } + + // Modular inverse in the field + private modPInv(a: bigint): bigint | null { + let t = 0n; let newT = 1n + let r = this.fieldPrime; let newR = this.modP(a) + + while (newR !== 0n) { + const q = r / newR + ;[t, newT] = [newT, t - q * newT] + ;[r, newR] = [newR, r - q * newR] + } + + if (r !== 1n) return null + if (t < 0n) t += this.fieldPrime + return t + } + + // Modular square root using Tonelli-Shanks or direct formula + // For Baby Jubjub, p ≡ 1 (mod 4), so we use Tonelli-Shanks + private modPSqrt(n: bigint): bigint | null { + const p = this.fieldPrime + + // Check if n is a quadratic residue + const ls = this.legendreSymbol(n, p) + if (ls !== 1n) return null + + // For p ≡ 5 (mod 8), we can use a direct formula + // Check if p ≡ 3 (mod 4) for simpler case + if (p % 4n === 3n) { + const exp = (p + 1n) / 4n + return this.modPPow(n, exp) + } + + // Otherwise use Tonelli-Shanks algorithm + return this.tonelliShanks(n, p) + } + + // Legendre symbol computation + private legendreSymbol(a: bigint, p: bigint): bigint { + const ls = this.modPPow(a, (p - 1n) / 2n) + return ls === p - 1n ? -1n : ls + } + + // Modular exponentiation + private modPPow(base: bigint, exp: bigint): bigint { + let result = 1n + base = this.modP(base) + + while (exp > 0n) { + if (exp % 2n === 1n) { + result = this.modP(result * base) + } + exp = exp / 2n + base = this.modP(base * base) + } + + return result + } + + // Tonelli-Shanks algorithm for computing square roots mod p + private tonelliShanks(n: bigint, p: bigint): bigint | null { + // Find Q and S such that p - 1 = Q * 2^S with Q odd + let Q = p - 1n + let S = 0n + while (Q % 2n === 0n) { + Q = Q / 2n + S += 1n + } + + // Find a quadratic non-residue z + let z = 2n + while (this.legendreSymbol(z, p) !== -1n) { + z += 1n + } + + let M = S + let c = this.modPPow(z, Q) + let t = this.modPPow(n, Q) + let R = this.modPPow(n, (Q + 1n) / 2n) + + while (true) { + if (t === 0n) return 0n + if (t === 1n) return R + + // Find the least i such that t^(2^i) = 1 + let i = 1n + let temp = this.modP(t * t) + while (temp !== 1n && i < M) { + temp = this.modP(temp * temp) + i += 1n + } + + const exp = M - i - 1n + const b = this.modPPow(c, 1n << exp) + M = i + c = this.modP(b * b) + t = this.modP(t * c) + R = this.modP(R * b) + } + } + // appendix D.(1, 2) rejectionSampling () { while (true) { @@ -74,19 +236,27 @@ class RailJubCurvePoint { SerializeElement (A: Point): Uint8Array { if (this.pointsEqual(A, this.identity)) throw new Error('SerializeElement: input is group identity') - const packed = packPoint(A) - const le = leBigIntToBuffer(packed) - return le + + // Use RFC 8032 compliant compression + const packed = this.pointCompress(A) + const bytes = leBigIntToBuffer(packed, 32) + + return bytes } DeserializeElement (buf: Uint8Array): Point { if (buf.length !== 32) throw new Error('DeserializeElement: invalid length') - const packed = leBufferToBigInt(buf) - const P = unpackPoint(packed) + + // Use RFC 8032 compliant decompression + const encoded = leBufferToBigInt(buf) + const P = this.pointDecompress(encoded) + if (P === null) throw new Error('DeserializeElement: invalid point encoding') if (this.pointsEqual(P, this.identity)) throw new Error('DeserializeElement: point is identity') + const check = mulPointEscalar(P, this.order) if (!this.pointsEqual(check, this.identity)) throw new Error('DeserializeElement: not in prime-order subgroup') + return P } @@ -117,15 +287,11 @@ class RailJubCurvePoint { return result } - toBytes (a: bigint): Uint8Array { - if (a < 0n) throw new Error('toBytes: negative bigint') - // reject values needing >32 bytes (purely defensive) - if (a >> (SCALAR_BITS) !== 0n) throw new Error('toBytes: value too large for 32 bytes') + toBytes (a: bigint) { return leBigIntToBuffer(a, 32) } fromBytes (a: Uint8Array) { - if (a.length !== 32) throw new Error('fromBytes: must be 32 bytes') return leBufferToBigInt(a) } @@ -168,4 +334,4 @@ class RailJubCurvePoint { } } export type { AffinePoint } -export { RailJubCurvePoint } +export { RailJubCurvePoint } \ No newline at end of file From 102dc60a8f313a00e0183644360eb165ffd63f0f Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Wed, 29 Oct 2025 14:13:30 -0400 Subject: [PATCH 27/44] lint --- src/curve.ts | 86 ++++++++++++++++++++++++---------------------------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/src/curve.ts b/src/curve.ts index 7ed6e62..0cc85ed 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -1,6 +1,6 @@ /* eslint-disable jsdoc/require-jsdoc */ import { blake512 } from '@noble/hashes/blake1.js' -import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js' +import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' import { Base8, Fr as FrValue, mulPointEscalar, order, r, subOrder } from '@zk-kit/baby-jubjub' import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' @@ -24,80 +24,80 @@ class RailJubCurvePoint { // RFC 8032 compliant point compression // Encodes point as y-coordinate with sign bit for x in bit 255 - private pointCompress(P: Point): bigint { + private pointCompress (P: Point): bigint { const x = P[0] const y = P[1] - + // Set bit 255 to the sign (least significant bit) of x const sign = x & 1n const encoded = y | (sign << 255n) - + return encoded } // RFC 8032 compliant point decompression // Recovers point from y-coordinate and x sign bit - private pointDecompress(s: bigint): Point | null { + private pointDecompress (s: bigint): Point | null { // Extract sign bit from bit 255 const sign = (s >> 255n) & 1n // Mask out the sign bit to get y const y = s & ((1n << 255n) - 1n) - + // Recover x from y and sign const x = this.recoverX(y, sign) if (x === null) return null - + return [x, y] } // Recover x-coordinate from y-coordinate and sign bit // Baby Jubjub curve equation: ax² + y² = 1 + dx²y² // where a = 168700, d = 168696 - private recoverX(y: bigint, sign: bigint): bigint | null { + private recoverX (y: bigint, sign: bigint): bigint | null { const a = 168700n const d = 168696n - const p = this.fieldPrime - + + // const p = this.fieldPrime // Compute x² from curve equation // x² = (y² - 1) / (dy² - a) const y2 = this.modP(y * y) const u = this.modP(y2 - 1n) const v = this.modP(d * y2 - a) - + const vInv = this.modPInv(v) if (vInv === null) return null - + const x2 = this.modP(u * vInv) - + // Compute square root let x = this.modPSqrt(x2) if (x === null) return null - + // Choose the square root with the correct sign if ((x & 1n) !== sign) { x = this.modP(-x) } - + return x } // Modular arithmetic in the field - private modP(x: bigint): bigint { + private modP (x: bigint): bigint { const r = x % this.fieldPrime return r < 0n ? r + this.fieldPrime : r } // Modular inverse in the field - private modPInv(a: bigint): bigint | null { + private modPInv (a: bigint): bigint | null { let t = 0n; let newT = 1n let r = this.fieldPrime; let newR = this.modP(a) - + while (newR !== 0n) { const q = r / newR ;[t, newT] = [newT, t - q * newT] ;[r, newR] = [newR, r - q * newR] } - + if (r !== 1n) return null if (t < 0n) t += this.fieldPrime return t @@ -105,35 +105,35 @@ class RailJubCurvePoint { // Modular square root using Tonelli-Shanks or direct formula // For Baby Jubjub, p ≡ 1 (mod 4), so we use Tonelli-Shanks - private modPSqrt(n: bigint): bigint | null { + private modPSqrt (n: bigint): bigint | null { const p = this.fieldPrime - + // Check if n is a quadratic residue const ls = this.legendreSymbol(n, p) if (ls !== 1n) return null - + // For p ≡ 5 (mod 8), we can use a direct formula // Check if p ≡ 3 (mod 4) for simpler case if (p % 4n === 3n) { const exp = (p + 1n) / 4n return this.modPPow(n, exp) } - + // Otherwise use Tonelli-Shanks algorithm return this.tonelliShanks(n, p) } // Legendre symbol computation - private legendreSymbol(a: bigint, p: bigint): bigint { + private legendreSymbol (a: bigint, p: bigint): bigint { const ls = this.modPPow(a, (p - 1n) / 2n) return ls === p - 1n ? -1n : ls } // Modular exponentiation - private modPPow(base: bigint, exp: bigint): bigint { + private modPPow (base: bigint, exp: bigint): bigint { let result = 1n base = this.modP(base) - + while (exp > 0n) { if (exp % 2n === 1n) { result = this.modP(result * base) @@ -141,12 +141,12 @@ class RailJubCurvePoint { exp = exp / 2n base = this.modP(base * base) } - + return result } // Tonelli-Shanks algorithm for computing square roots mod p - private tonelliShanks(n: bigint, p: bigint): bigint | null { + private tonelliShanks (n: bigint, p: bigint): bigint | null { // Find Q and S such that p - 1 = Q * 2^S with Q odd let Q = p - 1n let S = 0n @@ -154,22 +154,22 @@ class RailJubCurvePoint { Q = Q / 2n S += 1n } - + // Find a quadratic non-residue z let z = 2n while (this.legendreSymbol(z, p) !== -1n) { z += 1n } - + let M = S let c = this.modPPow(z, Q) let t = this.modPPow(n, Q) let R = this.modPPow(n, (Q + 1n) / 2n) - + while (true) { if (t === 0n) return 0n if (t === 1n) return R - + // Find the least i such that t^(2^i) = 1 let i = 1n let temp = this.modP(t * t) @@ -177,7 +177,7 @@ class RailJubCurvePoint { temp = this.modP(temp * temp) i += 1n } - + const exp = M - i - 1n const b = this.modPPow(c, 1n << exp) M = i @@ -236,27 +236,27 @@ class RailJubCurvePoint { SerializeElement (A: Point): Uint8Array { if (this.pointsEqual(A, this.identity)) throw new Error('SerializeElement: input is group identity') - + // Use RFC 8032 compliant compression const packed = this.pointCompress(A) const bytes = leBigIntToBuffer(packed, 32) - + return bytes } DeserializeElement (buf: Uint8Array): Point { if (buf.length !== 32) throw new Error('DeserializeElement: invalid length') - + // Use RFC 8032 compliant decompression const encoded = leBufferToBigInt(buf) const P = this.pointDecompress(encoded) - + if (P === null) throw new Error('DeserializeElement: invalid point encoding') if (this.pointsEqual(P, this.identity)) throw new Error('DeserializeElement: point is identity') - + const check = mulPointEscalar(P, this.order) if (!this.pointsEqual(check, this.identity)) throw new Error('DeserializeElement: not in prime-order subgroup') - + return P } @@ -281,12 +281,6 @@ class RailJubCurvePoint { return a[0] === b[0] && a[1] === b[1] } - // used in vss-dkg test for now. - multiplyUnsafe = (r: bigint) => { - const result = mulPointEscalar(this.generator, this.modCurveOrder(r)) - return result - } - toBytes (a: bigint) { return leBigIntToBuffer(a, 32) } @@ -334,4 +328,4 @@ class RailJubCurvePoint { } } export type { AffinePoint } -export { RailJubCurvePoint } \ No newline at end of file +export { RailJubCurvePoint } From 71f34161156df306db89af9eabf880cd18271253 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 31 Oct 2025 13:22:04 -0400 Subject: [PATCH 28/44] refactor: improve nonce handling in TrustedDKG and update exports in index --- src/frost/trusted-dkg.ts | 15 +++++++++++++-- src/hashing.ts | 1 + src/index.ts | 5 +++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/frost/trusted-dkg.ts b/src/frost/trusted-dkg.ts index b1d0ce1..3d03c5c 100644 --- a/src/frost/trusted-dkg.ts +++ b/src/frost/trusted-dkg.ts @@ -301,8 +301,19 @@ class TrustedDKG extends RailJubCurvePoint { ): bigint { if (keyBytes.length !== 32) throw new Error('bad AES key length') if (!Number.isInteger(participantId) || participantId <= 0) throw new Error('bad participant id') - const nonce = enc.nonce - const ct = enc.ciphertext + let nonce = new Uint8Array(Object.values(enc.nonce)) + const ct = new Uint8Array(Object.values(enc.ciphertext)) + if (nonce.length !== 12) { + const out = new Uint8Array(12) + if (nonce.length > 12) { + // trim from the end + out.set(nonce.subarray(0, 12)) + } else { + // pad with zeros at the end + out.set(nonce, 0) + } + nonce = out + } if (nonce.length !== 12) throw new Error('bad nonce length (expected 12)') if (ct.length < 16) throw new Error('bad ciphertext (must include 16B tag)') const digest = this.commitmentsDigest(allDealerCommitments) diff --git a/src/hashing.ts b/src/hashing.ts index b353399..e564d59 100644 --- a/src/hashing.ts +++ b/src/hashing.ts @@ -53,6 +53,7 @@ class RFC9591Hasher { // H2: challenge computation compatible with eddsaBuild.verifyPoseidon H2 (R8: Point, A: Point, msgHash: bigint): bigint { + // return poseidon5([...R8, ...A, msgHash]) return this.mod(poseidon5([...R8, ...A, msgHash])) } diff --git a/src/index.ts b/src/index.ts index 73f38a2..1683dc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,10 +60,15 @@ export type * from './frost/types.js' // } from './manager/index.js' export type { Point } +export { + bufferToBigInt, + bigIntToBuffer +} from '@zk-kit/utils' export { frost, BabyFROST, + FROSTSigningManager, DKGManager, eddsaBuild, From 3110c4b265e3b684a9aa5f9c4e156a98f0bf1982 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 31 Oct 2025 13:34:25 -0400 Subject: [PATCH 29/44] refactor: clean up exports and add default export for module --- src/index.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1683dc2..f45ed51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,6 @@ export { export { frost, BabyFROST, - FROSTSigningManager, DKGManager, eddsaBuild, @@ -81,3 +80,19 @@ export { poseidon, poseidonHex } + +export default { + frost, + BabyFROST, + FROSTSigningManager, + DKGManager, + eddsaBuild, + EddsaPoseidon, + getPublicSpendingKey, + getPublicViewingKey, + getShareableViewingKey, + signEDDSA, + verifyEDDSA, + poseidon, + poseidonHex, +}; \ No newline at end of file From bc2b47abbfac542e92163fe707a1a62007ff021b Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 31 Oct 2025 16:28:09 -0400 Subject: [PATCH 30/44] refactor: add bufferToBigInt and bigIntToBuffer exports in index --- src/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index f45ed51..ac090b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,13 +60,15 @@ export type * from './frost/types.js' // } from './manager/index.js' export type { Point } -export { - bufferToBigInt, - bigIntToBuffer -} from '@zk-kit/utils' +// export { +// bufferToBigInt, +// bigIntToBuffer +// } from '@zk-kit/utils' export { frost, + bufferToBigInt, + bigIntToBuffer, BabyFROST, FROSTSigningManager, DKGManager, @@ -82,6 +84,8 @@ export { } export default { + bufferToBigInt, + bigIntToBuffer, frost, BabyFROST, FROSTSigningManager, @@ -95,4 +99,4 @@ export default { verifyEDDSA, poseidon, poseidonHex, -}; \ No newline at end of file +} From 8f2d81b4c12792211d715cfbe721e3b324c07ba5 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 31 Oct 2025 20:09:36 -0400 Subject: [PATCH 31/44] refactor: update exports in package.json and improve buffer handling in babyposeidon --- package.json | 4 ++-- src/eddsa/babyposeidon.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0ddab1e..cc56603 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,12 @@ "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" }, - "./frost/babyfrost": { + "./babyfrost": { "types": "./dist/types/frost/babyfrost.d.ts", "import": "./dist/esm/frost/babyfrost.js", "require": "./dist/cjs/frost/babyfrost.js" }, - "./frost/trusted-dkg": { + "./trusted-dkg": { "types": "./dist/types/frost/trusted-dkg.d.ts", "import": "./dist/esm/frost/trusted-dkg.js", "require": "./dist/cjs/frost/trusted-dkg.js" diff --git a/src/eddsa/babyposeidon.ts b/src/eddsa/babyposeidon.ts index e4edd15..8432807 100644 --- a/src/eddsa/babyposeidon.ts +++ b/src/eddsa/babyposeidon.ts @@ -1,5 +1,4 @@ /* eslint-disable jsdoc/require-jsdoc */ -import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' import { derivePublicKey, @@ -8,6 +7,7 @@ import { unpackPublicKey, verifySignature, } from '@zk-kit/eddsa-poseidon' +import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' import { RailJubCurvePoint } from '../curve' @@ -33,11 +33,11 @@ class EddsaPoseidon extends RailJubCurvePoint { // misc helpers packPoint (point: Point): Uint8Array { const result = packPublicKey(point) - return hexToBytes(result.toString(16)) + return leBigIntToBuffer(result, 32) } unpackPoint (bytesIn: Uint8Array): Point | null { - const formatted = BigInt('0x' + bytesToHex(bytesIn)) + const formatted = leBufferToBigInt(bytesIn) const result = unpackPublicKey(formatted) return result } From 3591ad37b6fdb183057fc5ac047bffa383d83057 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Mon, 3 Nov 2025 11:15:21 -0500 Subject: [PATCH 32/44] refactor: update import paths to include file extensions and improve module resolution --- src/eddsa/babyposeidon.ts | 2 +- src/eddsa/index.ts | 4 ++-- src/frost/trusted-dkg.ts | 6 +++--- src/index.ts | 6 +++++- src/manager/dkg.ts | 4 ++-- src/manager/signing.ts | 4 ++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/eddsa/babyposeidon.ts b/src/eddsa/babyposeidon.ts index 8432807..386f83a 100644 --- a/src/eddsa/babyposeidon.ts +++ b/src/eddsa/babyposeidon.ts @@ -9,7 +9,7 @@ import { } from '@zk-kit/eddsa-poseidon' import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' -import { RailJubCurvePoint } from '../curve' +import { RailJubCurvePoint } from '../curve.js' type Signature = { R8: [bigint, bigint]; diff --git a/src/eddsa/index.ts b/src/eddsa/index.ts index 0f0213c..3200811 100644 --- a/src/eddsa/index.ts +++ b/src/eddsa/index.ts @@ -1,4 +1,4 @@ -import eddsaBuild from './babyposeidon' +import eddsaBuild from './babyposeidon.js' -export { EddsaPoseidon } from './babyposeidon' +export { EddsaPoseidon } from './babyposeidon.js' export { eddsaBuild } diff --git a/src/frost/trusted-dkg.ts b/src/frost/trusted-dkg.ts index 3d03c5c..d84782c 100644 --- a/src/frost/trusted-dkg.ts +++ b/src/frost/trusted-dkg.ts @@ -6,10 +6,10 @@ import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' import { addPoint, mulPointEscalar } from '@zk-kit/baby-jubjub' -import { RailJubCurvePoint } from '../curve' -import RFC9591Hasher from '../hashing' +import { RailJubCurvePoint } from '../curve.js' +import RFC9591Hasher from '../hashing.js' -import type { EncryptedShare } from './types' +import type { EncryptedShare } from './types.js' class TrustedDKG extends RailJubCurvePoint { public readonly contextString = 'FROST-EDBABYJUJUB-BLAKE512-v1' diff --git a/src/index.ts b/src/index.ts index ac090b4..7fde24b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { getPublicKey } from '@noble/ed25519' import type { Point } from '@zk-kit/baby-jubjub' -import { bigIntToBuffer, bufferToBigInt } from '@zk-kit/utils' +import { bigIntToBuffer, bufferToBigInt, leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' import msgpack from 'msgpack-lite' import type { Signature } from './eddsa/babyposeidon.js' @@ -69,6 +69,8 @@ export { frost, bufferToBigInt, bigIntToBuffer, + leBigIntToBuffer, + leBufferToBigInt, BabyFROST, FROSTSigningManager, DKGManager, @@ -86,6 +88,8 @@ export { export default { bufferToBigInt, bigIntToBuffer, + leBigIntToBuffer, + leBufferToBigInt, frost, BabyFROST, FROSTSigningManager, diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index cdd754f..ca66ae4 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -7,8 +7,8 @@ import { randomBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' import { bufferToBigInt } from '@zk-kit/utils' -import TrustedDKG from '../frost/trusted-dkg' -import type { EncryptedShare } from '../frost/types' +import TrustedDKG from '../frost/trusted-dkg.js' +import type { EncryptedShare } from '../frost/types.js' // Lightweight internal state to prevent wrong-order usage and surface clear errors // also output state updates for the upstream clients. diff --git a/src/manager/signing.ts b/src/manager/signing.ts index 4c41af9..18eb689 100644 --- a/src/manager/signing.ts +++ b/src/manager/signing.ts @@ -1,7 +1,7 @@ import type { Point } from '@zk-kit/baby-jubjub' -import { BabyFROST } from '../frost' -import type { Bindings, Commitment } from '../frost/types' +import { BabyFROST } from '../frost/index.js' +import type { Bindings, Commitment } from '../frost/types.js' type SignerShare = { id: number; skShareDiv8: bigint } type SessionBinding = { bindings: Bindings, share: SignerShare } From 81b6de6ec90de57c0f356f7051a769c244a86d09 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Thu, 6 Nov 2025 20:22:22 -0500 Subject: [PATCH 33/44] refactor: update version in package.json, enhance .gitignore, and add toMontgomery utility in RailJubCurvePoint --- .gitignore | 4 +++- package.json | 2 +- src/curve.ts | 15 ++++++++++++++- src/eddsa/babyposeidon.ts | 9 +++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 34e0f97..6607432 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ protected-main.json # misc dist .vscode -coverage \ No newline at end of file +coverage + +railgun-reloaded-*.tgz \ No newline at end of file diff --git a/package.json b/package.json index cc56603..814dabc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@railgun-reloaded/curves-lite", - "version": "0.0.1", + "version": "0.2.6", "description": "Dependency light version of railgun curves functionality.", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/curve.ts b/src/curve.ts index 0cc85ed..bceb979 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -90,7 +90,8 @@ class RailJubCurvePoint { // Modular inverse in the field private modPInv (a: bigint): bigint | null { let t = 0n; let newT = 1n - let r = this.fieldPrime; let newR = this.modP(a) + let r = this.fieldPrime + let newR = this.modP(a) while (newR !== 0n) { const q = r / newR @@ -326,6 +327,18 @@ class RailJubCurvePoint { const s = leBufferToBigInt(pr.subarray(0, 32)) return s >> 3n } + + // utility functions + toMontgomery (ed: { x: bigint; y: bigint }) { + const { x, y } = ed + if (x === 0n) throw new Error('toMontgomery: x = 0 maps to v undefined') + const denU = this.modP(1n - y) + if (denU === 0n) throw new Error('toMontgomery: y = 1 (identity) not mappable') + + const u = this.modP(this.modP(1n + y) * this.modPInv(denU)!) + const v = this.modP(u * this.modP(x)) + return { u, v } + } } export type { AffinePoint } export { RailJubCurvePoint } diff --git a/src/eddsa/babyposeidon.ts b/src/eddsa/babyposeidon.ts index 386f83a..f5443a8 100644 --- a/src/eddsa/babyposeidon.ts +++ b/src/eddsa/babyposeidon.ts @@ -11,7 +11,12 @@ import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' import { RailJubCurvePoint } from '../curve.js' -type Signature = { +type SignatureHex = { + R8: [string, string], + S: string +} + +type Signature = SignatureHex | { R8: [bigint, bigint]; S: bigint; } @@ -26,7 +31,7 @@ class EddsaPoseidon extends RailJubCurvePoint { return result } - verifyPoseidon (msg: Uint8Array, sig: Signature, A: Point) { + verifyPoseidon (msg: Uint8Array, sig: Signature, A: Point | Point) { return verifySignature(msg, sig, A) as any } From 87aa4359d5043ffbe6ad5653708b39c0126a0b0b Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Thu, 6 Nov 2025 20:59:44 -0500 Subject: [PATCH 34/44] refactor: update package version to 0.2.8, add pack script, and modify .gitignore --- .gitignore | 2 +- pack.sh | 25 +++++++++++++++++++++++++ package.json | 3 ++- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100755 pack.sh diff --git a/.gitignore b/.gitignore index 6607432..2bda944 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ dist .vscode coverage -railgun-reloaded-*.tgz \ No newline at end of file +local-distribution \ No newline at end of file diff --git a/pack.sh b/pack.sh new file mode 100755 index 0000000..a719566 --- /dev/null +++ b/pack.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# default to patch version bump +VERSION_TYPE="${1:-patch}" +OUT_DIR="${2:-./local-distribution}" + +# bump version in package.json +npm version $VERSION_TYPE --no-git-tag-version + +# pack the package +yarn pack + +# get package name and version +PACKAGE_NAME=$(node -p "require('./package.json').name.replace('@', '').replace('/', '-')") +PACKAGE_VERSION=$(node -p "require('./package.json').version") +OUT_NAME="${PACKAGE_NAME}-v${PACKAGE_VERSION}.tgz" + +# move the tarball to specified directory +if [ "$OUT_DIR" != "./" ]; then + mkdir -p "$OUT_DIR" + mv "$OUT_NAME" "$OUT_DIR/" + echo "Package moved to: $OUT_DIR/$OUT_NAME" +else + echo "Package created: $OUT_NAME" +fi \ No newline at end of file diff --git a/package.json b/package.json index 814dabc..40b7629 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@railgun-reloaded/curves-lite", - "version": "0.2.6", + "version": "0.2.8", "description": "Dependency light version of railgun curves functionality.", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -38,6 +38,7 @@ "pretest": "npm run build", "test": "node --import tsx --test --test-reporter=spec \"./test/**/*.test.ts\"", "coverage": "c8 --clean --reporter=text --reporter=lcov --include='src/**/*.ts' --exclude='src/**/*.test.ts' --exclude='**/dist/**' node --import tsx --test --test-reporter=spec \"./test/**/*.test.ts\"", + "pack:dev": "./pack.sh", "prepack": "npm run build" }, "repository": { From ac328b51352c44db0fa9821ac286fc66050caf6e Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 08:58:12 -0500 Subject: [PATCH 35/44] refactor: update package version to 0.2.9 and enhance DKGManager with participant name --- package.json | 2 +- src/manager/dkg.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 40b7629..204ed37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@railgun-reloaded/curves-lite", - "version": "0.2.8", + "version": "0.2.9", "description": "Dependency light version of railgun curves functionality.", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index ca66ae4..219d390 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -24,6 +24,7 @@ enum DKGFlowState { } class DKGManager { + name: string dkg: TrustedDKG participantID: number | undefined private secretComKey: Uint8Array @@ -78,11 +79,11 @@ class DKGManager { return ordered } - constructor () { + constructor (participantName: string, secretCommKey?: Uint8Array) { this.dkg = new TrustedDKG() // this.secretComKey = this.dkg.RandomScalar() - this.secretComKey = randomBytes(32) - + this.secretComKey = secretCommKey ?? randomBytes(32) + this.name = participantName // this will be announced with public commitments this.pubComKey = this.getPublicKey(this.secretComKey) this.roster = {} @@ -110,7 +111,7 @@ class DKGManager { } getAnnouncement () { - return { pubKey: this.pubComKey } + return { pubKey: this.pubComKey, name: this.name } } assignIdentifier (identifier: number) { From 40766c1815e1c197e9b0c78260516bf9148ebdac Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 09:13:47 -0500 Subject: [PATCH 36/44] refactor: replace leBigIntToBuffer and leBufferToBigInt with hexToBytes for packing and unpacking points in EddsaPoseidon --- src/eddsa/babyposeidon.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/eddsa/babyposeidon.ts b/src/eddsa/babyposeidon.ts index f5443a8..e2bea0a 100644 --- a/src/eddsa/babyposeidon.ts +++ b/src/eddsa/babyposeidon.ts @@ -1,16 +1,19 @@ /* eslint-disable jsdoc/require-jsdoc */ +import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js' import type { Point } from '@zk-kit/baby-jubjub' import { derivePublicKey, packPublicKey, + // packPublicKey, signMessage, unpackPublicKey, + // unpackPublicKey, verifySignature, } from '@zk-kit/eddsa-poseidon' -import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' import { RailJubCurvePoint } from '../curve.js' + type SignatureHex = { R8: [string, string], S: string @@ -38,11 +41,11 @@ class EddsaPoseidon extends RailJubCurvePoint { // misc helpers packPoint (point: Point): Uint8Array { const result = packPublicKey(point) - return leBigIntToBuffer(result, 32) + return hexToBytes(result.toString(16)) } unpackPoint (bytesIn: Uint8Array): Point | null { - const formatted = leBufferToBigInt(bytesIn) + const formatted = BigInt('0x' + bytesToHex(bytesIn)) const result = unpackPublicKey(formatted) return result } From 2ba912380ac18ab342ca0267bb66c448b233e060 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 10:58:05 -0500 Subject: [PATCH 37/44] refactor: remove unnecessary blank line in babyposeidon.ts --- src/eddsa/babyposeidon.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/eddsa/babyposeidon.ts b/src/eddsa/babyposeidon.ts index e2bea0a..5a51282 100644 --- a/src/eddsa/babyposeidon.ts +++ b/src/eddsa/babyposeidon.ts @@ -13,7 +13,6 @@ import { import { RailJubCurvePoint } from '../curve.js' - type SignatureHex = { R8: [string, string], S: string From 9e2f15df7f593c1caf1eaa6d5ea12366d913b4d5 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 11:02:00 -0500 Subject: [PATCH 38/44] refactor: remove commented-out packPublicKey and unpackPublicKey imports in babyposeidon.ts --- src/eddsa/babyposeidon.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/eddsa/babyposeidon.ts b/src/eddsa/babyposeidon.ts index 5a51282..e0ab3e2 100644 --- a/src/eddsa/babyposeidon.ts +++ b/src/eddsa/babyposeidon.ts @@ -4,10 +4,8 @@ import type { Point } from '@zk-kit/baby-jubjub' import { derivePublicKey, packPublicKey, - // packPublicKey, signMessage, unpackPublicKey, - // unpackPublicKey, verifySignature, } from '@zk-kit/eddsa-poseidon' From ab15b37190ce337f4b93e168462e51f52f09f5bd Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 11:04:13 -0500 Subject: [PATCH 39/44] refactor: remove unused import and update comment for secretComKey initialization in DKGManager --- src/manager/dkg.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/manager/dkg.ts b/src/manager/dkg.ts index 219d390..358e9fa 100644 --- a/src/manager/dkg.ts +++ b/src/manager/dkg.ts @@ -1,5 +1,4 @@ /* eslint-disable jsdoc/require-jsdoc */ -// import assert from 'node:assert' import { x25519 } from '@noble/curves/ed25519.js' @@ -81,7 +80,7 @@ class DKGManager { constructor (participantName: string, secretCommKey?: Uint8Array) { this.dkg = new TrustedDKG() - // this.secretComKey = this.dkg.RandomScalar() + // this.secretComKey = this.dkg.RandomScalar() -- example of another way for random bytes... this key does not need to be a scalar though. this.secretComKey = secretCommKey ?? randomBytes(32) this.name = participantName // this will be announced with public commitments From 73272861435aedcf859ceb179d666f15dc4e1220 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 11:10:36 -0500 Subject: [PATCH 40/44] refactor: update TODO comments for modularization in hashing and clarify signing session logic --- src/hashing.ts | 2 +- src/manager/signing.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hashing.ts b/src/hashing.ts index e564d59..db3d891 100644 --- a/src/hashing.ts +++ b/src/hashing.ts @@ -6,7 +6,7 @@ import { leBigIntToBuffer, leBufferToBigInt } from '@zk-kit/utils' import { poseidon5 } from 'poseidon-lite' type HashFn = (m: Uint8Array) => Uint8Array -// add hash functions? +// TODO: to modularize this hasher, we can add more hash functions here function blake2BWrapper (m: Uint8Array) { return blake2b(m, { dkLen: 64 }) } diff --git a/src/manager/signing.ts b/src/manager/signing.ts index 18eb689..80c0f6f 100644 --- a/src/manager/signing.ts +++ b/src/manager/signing.ts @@ -13,7 +13,8 @@ type SigningSession = { remoteSigners: Commitment[], partials: bigint[] } -/* eslint-disable jsdoc/require-jsdoc */ + +/* eslint-disable jsdoc/require-jsdoc */ class FROSTSigningManager { frost: BabyFROST @@ -120,6 +121,8 @@ class FROSTSigningManager { } // todo: should pass along the commitmentlist signed against, so out of sync // orchestration can still succeed with valid # of any threshold participants + // TODO: 11/6/25 @zy0n: re-evaluate this, ^ upon joining a signing-request we should assign an overall 'set' of participants + // and pass with these partials the participant list that these partials have been generated for. return partials } From 0d0fdf2f6a85aea8f56f631161cfaaef3b4e6277 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 11:10:41 -0500 Subject: [PATCH 41/44] refactor: remove commented-out exports in index.ts for cleaner code --- src/index.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7fde24b..6d280b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,16 +54,8 @@ function getShareableViewingKey (spendingPublicKey: Point, viewingPrivat } export type * from './frost/types.js' -// export { -// FROSTSigningManager, -// DKGManager -// } from './manager/index.js' export type { Point } -// export { -// bufferToBigInt, -// bigIntToBuffer -// } from '@zk-kit/utils' export { frost, From 668f3e74f2b2d7b12e99d13058bc3e83bd8f2e1a Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 11:12:32 -0500 Subject: [PATCH 42/44] refactor: add missing newline at end of file in pack.sh and update DKGManager instantiation in tests --- pack.sh | 2 +- test/dkg-manager.test.ts | 10 +++++----- test/signing-manager.test.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pack.sh b/pack.sh index a719566..ebcb017 100755 --- a/pack.sh +++ b/pack.sh @@ -22,4 +22,4 @@ if [ "$OUT_DIR" != "./" ]; then echo "Package moved to: $OUT_DIR/$OUT_NAME" else echo "Package created: $OUT_NAME" -fi \ No newline at end of file +fi diff --git a/test/dkg-manager.test.ts b/test/dkg-manager.test.ts index 4466aa5..1b8b5eb 100644 --- a/test/dkg-manager.test.ts +++ b/test/dkg-manager.test.ts @@ -9,7 +9,7 @@ import { bigIntToBuffer } from '@zk-kit/utils' describe('DKGManager e2e flow test', () => { it('should run trusted keygen and get expected group publickey', () => { - const dkgManager = new DKGManager() + const dkgManager = new DKGManager('test-participant-1') const secret = BigInt(0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn) const keys = dkgManager.runTrustedKeygen(secret, 5, 3) @@ -32,7 +32,7 @@ describe('DKGManager e2e flow test', () => { const announcements: Uint8Array[] = [] secrets.forEach((secret, idx) => { - const dealer = new DKGManager() + const dealer = new DKGManager('test-participant-1') dealers.push(dealer) const announce = dealer.getAnnouncement() announcements.push(announce.pubKey) @@ -95,7 +95,7 @@ describe('DKGManager e2e flow test', () => { const threshold = 3 const n = 5 - const dkgManager = new DKGManager() + const dkgManager = new DKGManager('test-participant-1') const secret = BigInt(0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn) const { groupPublicKey, shares, } = dkgManager.runTrustedKeygen(secret, n, threshold) @@ -145,7 +145,7 @@ describe('DKGManager e2e flow test', () => { // announcements and roster const announcements: Uint8Array[] = [] for (let i = 0; i < secrets.length; i++) { - const d = new DKGManager() + const d = new DKGManager('test-participant-1') dealers.push(d) announcements.push(d.getAnnouncement().pubKey) } @@ -203,4 +203,4 @@ describe('DKGManager e2e flow test', () => { const ok = eddsaBuild.verifyPoseidon(bigIntToBuffer(msg), sig, groupPublicKey) assert.strictEqual(ok, true, 'coordinator-less flow signing verification failed') }) -}) \ No newline at end of file +}) diff --git a/test/signing-manager.test.ts b/test/signing-manager.test.ts index e606bb5..49ddb9d 100644 --- a/test/signing-manager.test.ts +++ b/test/signing-manager.test.ts @@ -140,4 +140,4 @@ describe('BabyFrost Signing Manager', () => { assert.throws(() => signer.finalize(message)) } }) -}) \ No newline at end of file +}) From f5cbfafe3186dd0902b7e90494cc5e0bc0b11941 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 11:14:06 -0500 Subject: [PATCH 43/44] refactor: add missing newline at end of .gitignore for consistency --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2bda944..f3fe558 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ dist .vscode coverage -local-distribution \ No newline at end of file +local-distribution From b9b8ee2c16553a9958958e773638dd2dea193170 Mon Sep 17 00:00:00 2001 From: "zy0n.bear" Date: Fri, 7 Nov 2025 11:16:05 -0500 Subject: [PATCH 44/44] refactor: remove trailing whitespace in signing.ts for consistency --- src/manager/signing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manager/signing.ts b/src/manager/signing.ts index 80c0f6f..f25489a 100644 --- a/src/manager/signing.ts +++ b/src/manager/signing.ts @@ -14,7 +14,7 @@ type SigningSession = { partials: bigint[] } -/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable jsdoc/require-jsdoc */ class FROSTSigningManager { frost: BabyFROST