Commit 786cead
authored
feat(frost/roast): RFC-21 Phase 3.3 -- aggregation + bundle verification (#3970)
## Summary
Third Phase-3 implementation PR. Adds the methods that drive the
ROAST coordinator-aggregation flow defined in RFC-21's **Resolved
Decisions** section:
| Method | Role |
|---|---|
| \`RecordEvidence(handle, snap)\` | Accept a peer's signed
\`LocalEvidenceSnapshot\`. Validates structure, verifies the operator
signature, checks the snapshot's \`AttemptContextHash\` matches the
handle's bound context, applies first-write-wins / equal-or-reject. |
| \`AggregateBundle(handle)\` | Called by the elected coordinator. Sorts
accumulated snapshots ascending by \`SenderID\`, builds the
\`TransitionMessage\`, signs the canonical bundle bytes, transitions
state to \`Transitioned\`. |
| \`VerifyBundle(handle, msg)\` | Called by every receiver. Verifies
coordinator signature, every snapshot's operator signature, and -- if
the receiver has submitted its own snapshot -- presence of that snapshot
in the bundle (censorship detection). |
**Stacked on #3969 (Phase 3.2).**
## What's new
### \`pkg/frost/roast/signature.go\`
- \`Signer\` / \`SignatureVerifier\` interfaces (Phase 4 wires them to
\`pkg/net\`'s operator-key + member-keys surfaces).
- \`NoOpSigner\` / \`NoOpSignatureVerifier\` for tests that don't
exercise the crypto pipeline.
- \`CanonicalSnapshotBytes\` -- JSON of snapshot fields *excluding*
\`OperatorSignature\`.
- \`CanonicalBundleBytes\` -- JSON of bundle fields *excluding*
\`CoordinatorSignature\` but *including* every snapshot's
\`OperatorSignature\`, so the coordinator's signature attests to the
exact assembled set.
- \`verifySnapshotSignature\` / \`verifyBundleSignature\` /
\`verifyOwnObservationsPresent\` -- the receiver-side checks, each
testable in isolation.
- Sentinels: \`ErrSignatureInvalid\`, \`ErrSignatureMissing\`,
\`ErrCensorshipDetected\`.
### \`pkg/frost/roast/coordinator_state.go\` (extended)
- \`Coordinator\` interface gains \`RecordEvidence\`,
\`AggregateBundle\`, \`VerifyBundle\`.
- \`NewInMemoryCoordinatorWithSigning(selfMember, signer, verifier)\` --
production constructor (Phase 4 callers).
- \`NewInMemoryCoordinator()\` preserved as a Phase-3.1-compatible
convenience that uses no-op signing.
- New sentinels: \`ErrNotAggregator\`, \`ErrAttemptStateInvalid\`,
\`ErrAttemptContextMismatch\`, \`ErrSnapshotConflict\`.
### \`pkg/frost/roast/transition_message.go\` (touched)
- \`validate()\` methods promoted to public \`Validate()\` on both
\`LocalEvidenceSnapshot\` and \`TransitionMessage\` so callers that
construct messages in memory can validate without a marshal/unmarshal
round-trip.
## What's tested
### \`signature_test.go\` (13 cases)
Signature interfaces, canonical encodings, signature verification
round-trips (via a deterministic SHA-256 fake signer/verifier),
tampered-payload rejection, coordinator-mismatch rejection,
censorship-detection helper (missing snapshot, mutated signature, skip
semantics).
### \`bundle_aggregation_test.go\` (11 cases)
- \`RecordEvidence\`: nil rejection, unknown handle, context hash
mismatch, bad signature, valid-and-idempotent re-submission, conflict
rejection, self-submission tracking.
- \`AggregateBundle\`: non-aggregator rejection, signed bundle build
(size, ordering, signature, terminal state), **deterministic bundle JSON
across different record orderings**.
- \`VerifyBundle\`: valid acceptance, **censorship detection**,
coordinator-signature forgery, snapshot-signature forgery,
attempt-context mismatch, nil message, unknown attempt, concurrent
record-and-aggregate safety.
### Verification
| Command | Result |
|---|---|
| \`go build ./...\` | clean |
| \`go test ./pkg/frost/roast/...\` | pass |
| \`go test -race ./pkg/frost/roast/...\` | pass |
| \`go test -tags 'frost_native frost_tbtc_signer' ./pkg/frost/...\` |
pass (5 packages) |
| \`staticcheck -checks '-SA1019' ./pkg/frost/roast/...\` | silent |
| \`go vet ./pkg/frost/roast/...\` | clean |
| \`gofmt -l ./pkg/frost/roast/\` | silent |
## Why the censorship-detection check is what it is
A receiver that has submitted its own snapshot but is missing from
the bundle has two possible explanations: (1) the elected coordinator
maliciously dropped the snapshot, or (2) the bundle was assembled
before the receiver's submission arrived. In either case, feeding
the bundle into \`NextAttempt\` would penalise the receiver (via
silence-parking), so the bundle must be rejected pending re-broadcast
on the next attempt. \`ErrCensorshipDetected\` is the unambiguous
signal.
When the receiver has not yet submitted (selfMember == 0 or
selfSubmission == nil), the check is skipped: there is no submitted
snapshot whose presence to verify.
## Phase 3 status
| PR | Scope | State |
|---|---|---|
| 3.1 (#3968) | Coordinator skeleton + seed bridge | open |
| 3.2 (#3969) | TransitionMessage + LocalEvidenceSnapshot | open |
| **3.3 (this)** | **Aggregation + bundle verification** | **open** |
| 3.4 | NextAttempt policy + thresholds | next |
## Test plan
- [ ] CI green.
- [ ] Reviewer confirms the censorship-detection semantics are
acceptable (specifically that we *reject* the bundle on missing
self-snapshot rather than accept it and let silence-parking
catch it).
- [ ] Reviewer confirms the canonical-encoding contract (snapshot
omits its own signature; bundle includes snapshot signatures but
omits its own).5 files changed
Lines changed: 1365 additions & 27 deletions
File tree
- pkg/frost/roast
0 commit comments