Skip to content

feat(tbtc/signer): Phase 7.2a InteractiveAggregate (tweaked, self-verifying)#4052

Merged
mswilkison merged 4 commits into
extraction/frost-signer-mirror-2026-05-26from
feat/phase-7-2a-interactive-aggregate-2026-06-12
Jun 13, 2026
Merged

feat(tbtc/signer): Phase 7.2a InteractiveAggregate (tweaked, self-verifying)#4052
mswilkison merged 4 commits into
extraction/frost-signer-mirror-2026-05-26from
feat/phase-7-2a-interactive-aggregate-2026-06-12

Conversation

@mswilkison

@mswilkison mswilkison commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

What

Phase 7.2a of the frozen spec: coordinator-side InteractiveAggregate, the step that combines the responsive subset's signature shares into the final taproot-tweaked BIP-340 signature. Builds on the merged 7.1 session layer (#4051).

Design

  • Verifying material from session DKG state, never the request — same no-secret-on-the-FFI discipline as 7.1 (the verifying shares are public, but resolving them from engine state prevents a caller substituting them). Session must exist with completed DKG.
  • No policy gate here, deliberately. Aggregation is public computation over shares already gated at each signer's Round2 (lifecycle/quarantine-of-subset/firewall). The lock is dropped before the crypto — no secret is released, so a kill switch racing the aggregation is irrelevant. Documented inline.
  • Mandatory self-verification of the aggregate against the taproot-tweaked group verifying key before release, matching coarse finalize. Message binding is enforced cryptographically: a share produced over one message won't verify against a package over another.
  • FFI export frost_tbtc_interactive_aggregate + C header; call/success counters + latency telemetry consistent with the other interactive ops.

Attributable blame is deferred to 7.2b (review outcome)

The first cut emitted a per-member InvalidSignatureShare { culprits } on a bad share. Review (Codex P1) correctly flagged that as forgeable: the engine can't yet bind the aggregate's public inputs (signing package, taproot root) to what each member actually signed at Round2, so a coordinator aggregating against a different package/root would make honest shares fail and frame their members. Binding "to what the members signed" needs the per-member signed-package envelopes (spec §6) — which are 7.2b.

So this PR fails closed with a generic error on a verification failure (no signature, no culprit naming). Attributable blame returns in 7.2b together with the envelope binding that makes the attribution sound, and with the FFI structured-culprit payload (Codex P2 — the current FFI error carries only code/message/recovery_class).

Verification

  • Engine tests: interactive + stateless member shares aggregate to a verified BIP-340 signature; an invalid co-signer share fails closed (generic error, no signature); FFI dispatch smoke.
  • Full suite 268 passed / 1 ignored, clippy -D warnings clean, C header parses, Phase 5 chaos green.

🤖 Generated with Claude Code

…blame

Coordinator-side InteractiveAggregate per the frozen spec: collect the
responsive subset's signature shares, verify each against its verifying
share, and produce the BIP-340 signature - then self-verify it against
the (taproot-tweaked) group key before release, matching the coarse
finalize path.

Verifying material is resolved from the session's own DKG public key
package, never the request, consistent with the no-secret-on-the-FFI
discipline (the session must exist with completed DKG). Aggregation
operates on public material only, so no policy gate runs here: the
secret-bearing step is each signer's Round2, where lifecycle /
quarantine (full subset) / firewall were already enforced.

frost::aggregate already verifies every share and reports the culprit
identifiers on failure; instead of flattening that to a string, a bad
share now surfaces as the structured EngineError::InvalidSignatureShare
{ culprits } (code invalid_signature_share, recoverable) naming the
offending member(s), so the coordinator has attributable blame and can
exclude them on the next attempt.

FFI export frost_tbtc_interactive_aggregate + C header declaration;
call/success counters and latency telemetry consistent with the other
interactive ops. Tests: interactive member + stateless member shares
aggregate to a verified BIP-340 signature through the engine; a corrupt
co-signer share fails with attributable blame naming the culprit; FFI
dispatch smoke. Full suite 268 passed / 1 ignored, clippy -D warnings
clean, header parses, chaos green.

Deferred to 7.2b (needs persistence plumbing, not security-load-bearing
since aggregate is deterministic over public data): the "mark session
complete" marker, plus the signed-body package envelopes and
cross-language vectors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f282cc7f-b910-4566-9571-c9e64649c6f5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase-7-2a-interactive-aggregate-2026-06-12

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

… bound

Review finding (Codex P1): emitting per-member InvalidSignatureShare
blame from InteractiveAggregate is forgeable. The engine cannot yet
bind the aggregate's public inputs (signing package, taproot root) to
what each member actually signed at Round2, so a buggy or malicious
coordinator aggregating against a different package/root makes honest
shares fail verification and frames their members. Binding "to what
the members signed" requires the per-member signed-package envelopes
(frozen spec section 6), which are Phase 7.2b.

So this drops the premature attributable blame: a share-verification
failure is now a generic fail-closed Validation error (no signature,
no culprit naming). The InvalidSignatureShare error variant and the
culprit-mapping helper are removed and will be reintroduced in 7.2b
together with the envelope binding that makes the attribution sound -
and with the FFI structured-culprit payload (Codex P2), since the
current ffi error_result carries only code/message/recovery_class and
would drop a culprit vector anyway.

The aggregate still verifies every share (frost) and self-verifies the
result against the tweaked group key; only the blame OUTPUT is
deferred. Test renamed to assert the fail-closed generic error. Full
suite 268 passed / 1 ignored, clippy -D warnings clean, chaos green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mswilkison mswilkison changed the title feat(tbtc/signer): Phase 7.2a InteractiveAggregate with attributable blame feat(tbtc/signer): Phase 7.2a InteractiveAggregate (tweaked, self-verifying) Jun 13, 2026
mswilkison and others added 2 commits June 13, 2026 09:12
…behavior

Review finding (Codex P2): the previous commit removed the structured
invalid_signature_share error but left the public C header and the
InteractiveAggregateRequest doc still advertising attributable blame /
the invalid_signature_share code. A coordinator built against that
contract would wait for an error code that can no longer be returned.

Both now state the actual 7.2a behavior: an invalid share (or a
mismatched package/root) fails closed with the generic validation_error
code and no signature, and per-member attributable blame is deferred to
Phase 7.2b where the signed-package envelopes make the attribution
unforgeable. The frozen spec's design statement is unchanged - it
describes the completed feature, and the deferral is tracked.

Doc-only: header parses, crate builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gate

Review finding (Codex P2): aggregate took the engine lock but, unlike
open/round1/round2/abort, never called sweep_expired_interactive_state.
A process receiving only InteractiveAggregate calls would let expired
Round1 nonce handles linger in memory past the TTL until some other
interactive endpoint happened to run, breaking the documented TTL
guarantee for secret nonce handles. Aggregate now sweeps right after
acquiring the lock, like every other interactive entry point.

Test: an aged Round1 handle in one session is cleared by an aggregate
call targeting a different (missing) session - proving the sweep runs
regardless of which interactive endpoint takes the lock. Full suite
269 passed / 1 ignored, clippy -D warnings clean, chaos green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mswilkison mswilkison merged commit f96a54c into extraction/frost-signer-mirror-2026-05-26 Jun 13, 2026
19 checks passed
@mswilkison mswilkison deleted the feat/phase-7-2a-interactive-aggregate-2026-06-12 branch June 13, 2026 13:41
mswilkison added a commit that referenced this pull request Jun 14, 2026
… blame (#4054)

## What

A design note scoping Phase 7.2b **before** the implementation, matching
the spec-first discipline that preceded 7.1 (and that 7.2a's
blame-deferral made the case for: don't ship the feature ahead of its
foundation). Now includes a companion **open-questions discussion doc**
with the three design questions **resolved** post-review.

## Why a note first

7.2a (#4052) ships InteractiveAggregate but fails closed *without*
attributable blame, because the engine can't yet bind the aggregate's
inputs to what each member signed — so a coordinator aggregating against
a different package/root could frame honest members (the #4052 P1). The
frozen spec ties trustworthy blame to **signed package envelopes** (§6).
7.2b builds the foundation and the feature together. It spans Go
(wire/distribution/equivocation/**blame adjudication**) and Rust (engine
**candidate culprits**/FFI), so it needs design before code.

## Covers

- **Signed-body signing-package envelope** mirroring the #4040 pattern
(`SigningPackageBody` / `SignedSigningPackage`): signed once, embedded
verbatim, re-verified against exactly what was received. Members verify
the coordinator signature, the `attempt_context_hash`, **and
`taproot_merkle_root` against their session root** before signing.
- **Equivocation detection** extending #4044: a coordinator distributing
different bodies (incl. different roots) to different members is
self-incriminating.
- **Blame split (corrected post-review — see below):** the **Rust engine
does pure FROST math** and returns mathematically-failing members as
*candidate* culprits; **all envelope verification + authoritative
blame** runs **Go-side at the f+1 accuser quorum**, re-checking each
accused share against that member's retained envelope. `culprits == full
subset` is treated as suspect (coordinator misconfig, not universal
cheating).
- **FFI structured-culprit payload** (#4052 P2): typed `culprits:
Option<Vec<u16>>` on the error response so candidate culprits are
machine-readable.
- **Completion marker** + **cross-language vectors**.

## Post-review correction

The companion discussion doc resolves the three open questions. The
substantive change: an earlier draft had the *engine* verify envelopes —
**wrong**, on a converging Gemini+Codex P1. The engine has no
operator-key registry (can't verify operator signatures), and a
coordinator-signed envelope is the wrong authentication direction for
*member* blame. The resolution **realigns to the frozen spec** (§5.4/§6
— no spec amendment): engine = pure math → candidate culprits; Go =
envelope verification + authoritative blame at the f+1 quorum vs the
member's retained bytes. Two follow-up Codex P1s folded in: members must
verify the taproot root before signing, and **member-authenticated share
submission is a hard prerequisite** before blame is enabled.

## Plan

Go/Rust split spelled out, a 5-step sub-PR sequence where
**authoritative blame (7.2b-4) lands only after the envelope (7.2b-2)
and the engine's candidate culprits (7.2b-3), and is gated on
member-authenticated share submission**. Three open questions resolved
(recorded in the discussion doc's Decision Log; pending owner sign-off
to record in the gates-doc).

Not a frozen contract — a scoping doc to review before the 7.2b-1
implementation PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

1 participant