Skip to content

Commit a7dd330

Browse files
mswilkisonclaude
andcommitted
spec(frost/roast): normative coordinator-seed derivation annex + cross-language conformance vectors
The coordinator-shuffle seed existed in two divergent implementations: the Go RFC-21 layer derives fold(SHA256(KeyGroup || SessionID || MessageDigest)) with 0-based attempt numbers, while the Rust signer's attempt-context validation used the first 8 bytes of the raw message digest with 1-based wire attempts (the legacy signingAttemptSeed convention). Flagged in #4026; at Phase-7 wiring every Go-derived attempt context would fail Rust-side validation. This PR makes the Go derivation normative and durable: - RFC-21 Annex A (normative) specifies the derivation, the KeyGroupBytes definition for FrostTBTCSignerV1 material, the 0-based/1-based wire mapping (wire = AttemptNumber + 1), wrapping semantics, and the accepted non-goals (unframed concatenation, first-8-bytes fold, grindability bounds) with rationale. - pkg/frost/roast/testdata/coordinator_seed_vectors.json pins ten end-to-end vectors (seed int64 + selected coordinator), covering attempt 0/1/5/3/7, sparse and production-size (n=100) member sets, opaque key-group handles, and negative folded seeds. The file is regenerated from the Go implementation via ROAST_SEED_VECTORS_REGEN=1 (TestRegenerateCoordinatorSeedVectors), so the vectors provably come from the spec'd input matrix. - TestCoordinatorSeedDerivation_ConformanceVectors consumes the file and pins DeriveAttemptSeed -> foldAttemptSeed -> SelectCoordinator end to end, including the wire-mapping invariant and at least one negative-seed pin so an unsigned port cannot pass. The Rust signer adopts the same derivation and a byte-identical vector copy in the paired PR stacked on #4005. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b7317f2 commit a7dd330

3 files changed

Lines changed: 880 additions & 0 deletions

File tree

docs/rfc/rfc-21-roast-coordinator-retry-and-transition-evidence.adoc

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,83 @@ gossiped attestations *are* the persistent record.
732732
signer remains a single-process engine; coordinator state lives on
733733
the keep-core side.
734734

735+
== Annex A (normative): coordinator-shuffle seed derivation
736+
737+
This annex is the single normative definition of the coordinator-
738+
shuffle seed. Two independent implementations exist -- the Go RFC-21
739+
layer (`pkg/frost/roast`) and the Rust tbtc-signer attempt-context
740+
validation (`pkg/tbtc/signer/src/engine.rs`) -- and both MUST derive
741+
byte-identical values from this text. Conformance is pinned by the
742+
shared vector file regenerated from the Go implementation
743+
(`pkg/frost/roast/testdata/coordinator_seed_vectors.json`, mirrored
744+
byte-identically to `pkg/tbtc/signer/testdata/`); any divergence
745+
fails the drifting side's own unit suite.
746+
747+
=== Inputs
748+
749+
* `KeyGroupBytes`: the UTF-8 bytes of the canonical FROST key-group
750+
handle. For `FrostTBTCSignerV1` signer material this handle is the
751+
lowercase hex encoding of the serialized group verifying key as
752+
produced by the tbtc-signer engine (the `KeyGroup` string), and
753+
`ExtractDkgGroupPublicKeyFromMaterial` returns exactly these bytes.
754+
The handle is treated as an opaque byte string; implementations
755+
MUST NOT decode it to point bytes before hashing.
756+
* `SessionID`: the session identifier's raw UTF-8 bytes.
757+
* `MessageDigest`: the 32-byte signing message digest.
758+
* `AttemptNumber`: the RFC-21 attempt number, **0-based** (attempt
759+
zero is the first attempt). The tbtc-signer FFI `AttemptContext`
760+
carries `wire_attempt_number = AttemptNumber + 1` (1-based, zero
761+
rejected); implementations consuming the wire form MUST subtract
762+
one before the composition below.
763+
* `IncludedSet`: the attempt's included member indices.
764+
765+
=== Derivation
766+
767+
----
768+
AttemptSeed32 = SHA256(KeyGroupBytes || SessionID || MessageDigest)
769+
ShuffleSeed_i64 = int64_from_be_bytes(AttemptSeed32[0..8])
770+
SourceSeed_i64 = ShuffleSeed_i64 + int64(AttemptNumber) // two's-complement wrap
771+
Coordinator = GoMathRandShuffle(sort_ascending(IncludedSet), SourceSeed_i64)[0]
772+
----
773+
774+
`GoMathRandShuffle` is the exact shuffle of Go's legacy `math/rand`:
775+
`rand.New(rand.NewSource(seed)).Shuffle` -- ported bit-exactly to
776+
Rust in `pkg/tbtc/signer/src/go_math_rand.rs` and pinned by the
777+
cross-language vectors of keep-core PRs #4026/#4027. The addition is
778+
two's-complement wrapping on both sides (Go's defined signed
779+
overflow; Rust `wrapping_add`).
780+
781+
=== Non-goals and accepted properties
782+
783+
* The concatenation `KeyGroupBytes || SessionID || MessageDigest` is
784+
not length-framed, so input-boundary collisions are theoretically
785+
expressible. This is accepted: the seed feeds a *non-cryptographic*
786+
shuffle whose only job is deterministic agreement on rotation
787+
order among honest members; every input is independently and
788+
unambiguously bound (and framed) in the `AttemptContext` hash that
789+
protocol messages verify. The seed is not a security boundary, and
790+
no exclusion or signing decision may ever be derived from it.
791+
* Only the first 8 bytes of `AttemptSeed32` influence the shuffle;
792+
the remaining 24 bytes are bound via the `AttemptContext` hash
793+
only.
794+
* Grindability: an adversary who can choose `SessionID` or
795+
`MessageDigest` can bias coordinator rotation order. Session and
796+
message identifiers are protocol-fixed inputs (deposit/redemption
797+
driven), not free adversary choices; rotation-order bias degrades
798+
at worst into the serial-attempt latency bound, never into a
799+
safety property.
800+
801+
=== History
802+
803+
Before this annex, the two implementations diverged: the Go layer
804+
derived `fold(SHA256(KeyGroup || SessionID || MessageDigest))` with
805+
0-based attempts while the Rust engine used the first 8 bytes of the
806+
raw `MessageDigest` with 1-based wire attempts (the legacy
807+
`signingAttemptSeed` convention of the pre-ROAST signing loop, which
808+
`pkg/tbtc/signing_loop.go` retains until Phase-7 migrates it to this
809+
annex). The divergence was flagged in keep-core PR #4026 and resolved
810+
by adopting the Go derivation as normative.
811+
735812
== References
736813

737814
* Ruffing, Ronge, Aranha, Schneider. ``ROAST: Robust Asynchronous

0 commit comments

Comments
 (0)