Skip to content

feat(frost/roast): RFC-21 Phase 3.4 -- NextAttempt policy + thresholds#3971

Merged
mswilkison merged 1 commit into
feat/frost-schnorr-migration-scaffoldfrom
feat/frost-roast-next-attempt-2026-05-22
May 23, 2026
Merged

feat(frost/roast): RFC-21 Phase 3.4 -- NextAttempt policy + thresholds#3971
mswilkison merged 1 commit into
feat/frost-schnorr-migration-scaffoldfrom
feat/frost-roast-next-attempt-2026-05-22

Conversation

@mswilkison
Copy link
Copy Markdown
Contributor

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)

Step Logic
1. Permanent exclusion (transport) overflow count summed across bundle ≥ `OverflowExclusionThreshold` (constant = 4).
2. Permanent exclusion (validation) reject events; no-op hook (reject category lands in a later phase).
3. Silence parking (strictly transient) senders in prev IncludedSet not present in bundle, not now permanently excluded → moved to TransientlyParked for one attempt only.
4. Reinstatement prev TransientlyParked members rejoin IncludedSet automatically.
5. Infeasibility if next IncludedSet < threshold → `ErrAttemptInfeasible`.

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)`

Parameter Why
`handle` identifies the previous attempt (its IncludedSet + ExcludedSet + TransientlyParked)
`bundle` verified TransitionMessage (caller must call `VerifyBundle` first)
`threshold` FROST signing threshold `t` for the key group; constant across a session
`dkgGroupPublicKey` per RFC-21 Decision 2: extract from FFI signer material at attempt construction

Threshold = 0 disables the infeasibility check (test seam; never
production).

Test coverage

15 new cases in `next_attempt_test.go` covering:

  • No-evidence baseline (IncludedSet unchanged, AttemptNumber++)
  • Overflow at exact threshold triggers permanent exclusion
  • Overflow below threshold does not exclude
  • Silent member → `TransientlyParked`, not `ExcludedSet`
  • Previously parked → reinstated to `IncludedSet`
  • Full park/reinstate cycle across N → N+1 → N+2 -- the
    defining test for "strictly transient" parking
  • Original signer set size preserved across transitions
  • Determinism: same inputs → same next-context hash
  • Infeasibility error when below threshold
  • Threshold = 0 disables check
  • Overflow summed across observers (not maxed)
  • Nil bundle rejected
  • Unknown handle returns `ErrUnknownAttempt`
  • `OverflowExclusionThreshold` constant matches RFC-21 spec

Phase 3 status

PR Scope State
3.1 (#3968) Coordinator skeleton + seed bridge open, Gemini-approved
3.2 (#3969) TransitionMessage + LocalEvidenceSnapshot open, Gemini-approved
3.3 (#3970) Aggregation + bundle verification open, Gemini-approved
3.4 (this) NextAttempt policy + thresholds open

Phase 3 is now feature-complete. Phase 4 wires receivers to a
real coordinator instance behind the `frost_roast_retry` build tag.

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

Test plan

  • CI green.
  • Reviewer confirms the canonical-hash extension is acceptable
    (Phase 1B field is optional during Phases 1-5, so live peers
    running older code can still interop without consuming the hash).
  • Reviewer confirms the parking discipline as documented matches
    what RFC-21's Resolved Decisions section specifies.

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
@mswilkison mswilkison merged commit 3813a62 into feat/frost-schnorr-migration-scaffold May 23, 2026
14 checks passed
@mswilkison mswilkison deleted the feat/frost-roast-next-attempt-2026-05-22 branch May 23, 2026 01:11
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