diff --git a/src/frost/babyfrost.ts b/src/frost/babyfrost.ts index bc90a62..75de41b 100644 --- a/src/frost/babyfrost.ts +++ b/src/frost/babyfrost.ts @@ -96,10 +96,14 @@ class BabyFROST extends RailJubCurvePoint { let dom = 1n for (const x_j of L) { if (x_j === x_i) continue - num *= x_j - dom *= x_j - x_i + num = this.modOrder(num * x_j) + dom = this.modOrder(dom * (x_j - x_i)) } - const value = num / dom + // Field division: BigInt `num / dom` truncates toward zero and yields + // wrong coefficients whenever dom does not divide num exactly (e.g. the + // quorum {1, 3} gives lambda_1 = 1 and lambda_3 = 0, so any quorum other + // than the full consecutive set produces invalid signatures). + const value = this.modOrder(num * this.invModOrder(dom)) return value } diff --git a/test/babyfrost.test.ts b/test/babyfrost.test.ts index 10c169e..34ae3e0 100644 --- a/test/babyfrost.test.ts +++ b/test/babyfrost.test.ts @@ -4,6 +4,7 @@ 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 { TrustedDKG } from '../src/frost' import { subOrder } from '@zk-kit/baby-jubjub' // can skip the mod purely for tests, it passes without. @@ -137,3 +138,61 @@ describe("BabyFrost RFC9591 spec implementation", () => { assert(ok, 'validation failed') }) }) + +describe('BabyFrost Lagrange coefficients (regression)', () => { + // The Lagrange coefficients at zero are integers for any consecutive + // identifier set {1, ..., t}, so signing with such quorums cannot detect a + // deriveInterpolatingValue that uses BigInt integer division instead of a + // modular inverse. A non-consecutive quorum such as {1, 3} did produce + // invalid signatures before the fix (lambda_1 = 1, lambda_3 = 0). + const dkg = new TrustedDKG() + const threshold = 2 + const n = 3 + const secret = 0x43583e33fb2f47faa243b5cdf8cb251f7e9482f0386064901ae0c5e2134b78fn + const { participantPrivateKeys: shares, vssCommitment } = dkg.trustedDealerKeygen(secret, n, threshold) + const group = dkg.deriveGroupInfo(n, threshold, vssCommitment) + const finalized = shares.map(({ x_i, y_i }) => { + const res = dkg.finalizeParticipant(x_i, [{ dealerId: 1, s_ki: y_i }], [vssCommitment]) + return { id: x_i, skShare: res.share.skShare } + }) + + for (const ids of [[1, 2], [1, 3], [2, 3]]) { + it(`should properly compute signature with quorum {${ids.join(', ')}}`, () => { + const frost = new BabyFROST_RFC9591() + const subset = ids.map(id => finalized[id - 1]!) + const groupPublicKey = group.PK! + const msgHash = BigInt('0x' + poseidonHex(['0x' + 12345n.toString(16)], true)) + + // round 1 commitments + const rounds = subset.map(s => frost.commit(s.skShare, BigInt(s.id))) + const commitmentList: Commitment[] = rounds.map((a) => ({ ...a.commitments })) + + // round 2 signature shares + const sigShares = rounds.map((r, idx) => frost.sign( + r.commitments.identifier, + subset[idx]!.skShare, + groupPublicKey, + r.nonces, + msgHash, + commitmentList + )) + + rounds.forEach((r, idx) => { + const verified = frost.verifySignatureShare( + r.commitments.identifier, + subset[idx]!.skShare, + r.commitments, + sigShares[idx]!, + commitmentList, + groupPublicKey, + msgHash + ) + assert(verified, `participant ${r.commitments.identifier} provided a share that failed verification`) + }) + + const sig = frost.aggregate(commitmentList, msgHash, groupPublicKey, sigShares) + const ok = eddsaBuild.verifyPoseidon(frost.toBytes(msgHash).toReversed(), sig, groupPublicKey) + assert(ok, `validation failed for quorum {${ids.join(', ')}}`) + }) + } +})