spec(frost/roast): normative coordinator-seed derivation (RFC-21 Annex A) + cross-language conformance vectors#4030
Conversation
…s-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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…not a transcript hash Review on the paired Rust PR caught the engine seeding the shuffle from its internal SHA256(message) transcript digest while the Go layer seeds from messageDigestFromBigInt(request.Message) -- the padded 32-byte message itself. Make the annex unambiguous: MessageDigest is the raw signing message big-endian left-padded to 32 bytes (>32 significant bytes rejected), never a transcript hash of it; the engine's internal digest feeds only round_id/attempt_id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Annex A amended (latest commit): |
… event A regen run that produces different bytes means the normative Annex A derivation changed; it requires an annex update in the same change and a mixed-fleet rollout note. Both language suites passing after a dual regen is not evidence of compatibility with deployed engines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ cross-language conformance vectors (#4031) Stacked on #4005 (base: `extraction/frost-signer-mirror-2026-05-26`). Implements item 3 of the review feedback (duplicated, divergent protocol constants) — Rust half; pairs with the Go-side PR #4030 stacked on #3866. ## Problem Flagged in #4026: the engine validated attempt contexts using `int64_be(MessageDigest[0..8])` with the 1-based wire attempt number (the legacy `signingAttemptSeed` convention), while the Go RFC-21 layer derives `fold(SHA256(KeyGroup ‖ SessionID ‖ MessageDigest))` with 0-based attempt numbers. At Phase-7 wiring, every Go-derived attempt context would fail the engine's strict-mode `validate_attempt_context` — a deterministic, network-wide liveness failure invisible to either side's property tests. ## What changed - **`roast_attempt_shuffle_seed(key_group, session_id, message_digest_hex)`** implements the normative RFC-21 Annex A derivation (see #4030). The key-group handle — this engine's hex-encoded serialized group verifying key — feeds the hash as an opaque UTF-8 string, exactly matching keep-core's `attempt.DeriveAttemptSeed` + `foldAttemptSeed` composition, including the strict 32-byte digest requirement. - **`validate_attempt_context` now takes the session's key group** (threaded from `dkg.key_group` at StartSignRound and the session's `DkgResult` at FinalizeSignRound) and composes the shuffle source with the **0-based** RFC-21 attempt number. The FFI wire encoding stays 1-based (`attempt_number >= 1` still enforced; `wire = AttemptNumber + 1`); the engine subtracts one before composition, per the annex. - **`testdata/coordinator_seed_vectors.json`** — byte-identical copy of the canonical file generated from the Go implementation. `coordinator_seed_derivation_matches_cross_language_vectors` pins, for all ten vectors: the folded seed (including negative values, so an unsigned port cannot pass), the selected coordinator (including the n=100 production-shape set), the 0-/1-based wire mapping, and end-to-end strict-mode `validate_attempt_context` acceptance of a context built from the wire encoding. Either language drifting now fails its own unit suite. - **`docs/roast-coordinator-seed-derivation.md`** mirrors the normative annex for signer-side readers, with the regen/copy procedure. - The coordinator-mismatch test derives the provably-wrong coordinator instead of hardcoding member 1 (which, under the new seed, happened to become the correct selection — exactly the class of silent assumption these vectors exist to catch). ## Notes - Mixed-version note: engines on the old derivation reject contexts produced under the new one (and vice versa) — strict-mode attempt contexts are not yet produced by the Go layer in any deployment, so this is pre-wiring cleanup with no live-fleet impact. - The attempt-context vector suite (`roast-attempt-context-v1.json`) is unaffected: it pins fingerprint/attempt-id domains with the coordinator as an *input*. - Port back to the tBTC monorepo signer alongside the next extraction sync. ## Tests Full suite: 245 passed, 0 failed; clippy and rustfmt clean. New conformance test exercises all ten cross-language vectors. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
09f61d3
into
feat/frost-schnorr-migration-scaffold
…e coordinator shuffle (#4034) Stacked on #3866 (base: `feat/frost-schnorr-migration-scaffold`). Implements the review item "widen the Go↔Rust math/rand parity from a handful of pinned vectors to corpus-based differential fuzzing" — Go half; the Rust consumer is the paired PR stacked on #4005. ## What A generated 600-case differential corpus over `SelectCoordinator` (`testdata/coordinator_shuffle_corpus.json`, 176 KB), replayed by `TestCoordinatorShuffle_DifferentialCorpus` here and by the identical byte-for-byte copy in the Rust signer's `go_math_rand` tests: - **216 boundary cases**: seeds {0, ±1, `i64::MIN/MAX`, `MIN+3`/`MAX−3`, the #4026 pin seed and its negation} × attempts {0, 1, 7, `u32::MAX`} — exercising the two's-complement wrapping `seed + attempt` composition — × six member sets including unsorted and reversed inputs (pinning the internal sort both implementations perform). - **384 generated cases**: fixed-seed generator sweeping set sizes 1..255 (the full `group.MemberIndex` range), full-range `int64` seeds, and small/large/extreme attempt numbers. Regeneration is deterministic and gated (`ROAST_SHUFFLE_CORPUS_REGEN=1`), so the corpus provably comes from the documented case matrix rather than hand-pinning. This complements #4030's Annex-A seed-derivation vectors: those pin the *derivation* end-to-end on 10 vectors; this corpus stress-pins the *shuffle port itself* — the actual cross-language landmine — at volume, including the integer-boundary regions where a port diverges first. Not full continuous fuzzing (no coverage-guided harness); it's the pragmatic corpus-differential version that rides the existing unit-test CI on both sides at negligible cost. A coverage-guided Go-oracle harness can layer on later if desired. ## Tests `go test ./pkg/frost/roast/...` passes (corpus replay + regeneration roundtrip verified). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…chain timeouts (#4032) Stacked on #4030 (base: `unify/roast-coordinator-seed-go-2026-06-11`), which is itself stacked on #3866 — so the annex numbering (A: seed derivation, B: latency budget) lands in order. Implements item 4 of the review feedback (serial attempts vs real ROAST concurrency: "it should be a computed bound in the docs"). ## What Adds **RFC-21 Annex B (informative): serial-attempt latency budget vs on-chain timeouts** — the computed bound with current code and chain values, and an honest statement of which constraint actually binds: - **Parameters**: 41 blocks (≈8.2 min) per attempt (`signingAttemptMaximumBlocks`), `signingAttemptsLimit = 5`, engine ROAST coordinator timeout 30 s (sub-dominant), n=100/t=51/f=49, `redemptionTimeout` 5 days, moving-funds timeouts 7 days. - **Serial-delay arithmetic**: as deployed, ≤ ~41 min before the loop gives up (~175× inside the redemption timeout); the review's f·τ worst case at a hypothetical f+1 = 50-attempt limit is ~6.8 h (~17× inside). Serial latency comfortably fits the deadlines whenever attempts can succeed at all. - **The honest caveat**: the binding liveness constraint is **all-honest-subset sampling**, not serial latency. Because the transitional finalize requires every included member to contribute, per-attempt success is `∏(49−i)/(100−i)` — 0.49 / 0.24 / 0.11 / 0.025 for f = 1/2/3/5 — so for f ≥ 3 the 5-attempt loop fails with better-than-even odds long before any timeout is approached. The existing `signingAttemptsLimit` rationale in `pkg/tbtc/node.go` explicitly assumes f ≤ 2; beyond that the backstops are operator-inactivity claims, redemption-timeout slashing, and wallet retirement — all outside the signing loop. (This profile is inherited from the tECDSA-era loop, not introduced by FROST.) - **Codified recommendation**: before the ECDSA-retirement phases, adopt t-of-included finalize (first t responsive members — groundwork already in the signer's `true-late-t-of-n-finalize-considerations.md`) at minimum for redemption signings; treat bounded `n−t+1` concurrency as the follow-on; until then alert when observed attempt-failure rates imply f ≥ 3 behaviour. Docs-only; no code change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Stacked on #3866 (base:
feat/frost-schnorr-migration-scaffold). Implements item 3 of the review feedback (duplicated, divergent protocol constants) — Go half; the Rust half is the paired PR stacked on #4005.Problem
The coordinator-shuffle seed derivation exists twice, in two languages, on two branches, with no single source of truth — and the two copies disagree (flagged in #4026):
fold(SHA256(KeyGroup ‖ SessionID ‖ MessageDigest))int64_be(MessageDigest[0..8])(legacysigningAttemptSeedconvention)At Phase-7 wiring, every Go-derived attempt context would fail the Rust engine's strict-mode validation — a network-fracturing liveness failure that property tests on either side cannot catch.
What this PR does (Go half)
RFC-21 Annex A (normative) — single normative definition of the derivation: inputs (including the exact
KeyGroupBytesdefinition forFrostTBTCSignerV1material — the UTF-8 bytes of the hex key-group handle, treated opaquely), the 0-based composition with the two's-complement-wrapping addition, thewire = AttemptNumber + 1FFI mapping, and the accepted non-goals (unframed concatenation, first-8-byte fold, grindability bounds) with rationale. The Go derivation is adopted as normative: it binds key group + session + digest rather than the digest alone, and the livepkg/tbtcsigning loop's legacy convention is explicitly documented as the thing Phase 7 migrates from.Generated conformance vectors —
pkg/frost/roast/testdata/coordinator_seed_vectors.json: ten end-to-end vectors (folded seed int64 + selected coordinator) covering attempts 0/1/3/5/7, sparse and production-size (n=100) member sets, opaque key-group handles, and negative folded seeds. Regenerated from the deterministic input matrix viaROAST_SEED_VECTORS_REGEN=1 go test -run TestRegenerateCoordinatorSeedVectors— generation-from-spec rather than hand-pinning, per the review.Conformance test —
TestCoordinatorSeedDerivation_ConformanceVectorspinsDeriveAttemptSeed → foldAttemptSeed → SelectCoordinatorend to end against the file, asserts the wire-mapping invariant on every vector, and requires at least one negative-seed pin so an unsigned-integer port cannot pass.The paired Rust PR switches the engine to this derivation (subtracting 1 from the wire attempt number before composition) and consumes a byte-identical copy of the vector file, so either side drifting fails its own CI rather than fracturing coordinator agreement in a mixed deployment.
No behavior change on the Go side — it was already normative-conformant; this PR makes that the specified behavior and pins it.
Tests
go test ./pkg/frost/...passes; vectors verified present with 7 negative-seed pins out of 10.🤖 Generated with Claude Code