Skip to content

Commit 9a2d113

Browse files
authored
feat(frost): finalize interactive signing over the first t responsive committers (7.3 t-of-included PR2/3) (#4093)
## What RFC-21 Phase 7.3, the **runner subset** of t-of-included finalize. Today the interactive ROAST signing runner (`pkg/frost/signing/roast_runner_frost_native.go`) waits for **all** included members in round 1 (`collectCommitments`) and round 2 (`collectShares`), so one slow or offline member stalls the attempt to its ctx deadline → fail → ROAST retry. This makes an attempt finalize over the **first `t` responsive committers**. ### Roles (decided by the package's signed `signer_ids`, PR1's field #4092) - **Coordinator** — `collectCommitments` collects until **exactly `t`** (its own commitment seeded), proceeds the instant `t` have arrived, and builds the FROST package over that `t`-subset. It sets `signer_ids` (ascending, distinct) in `signSigningPackage` to the chosen members. If fewer than `t` ever commit, the ctx deadline fires and the run fails into the existing retry path. - **Non-coordinator signer** (in `signer_ids`) — runs round 2 and collects round-2 shares from the **package's signer set**, not the full included set. - **Observer** (committed in round 1 but **not** in `signer_ids`) — does **not** run round 2. It aggregates the subset's broadcast shares publicly, `MarkSucceeded`, and returns the signature — and **still aborts the engine session on success** to drop its unconsumed round-1 nonces. The success path previously suppressed the abort, which leaked an observer's nonces; fixed via a `signedRound2` flag. A round-2-silent member among the chosen `t` still fails the attempt → retry (existing path). Only the elected coordinator builds the package, so there is no honest divergence (receivers already filter `sender==elected` + attempt hash). The FROST `SigningPackageBytes` remains the cryptographic source of truth, so a coordinator that lies in `signer_ids` causes only a liveness failure (aggregate fails closed), never a wrong signature or false blame. ## Inert until oversizing (MacLane's policy knob) t-of-included is **dormant** until participant selection oversizes the included set past the threshold — it currently trims to exactly `honestThreshold` (`signing_loop_legacy_selector.go:91`), so the chosen subset equals the full included set and every member signs (today's behavior). The machinery is harmless and inert until then. ## Hardening The runner constructor now rejects `threshold > len(includedSet)` — the new round-1 loop's termination invariant (a well-formed attempt always selects ≥ threshold members; fail fast rather than silently degrade into timeout-driven retries). Found by the inline adversarial review. ## Tests (all `frost_native`, behind pre-prod tags) - Extended `buildInteractiveSigningHarness` with `runMembers` to drive **silence** (a non-responsive member). - `FinalizesOverResponsiveThresholdSubset` — a non-coordinator offline member is excluded; the two responsive members finalize over `t`; the coordinator's package omits the offline member. - `OversizedAllOnline_FinalizesOverThreshold` — all online; exactly `t` sign (round-2 count), `n−t` observe and abort their nonces, all reach Succeeded; the broadcast package carries exactly `t` ascending signer ids. - `ObserverAggregatesAndAbortsWithoutSigning` — focused observer: aggregates, Succeeded, **0** round-2 calls, **1** abort. - Existing `UsesEngineDerivedFrostIdentifiers` retargeted to a full-included (`n==t`) attempt for determinism. Validated: build+vet+test across the 5 tag combos (default / `frost_native` / `frost_roast_retry` / `frost_native frost_roast_retry` / `frost_native frost_tbtc_signer` w/ CGO), `-race` on `pkg/frost/signing`, the `frost_roast_retry` drive tests, repo-wide `frost_native` vet, `pkg/tbtc` build, and gofmt — all clean. ## Follow-up (PR3, lands with/right after this) `signingDoneCheck` (`pkg/tbtc/signing_done.go:93`, called from `signing_loop.go:458`) sets `expectedSignersCount = len(attemptMembersIndexes)`; once oversizing is enabled, a successful attempt that omitted an offline member would hang the outer done-check. PR3 makes it threshold/package-set aware. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 80cf249 + 2d915a7 commit 9a2d113

3 files changed

Lines changed: 597 additions & 89 deletions

File tree

pkg/frost/signing/roast_runner_engine_frost_native_test.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,16 @@ type fakeInteractiveSigningEngine struct {
3333
commitmentsByMember map[uint16][]byte
3434
shareByMember map[uint16][]byte
3535

36-
mu sync.Mutex
37-
openCalls int
38-
round1Calls int
39-
newPackageCalls int
40-
round2Calls int
41-
aggregateCalls int
42-
abortCalls int
43-
deriveCalls int
44-
lastAggregateShares []nativeFROSTSignatureShare
36+
mu sync.Mutex
37+
openCalls int
38+
round1Calls int
39+
newPackageCalls int
40+
round2Calls int
41+
aggregateCalls int
42+
abortCalls int
43+
deriveCalls int
44+
lastAggregateShares []nativeFROSTSignatureShare
45+
lastNewPackageCommitments []nativeFROSTCommitment
4546
}
4647

4748
func (f *fakeInteractiveSigningEngine) DeriveInteractiveAttemptContext(
@@ -93,6 +94,20 @@ func (f *fakeInteractiveSigningEngine) abortCallCount() int {
9394
return f.abortCalls
9495
}
9596

97+
func (f *fakeInteractiveSigningEngine) round2CallCount() int {
98+
f.mu.Lock()
99+
defer f.mu.Unlock()
100+
return f.round2Calls
101+
}
102+
103+
// newPackageCommitments returns a copy of the commitments the engine last built a
104+
// signing package over - the chosen t-subset for the coordinator.
105+
func (f *fakeInteractiveSigningEngine) newPackageCommitments() []nativeFROSTCommitment {
106+
f.mu.Lock()
107+
defer f.mu.Unlock()
108+
return append([]nativeFROSTCommitment(nil), f.lastNewPackageCommitments...)
109+
}
110+
96111
func newFakeInteractiveSigningEngine() *fakeInteractiveSigningEngine {
97112
return &fakeInteractiveSigningEngine{
98113
attemptID: "attempt-1",
@@ -153,6 +168,7 @@ func (f *fakeInteractiveSigningEngine) NewSigningPackage(
153168
) ([]byte, error) {
154169
f.mu.Lock()
155170
f.newPackageCalls++
171+
f.lastNewPackageCommitments = append([]nativeFROSTCommitment(nil), commitments...)
156172
f.mu.Unlock()
157173
return f.signingPackage, nil
158174
}

0 commit comments

Comments
 (0)