Skip to content

feat(frost): wire interactive share-verification blame into the bundle (7.3 share-blame)#4091

Merged
mswilkison merged 1 commit into
feat/frost-schnorr-migration-scaffoldfrom
feat/frost-7.3-share-blame
Jun 19, 2026
Merged

feat(frost): wire interactive share-verification blame into the bundle (7.3 share-blame)#4091
mswilkison merged 1 commit into
feat/frost-schnorr-migration-scaffoldfrom
feat/frost-7.3-share-blame

Conversation

@mswilkison

Copy link
Copy Markdown
Contributor

RFC-21 Phase 7.3 — interactive share-verification blame (the third fault source)

Follows PR2b-2 (the blame layer: 2a #4088 coarse f+1 evidence, 2b #4089 coordinator-equivocation proofs, 2c #4090 tests). This closes the last blame gap.

The gap

The blame layer excluded members for coarse faults and coordinator equivocation, but an interactive bad-share submitter escaped blame entirely. The engine's InteractiveAggregate surfaces candidate culprits in a typed InteractiveAggregateShareVerificationError{CandidateCulprits []uint16}, yet the runner just wrapped and returned the error, dropping them. EngineRound2ShareVerifier and Round2Collector.ClassifyCandidateCulprits were both complete but constructed/called nowhere — so a member that submitted an invalid FROST signature share was never excluded and the retry could loop.

The wiring

On a runner.Run share-verification failure, driveInteractiveRoastSigningIfEnabled (where 2b already extracts proofs):

  1. errors.As the typed *InteractiveAggregateShareVerificationError (through the runner's %w wrap);
  2. converts the engine's uint16 candidates → MemberIndex, dropping 0 / out-of-range so a malformed candidate can't truncate into an honest seat;
  3. type-asserts the engine to Round2ShareVerifyingEngine (skip if absent — still stash 2b proofs);
  4. builds an EngineRound2ShareVerifier bound to the attempt (SessionID == active.SessionID(), AttemptContextHash, TaprootMerkleRoot);
  5. ClassifyCandidateCulprits re-verifies each candidate's retained share (frozen-Q1 boundary: only an ACCEPTED retained share that re-verifies INVALID is blamed; every not-the-member's-fault condition fails closed);
  6. stashes the reject accusations in the same union pending-evidence entry as the 2b proofs.

BroadcastForcedSnapshot carries them; computeNextAttempt's f+1 reject gate excludes an f+1-corroborated bad-share member.

Why f+1 (not instant)

A share is self-incriminating only against the package this observer accepted — a byzantine coordinator's targeted split could otherwise make one honest observer instant-exclude an honest peer. f+1 corroboration (honest observers re-verify identical retained bytes deterministically) closes that. Only unforgeable coordinator-equivocation proofs (2b) are instant.

Safety

Best-effort + fail-safe: a non-share error, a verifier-incapable engine, malformed candidates, or an empty classification all stash nothing. EngineRound2ShareVerifier already fails closed against false blame (bound-attempt / root / submitter / package-body checks). Compile-time assertion that the cgo engine satisfies Round2ShareVerifyingEngine. Stale "evidence/proofs mutually exclusive" comments corrected (one interactive failure now carries both).

Scope / gating

Prod-dormant until the cgo interactive engine is registered (gated on the frost-secp256k1-tr audit).

Verification

  • build + vet across 5 tag combos (default / frost_native / frost_roast_retry / frost_native frost_roast_retry / frost_native frost_tbtc_signer w/ CGO)
  • tests: blame-fires (engine-verified-invalid share → reject through the real drive+runner+collector+verifier), skip-without-verifier, malformed-candidates-dropped; -race; gofmt clean
  • design locked via Codex + Gemini consult (both PROCEED, no holes)

🤖 Generated with Claude Code

…e (7.3 share-blame)

The blame layer (PR2b-2) excluded members for coarse f+1 evidence (2a) and coordinator
equivocation (2b), but an interactive bad-share submitter escaped blame entirely: the
engine's InteractiveAggregate surfaces candidate culprits in a typed
InteractiveAggregateShareVerificationError, yet the runner just wrapped and returned the
error, dropping them. EngineRound2ShareVerifier and Round2Collector.ClassifyCandidate-
Culprits were both complete but constructed/called nowhere -- so a member that submitted
an invalid FROST signature share was never excluded and the retry could loop.

Wire the third fault source on the interactive failure path (mirroring 2b): on a
runner.Run share-verification failure, driveInteractiveRoastSigningIfEnabled type-asserts
the engine to Round2ShareVerifyingEngine, converts the engine's uint16 candidates to
MemberIndex (dropping 0 / out-of-range so a malformed candidate can never truncate into
an honest seat), builds an EngineRound2ShareVerifier bound to the attempt, classifies
each candidate's RETAINED share via ClassifyCandidateCulprits (frozen-Q1 boundary: only
an ACCEPTED retained share that re-verifies INVALID is blamed; every not-the-member's-
fault condition fails closed), and stashes the resulting reject accusations in the same
union pending-evidence entry as the 2b proofs. BroadcastForcedSnapshot carries them;
computeNextAttempt's f+1 reject gate excludes an f+1-corroborated bad-share member.

f+1, not instant: a share is self-incriminating only against the package THIS observer
accepted, so a byzantine coordinator's targeted split could otherwise make one honest
observer instant-exclude an honest peer; f+1 corroboration (honest observers re-verify
identical retained bytes deterministically) closes that. Best-effort + fail-safe: a
non-share error, a verifier-incapable engine, malformed candidates, or an empty
classification all stash nothing. Compile-time assertion that the cgo engine satisfies
Round2ShareVerifyingEngine; the stale "evidence/proofs mutually exclusive" comments are
corrected (one interactive failure now carries both).

Design locked via Codex + Gemini consult (both PROCEED, no holes). Prod-dormant until the
cgo interactive engine is registered (gated on the frost-secp256k1-tr audit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 19, 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: 646844a0-2d41-4182-b01a-ad522caaa9bd

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/frost-7.3-share-blame

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.

@mswilkison mswilkison merged commit 0f6e5f9 into feat/frost-schnorr-migration-scaffold Jun 19, 2026
15 of 16 checks passed
@mswilkison mswilkison deleted the feat/frost-7.3-share-blame branch June 19, 2026 19:50
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