Add two-group signing with group-tagged binding factors - #8
Conversation
Generalizes nested signing to a second participant group that is itself a t-of-n FROST group with its own identifier space. Collections are keyed by (group, identifier) and the binding-factor preimage is domain-separated by a one-byte group tag (0x00/0x01), in both the per-signer preimage and each commitment-list entry, so identifiers that collide numerically across groups never alias. Group commitment, challenge, share computation/verification, and even-Y handling reuse the Ciphersuite hooks unchanged; existing entry points are untouched. Aggregation carries verifying shares for the primary group only: share-level blame is assigned for primary signers, while a failure that lies with the secondary group (whose verifying shares are not available) is reported as InvalidSignature without blame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryAdds an explicit two-group FROST signing scheme with group-tagged binding factors.
Confidence Score: 4/5The PR appears safe to merge, with a non-blocking test-coverage gap around deterministic exercise of both aggregate-nonce parities. The implementation consistently hashes and aggregates both tagged groups, while the only accepted concern is that one randomized test can pass without covering both parity branches it claims to exercise. Files Needing Attention: frost-secp256k1-tr/tests/two_group_tests.rs
|
| Filename | Overview |
|---|---|
| frost-core/src/two_group.rs | Implements tagged two-group packages, binding factors, per-group interpolation, signing, aggregation, and primary-share verification. |
| frost-secp256k1-tr/tests/two_group_tests.rs | Adds broad two-group coverage, but its randomized loop does not guarantee the claimed coverage of both aggregate-nonce parities. |
| frost-core/src/lib.rs | Exposes the new two_group module through the public frost-core API. |
Sequence Diagram
sequenceDiagram
participant P as Primary signers
participant S as Secondary signers
participant A as Aggregator
P->>A: Tagged commitments (0x00, id)
S->>A: Tagged commitments (0x01, id)
A-->>P: TwoGroupSigningPackage
A-->>S: TwoGroupSigningPackage
P->>P: Compute tagged binding factor and primary-group Lagrange coefficient
S->>S: Compute tagged binding factor and secondary-group Lagrange coefficient
P->>A: Primary signature shares
S->>A: Secondary signature shares
A->>A: Sum all shares and verify combined signature
alt Aggregate invalid
A->>A: Verify primary shares for blame
end
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
frost-secp256k1-tr/tests/two_group_tests.rs:121
**Parity coverage is nondeterministic**
The fixed random loop never records the aggregate commitment parity, so all eight iterations can exercise the same branch while the test still passes, leaving a parity-specific regression undetected.
Reviews (1): Last reviewed commit: "Add two-group signing with group-tagged ..." | Re-trigger Greptile
| for _ in 0..8 { | ||
| let groups = make_groups((5, 3), (3, 2), None, None)?; | ||
| let message = b"two-group message"; | ||
|
|
There was a problem hiding this comment.
Parity coverage is nondeterministic
The fixed random loop never records the aggregate commitment parity, so all eight iterations can exercise the same branch while the test still passes, leaving a parity-specific regression undetected.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frost-secp256k1-tr/tests/two_group_tests.rs
Line: 121
Comment:
**Parity coverage is nondeterministic**
The fixed random loop never records the aggregate commitment parity, so all eight iterations can exercise the same branch while the test still passes, leaving a parity-specific regression undetected.
How can I resolve this? If you propose a fix, please make it concise.The two sign+aggregate tests looped a fixed number of rounds and relied on random nonces to hit both parities of the aggregate nonce R, without checking that they had, so they could pass while claiming coverage they did not get. Loop until every parity case has actually occurred instead: a parity-specific regression now fails deterministically rather than at 1 - 2^(1-n), and the tests finish sooner because they stop as soon as they are covered. The tweaked test waits on a four-case grid, since the untweaked combined key's parity decides the primary key-package normalization and flips per round too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2dafcd5 to
038f116
Compare
…(#7843) ## Summary Implements MPC user-group signing and aggregation in the frost signer (SP-2884: "SO send: FROST aggregation for a multi-member user group"). `SIGNING_SCHEME_MPC_USER_GROUP` now dispatches to the two-group scheme implemented in the lightsparkdev/frost fork (lightsparkdev/frost#8): binding factors computed over both groups' round-1 commitments with a one-byte group tag (0x00 statechain / 0x01 user group), Lagrange coefficients within each signer's own group, and even-Y parity decided once by the single aggregate nonce over both groups. Deployed key conventions are preserved exactly: the taproot tweak (empty merkle root) rides with the statechain group as on the single-user path, and sub-users sign client-side with even-Y-normalized shares (by the untweaked combined key's parity) under the tweaked combined key. Scheme dispatch is an exhaustive match on the decoded discriminant rather than an equality test, so a value the signer does not recognize fails closed instead of being processed as legacy single-user traffic, and adding a scheme later is a compile error at the two dispatch sites. Sub-user verifying shares are deliberately not on the wire: a bad sub-user contribution is detected by the aggregate verification, which blames statechain shares individually but reports a sub-user-side failure without per-position blame. The frost dependency is pinned by rev to the fork branch; re-pin to the merge commit once lightsparkdev/frost#8 lands (do not merge this before that PR). ## Test Plan - `cargo test -p spark-frost` (66 tests). The headline test pins the SP-2884 acceptance criterion: the same user key signs once single-party and once split 2-of-3 across sub-users — with sub-user positions numerically colliding with operator identifiers — and both signatures verify against the same tweaked combined key. - Negative coverage: corrupted sub-user share fails aggregation without blame; mixed scheme/field forms rejected in both directions; USER role rejected for MPC jobs; unsorted positions rejected on both the signing and aggregation paths, which validate them separately; adaptor + MPC rejected on both paths; an unknown scheme discriminant rejected on both paths. - Fork-side crypto tests in lightsparkdev/frost#8 (`two_group_tests.rs`): tagged-vs-flat scheme divergence, divergent commitment views fail loudly, taproot-tweaked flow, both R parities. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Lightspark Eng <engineering@lightspark.com> GitOrigin-RevId: 77ad3a10eaa8ed09a66a5b5f9d366f33f4ce4a66
What
Adds
frost_core::two_group: FROST signing where the second participant group (Spark: the user side) is itself a t-of-n FROST group with its own identifier space, per the settled F2 design (SP-2884).(group, identifier); sub-user positions may numerically collide with operator identifiers and never alias.0x00primary /0x01secondary), present both in each signer's preimage and in every commitment-list entry hashed by H5:aggregateonly sums shares.Ciphersuitehooks unchanged — even-Y parity is decided by the single R over both groups' commitments.InvalidSignatureShare); a failure attributable to the secondary group (no verifying material on the wire by design) is reported asInvalidSignaturewithout blame.Tests
frost-secp256k1-tr/tests/two_group_tests.rs:InvalidSignaturewithout blametweak()at signing so the taptweak scalar enters the sum exactly once), repeated until all four combinations of the untweaked key's parity and R's parity have occurredNote:
serialization_tests::check_signing_package_postcard_serializationfails onmainbefore this PR (stale upstream snapshot vs the fork's addedSigningPackagefields); not addressed here.