Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/frost/babyfrost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
59 changes: 59 additions & 0 deletions test/babyfrost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(', ')}}`)
})
}
})