feat(frost/roast): RFC-21 Phase 3.4 -- NextAttempt policy + thresholds#3971
Merged
mswilkison merged 1 commit intoMay 23, 2026
Conversation
Closes Phase 3 of RFC-21 by implementing the deterministic
(AttemptContext, TransitionMessage) -> AttemptContext policy that
makes ROAST-aware retry possible.
* pkg/frost/roast/next_attempt.go
- OverflowExclusionThreshold = 4 (RFC-21 Layer B constant).
- ErrAttemptInfeasible sentinel for the threshold floor.
- Coordinator.NextAttempt(handle, bundle, threshold, dkgPubKey).
- computeNextAttempt pure-function policy core, independently
testable without a Coordinator instance.
- overflowBlamedSenders sums per-sender overflow counts across
every snapshot in the bundle and returns those meeting the
threshold.
- memberSet helper for set arithmetic over group.MemberIndex.
- filterOut for ordered subtraction.
* pkg/frost/roast/attempt/attempt_context.go
- AttemptContext gains a TransientlyParked field so parking
metadata flows between attempts via the canonical hash. Parked
members are skipped from THIS attempt only; the attempt after
that automatically reinstates them.
- NewAttemptContext preserves its seven-argument signature
(attempt-zero / no-parking shape); the new
NewAttemptContextWithParking is the constructor used by
NextAttempt.
- Hash() includes the parked set (between ExcludedSet and
AttemptSeed in the canonical encoding).
- Pinned-fixture reference encoder updated to match.
* pkg/frost/roast/coordinator_state.go
- Coordinator interface gains NextAttempt.
Policy (matches RFC-21 Resolved Decision on silence-parking
transience):
1. Permanent exclusion (transport-blamable): overflow count
summed across the bundle >= OverflowExclusionThreshold.
2. Permanent exclusion (validation-blamable): reject events --
no-op in Phase 3.4 since the reject category does not yet
exist on the recorder; hook documented for a later phase.
3. Silence parking: senders in prev IncludedSet not present in
bundle, not now permanently excluded -- moved to
TransientlyParked for ONE attempt.
4. Reinstatement: prev TransientlyParked members rejoin
IncludedSet automatically.
5. Infeasibility: if next IncludedSet < threshold, return
ErrAttemptInfeasible.
The "strictly transient" parking discipline is the formal
mitigation Gemini's review asked for: a peer falsely labelled
silent (network blip, coordinator censorship caught at
VerifyBundle) is reinstated by the very next attempt without
intervention.
Tests (15 cases in next_attempt_test.go):
* No-evidence baseline: IncludedSet unchanged, attempt number
incremented.
* Overflow threshold (4 observers x 1 event = 4) triggers
permanent exclusion.
* Overflow below threshold (1 < 4) does NOT exclude.
* Silent member moved to TransientlyParked, not ExcludedSet.
* Previously parked member is reinstated to IncludedSet.
* Full park/reinstate cycle across two transitions (N -> N+1 ->
N+2): the originally-silent member appears in N+1's
TransientlyParked and N+2's IncludedSet.
* Original signer set size (|Inc| + |Exc| + |Park|) preserved
across transitions.
* Determinism: identical inputs produce identical AttemptContext
hashes.
* Infeasibility: threshold of 5 with only 3 included members
returns ErrAttemptInfeasible.
* threshold=0 disables the infeasibility check (test seam).
* Overflow counts summed across observers, not maxed.
* Nil bundle rejected.
* Unknown handle rejected with ErrUnknownAttempt.
* OverflowExclusionThreshold constant matches RFC-21 specification.
All pass under: go test ./pkg/frost/roast/..., go test -race
./pkg/frost/roast/..., go test -tags 'frost_native frost_tbtc_signer'
./pkg/frost/..., staticcheck -checks '-SA1019' ./pkg/frost/roast/...,
go vet ./pkg/frost/roast/..., gofmt -l ./pkg/frost/roast/.
Stacked on Phase 3.3 (#3970). Completes the Phase 3 surface.
Base automatically changed from
feat/frost-roast-bundle-aggregation-2026-05-22
to
feat/frost-schnorr-migration-scaffold
May 23, 2026 01:07
3813a62
into
feat/frost-schnorr-migration-scaffold
14 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes Phase 3 of RFC-21. Adds the deterministic
`(AttemptContext, TransitionMessage) -> AttemptContext` policy that
makes ROAST-aware retry possible.
Two honest signers fed the same previous context and the same
verified bundle compute byte-identical next contexts -- the
foundational invariant the coordinator-aggregation model exists to
enforce.
Stacked on #3970 (Phase 3.3).
What lands
Policy (RFC-21 Layer B)
AttemptContext: `TransientlyParked` field
Parking metadata must flow between attempts because two honest
coordinators computing the next attempt must agree on who is parked.
Including `TransientlyParked` in the canonical hash binds the
contract.
The Phase 1A pinned-fixture test's inline reference encoder is
updated in lockstep, so any future drift between the production
encoder and the reference is still caught at code review.
Why "strictly transient"
This was the formal mitigation Gemini's Phase-3 design review asked
for. A peer falsely labelled silent (network blip, coordinator
censorship caught at `VerifyBundle`) is reinstated by the very next
attempt without intervention. Permanent exclusion only follows from
overflow or validation reject, neither of which can fire on a
slow-but-honest peer.
`NextAttempt(handle, bundle, threshold, dkgGroupPublicKey)`
Threshold = 0 disables the infeasibility check (test seam; never
production).
Test coverage
15 new cases in `next_attempt_test.go` covering:
defining test for "strictly transient" parking
Phase 3 status
Phase 3 is now feature-complete. Phase 4 wires receivers to a
real coordinator instance behind the `frost_roast_retry` build tag.
Verification
Test plan
(Phase 1B field is optional during Phases 1-5, so live peers
running older code can still interop without consuming the hash).
what RFC-21's Resolved Decisions section specifies.