Skip to content

Phase 7.2b-4a: candidate-culprit blame classifier#4063

Merged
mswilkison merged 2 commits into
feat/frost-schnorr-migration-scaffoldfrom
feat/frost-7.2b-4a-blame-classifier
Jun 15, 2026
Merged

Phase 7.2b-4a: candidate-culprit blame classifier#4063
mswilkison merged 2 commits into
feat/frost-schnorr-migration-scaffoldfrom
feat/frost-7.2b-4a-blame-classifier

Conversation

@mswilkison

@mswilkison mswilkison commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

What

ClassifyCandidateCulprits (Phase 7.2b-4a) turns the engine's candidate culprits (members whose FROST shares failed verification — shipped in the Rust engine as #4062) into this observer's RejectEntry accusations, adjudicated against the Round2Collector's retained operator-signed bytes. This is where the crypto-only engine's verdict becomes envelope-bound, attributable member blame — the Go side of the frozen Q1 boundary. Design converged via a Codex+Gemini consultation.

Classification (per candidate, against the attempt's authoritative package)

candidate state result
accepted retained share, re-verifies ShareInvalid RejectEntry{Sender, "invalid_signature_share", 1}
accepted retained share, re-verifies ShareValid (yet engine-flagged) nothing
divergent share only nothing (neutral)
no retained share here nothing
ShareIndeterminate re-verification nothing (fail closed)
  • Self-incrimination gate: a reject is emitted only when the observer holds the member's operator-signed share and re-verifies it ShareInvalid against the package it accepted — independently checkable (RFC-21 Layer B's "no bare counters"). Accusations feed this observer's LocalEvidenceSnapshot.RejectsNextAttempt's existing f+1 establishment gate; this never excludes anyone by itself.
  • Neutral framing (Q1): a valid-but-flagged candidate is not asserted to be coordinator substitution — the cause isn't provable from these bytes, so the member simply isn't blamed. Coordinator-directed faults are a separate path (7.2b-4b), explicitly out of scope here.
  • Divergent shares stay neutral — possible targeted coordinator equivocation, so must not alone exclude.

The Round2ShareVerifier seam (tri-state)

FROST share re-verification is engine-backed: Go has no native FROST share crypto (the collector's SignatureVerifier checks operator signatures, not the FROST share equation). The new Round2ShareVerifier interface is that seam — pure crypto, no blame, within the engine's crypto-only boundary. It returns a deliberate tri-state ShareVerificationResult, not (bool, error):

  • ShareValid — valid FROST share → don't blame.
  • ShareInvalidmember-attributable garbage: mathematically invalid or undecodable share bytes the member operator-signed → blame.
  • ShareIndeterminate — not the member's fault (missing verifying material, ambiguous key/root, undecodable authoritative package, engine/FFI failure) → fail closed.

The classifier blames only ShareInvalid. The enum forces the (future, engine-backed) implementer to categorize the member-fault boundary deliberately — a (bool, error) shape would silently route a member's undecodable bytes into the error channel and let the cheater dodge blame. It's injected, so this lands/reviews now with a fake verifier; the engine-backed impl wires in with the (still-unbuilt) interactive orchestration.

Notes

  • Reuses the existing reject category + ExclusionAccuserQuorum/NextAttempt f+1 machinery; ConflictEntry stays reserved for self-conflicting bytes.
  • Deterministic (deduped, ascending by member, Count 1) so honest observers over identical bytes agree byte-for-byte.
  • Retained bytes snapshotted (owned copies) under the collector lock; re-verification runs lock-free.

Tests

round2_classifier_test.go — full matrix (ShareInvalid→reject, ShareValid→nothing, ShareIndeterminate→nothing, divergent→neutral + verifier-not-consulted, absent→nothing + not-consulted), multi-candidate dedupe/sort/determinism, errors (nil verifier, unknown attempt, no package, no candidates). Full roast package + gofmt + vet green.

Review fold

  • Gemini [P2] (head 8f8e5bf1a): replaced the (valid, err) verifier seam with the ShareVerificationResult tri-state so a member's undecodable/garbage share bytes can't be misrouted as "indeterminate" and dodge blame. Codex: no issues.

Follow-ups

  • 7.2b-4b: the coordinator/package + divergent-share f+1 comparison path (coordinator-equivocation completeness).
  • Engine-backed Round2ShareVerifier impl, wired with the interactive orchestration.

🤖 Generated with Claude Code

ClassifyCandidateCulprits turns the engine's candidate culprits (the members
whose FROST shares failed verification, #4062) into this observer's RejectEntry
accusations, adjudicated against the Round2Collector's retained operator-signed
bytes - where the engine's crypto verdict becomes envelope-bound, attributable
member blame (frozen Q1 boundary; the engine never inspects envelopes).

Per attempt, for each candidate against the authoritative package:
- accepted retained share that re-verifies INVALID -> RejectEntry (the observer
  holds the member's operator-signed share: self-incriminating, independently
  checkable; feeds NextAttempt's existing f+1 establishment gate).
- accepted share that re-verifies VALID (yet flagged) -> nothing: not
  self-incriminating under this observer's package; coordinator-directed faults
  are a SEPARATE path (7.2b-4b), never inferred here.
- divergent share only -> nothing (NEUTRAL: possible targeted coordinator
  equivocation, must not alone exclude).
- no retained share / indeterminate re-verification -> nothing (fail closed).

FROST share re-verification is engine-backed (Go has no native FROST share
crypto - the collector's SignatureVerifier checks operator sigs, not the share
equation): the new Round2ShareVerifier interface is that seam, injected so this
lands and reviews now with a fake verifier; the engine-backed impl wires in with
the interactive orchestration. Reuses the existing reject category +
ExclusionAccuserQuorum/NextAttempt f+1 machinery; ConflictEntry stays reserved
for self-conflicting bytes.

Deterministic output (deduped, ascending by member, Count 1). Retained bytes are
snapshotted under the collector lock; re-verification runs lock-free.

Design converged via Codex+Gemini consultation.

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

coderabbitai Bot commented Jun 15, 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: bf255124-71f9-4656-bf22-ecb83b3ce634

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.2b-4a-blame-classifier

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.

…4063)

The (valid bool, error) verifier seam conflated a member's OWN undecodable /
garbage FROST share bytes (operator-signed -> self-incriminating -> blame) with
a not-the-member's-fault execution failure. A Go FFI bridge would naturally map
a Rust/FROST deserialization error to a Go error, which the classifier read as
"indeterminate -> don't blame", letting that cheater dodge a RejectEntry.

Replace it with an explicit ShareVerificationResult tri-state
(ShareValid / ShareInvalid / ShareIndeterminate): ShareInvalid covers both a
mathematically invalid share AND undecodable member bytes; ShareIndeterminate is
reserved for failures that are NOT the member's fault. The classifier blames
ONLY ShareInvalid - everything else, including any future verdict, fails closed.
This forces the engine-backed implementer to categorize the boundary
deliberately rather than leak member-fault into an error channel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mswilkison mswilkison merged commit 3dcaf5b into feat/frost-schnorr-migration-scaffold Jun 15, 2026
15 checks passed
@mswilkison mswilkison deleted the feat/frost-7.2b-4a-blame-classifier branch June 15, 2026 16:39
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