Skip to content

Add two-group signing with group-tagged binding factors - #8

Merged
keenan-lightspark merged 2 commits into
mainfrom
sp2884-two-group-signing
Aug 6, 2026
Merged

Add two-group signing with group-tagged binding factors#8
keenan-lightspark merged 2 commits into
mainfrom
sp2884-two-group-signing

Conversation

@keenan-lightspark

@keenan-lightspark keenan-lightspark commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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).

  • Collections are keyed by (group, identifier); sub-user positions may numerically collide with operator identifiers and never alias.
  • Binding-factor preimage is domain-separated by a one-byte group tag (0x00 primary / 0x01 secondary), present both in each signer's preimage and in every commitment-list entry hashed by H5:
    rho_i = H1(tag(group(i)) || vk || H4(msg) || H5(commitment_list) || id(i))
    commitment_list = concat over signers sorted by (tag, id) of tag || id || hiding || binding
    
  • Lagrange coefficients stay within the signer's own group (same rule as the deployed participant-groups scheme); aggregate only sums shares.
  • Group commitment R, challenge, share computation/verification, and BIP-340 even-Y handling reuse the Ciphersuite hooks unchanged — even-Y parity is decided by the single R over both groups' commitments.
  • Aggregation takes verifying shares for the primary group only: primary shares get share-level blame (InvalidSignatureShare); a failure attributable to the secondary group (no verifying material on the wire by design) is reported as InvalidSignature without blame.
  • No changes to existing entry points — deployed single-party byte behavior is untouched; callers select this scheme explicitly by calling the new module.

Tests

frost-secp256k1-tr/tests/two_group_tests.rs:

  • (3-of-5)×(2-of-3) sign+aggregate verifies, repeated until both parities of the aggregate nonce R have actually occurred; default identifier lists make cross-group identifier collisions the default case
  • colliding identifiers get distinct tagged binding factors (tag is the only preimage difference)
  • tampered primary share → blamed by identifier; tampered secondary share → InvalidSignature without blame
  • a secondary signer binding a divergent primary commitment set fails loudly at the aggregate check
  • taproot-tweaked flow under the deployed key convention (primary pre-normalized to the untweaked combined key's parity carrying the tweaked VK; secondary applies tweak() 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 occurred
  • the tagged scheme produces a different (still valid) signature than the flat participant-groups scheme over identical commitments and nonces

Note: serialization_tests::check_signing_package_postcard_serialization fails on main before this PR (stale upstream snapshot vs the fork's added SigningPackage fields); not addressed here.

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-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown

Greptile Summary

Adds an explicit two-group FROST signing scheme with group-tagged binding factors.

  • Introduces independent primary and secondary identifier spaces and canonical tagged commitment encoding.
  • Adds two-group share generation, aggregation, primary-share blame, and secp256k1-tr parity/tweak handling.
  • Adds integration tests for identifier collisions, tampering, divergent commitment views, Taproot tweaking, and compatibility boundaries.

Confidence Score: 4/5

The 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

Important Files Changed

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
Loading
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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor Author

@faraday

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>
@keenan-lightspark
keenan-lightspark force-pushed the sp2884-two-group-signing branch from 2dafcd5 to 038f116 Compare August 6, 2026 17:03
@keenan-lightspark
keenan-lightspark merged commit 997427e into main Aug 6, 2026
1 of 18 checks passed
@keenan-lightspark
keenan-lightspark deleted the sp2884-two-group-signing branch August 6, 2026 20:32
lightspark-bot pushed a commit to buildonspark/spark that referenced this pull request Aug 7, 2026
…(#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants