test(frost/roast): pin cross-language coordinator selection vectors#4026
Merged
mswilkison merged 1 commit intoJun 11, 2026
Conversation
The Rust engine ports Go's math/rand shuffle semantics (pkg/tbtc/signer/src/go_math_rand.rs on the FROST signer mirror branch) and pins concrete SelectCoordinator outcomes in select_coordinator_matches_known_keep_core_vectors. The Go side only asserted determinism properties, so a Go-side change to the selection semantics (math/rand/v2, a different shuffle, a different seed composition) would pass the Go suite while silently fracturing coordinator agreement with the Rust engine. Pin the same concrete vectors on the Go side so either side drifting fails its own test suite. 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 |
b7317f2
into
feat/frost-schnorr-migration-scaffold
16 checks passed
mswilkison
added a commit
that referenced
this pull request
Jun 11, 2026
…nce test (#4027) Stacked on #4005 (base: `extraction/frost-signer-mirror-2026-05-26`). Companion to #4026 on the Go branch. ## What `select_coordinator_is_input_order_independent` in `pkg/tbtc/signer/src/go_math_rand.rs` asserted only that two input orderings of the member set agree — not what they agree on. This pins the concrete result (`Some(4)` for `(members=[1..6], seed=333, attempt=4)`), matching the value now pinned on the Go side in `TestSelectCoordinator_CrossLanguagePinnedVectors` (#4026). ## Why Coordinator selection must agree byte-for-byte between Go's `SelectCoordinator` and this Rust port of Go's `math/rand` shuffle, or honest nodes elect different coordinators and the ROAST liveness path fractures. With the vector sets symmetric across both test suites, either implementation drifting fails its own unit tests instead of surfacing in mixed-version soak testing. Verified locally: `cargo test --lib go_math_rand` passes, `cargo fmt --check` clean.
This was referenced Jun 11, 2026
Merged
mswilkison
added a commit
that referenced
this pull request
Jun 11, 2026
…ffle corpus (#4035) Stacked on #4005 (base: `extraction/frost-signer-mirror-2026-05-26`). Rust half of the corpus-based differential parity item from the review; pairs with the Go-side PR #4034. Adds `testdata/coordinator_shuffle_corpus.json` — a byte-identical copy of the canonical 600-case corpus generated from keep-core's Go `SelectCoordinator` — and `select_coordinator_matches_cross_language_differential_corpus`, which replays every case through the `go_math_rand` port: 216 integer-boundary cases (seeds 0/±1/`i64::MIN`/`i64::MAX`/the #4026 pin seed; wrapping `seed + attempt` composition up to `u32::MAX`; unsorted and reversed member inputs pinning the internal sort) plus 384 generated sweeps over set sizes 1..255 with full-range seeds. All 600 cases replay identically today — direct evidence the `math/rand` port is bit-exact across the boundary regions where ports diverge first. Any future drift in source seeding, Fisher-Yates order, `int31n` bounds, sign handling, wrapping, or sorting fails this suite on the drifting side. Full signer suite passes (245 tests); clippy/rustfmt clean. Mirror note: port back to the tBTC monorepo signer with the next extraction sync. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mswilkison
added a commit
that referenced
this pull request
Jun 11, 2026
…+ 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)
mswilkison
added a commit
that referenced
this pull request
Jun 11, 2026
…x A) + cross-language conformance vectors (#4030) 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): | | seed | attempt numbering | |---|---|---| | Go RFC-21 layer | `fold(SHA256(KeyGroup ‖ SessionID ‖ MessageDigest))` | 0-based | | Rust engine validation | `int64_be(MessageDigest[0..8])` (legacy `signingAttemptSeed` convention) | 1-based wire | 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) 1. **RFC-21 Annex A (normative)** — single normative definition of the derivation: inputs (including the exact `KeyGroupBytes` definition for `FrostTBTCSignerV1` material — the UTF-8 bytes of the hex key-group handle, treated opaquely), the 0-based composition with the two's-complement-wrapping addition, the `wire = AttemptNumber + 1` FFI 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 live `pkg/tbtc` signing loop's legacy convention is explicitly documented as the thing Phase 7 migrates *from*. 2. **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 via `ROAST_SEED_VECTORS_REGEN=1 go test -run TestRegenerateCoordinatorSeedVectors` — generation-from-spec rather than hand-pinning, per the review. 3. **Conformance test** — `TestCoordinatorSeedDerivation_ConformanceVectors` pins `DeriveAttemptSeed → foldAttemptSeed → SelectCoordinator` end 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](https://claude.com/claude-code)
mswilkison
added a commit
that referenced
this pull request
Jun 11, 2026
…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)
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.
Stacked on #3866 (base:
feat/frost-schnorr-migration-scaffold).What
Adds
TestSelectCoordinator_CrossLanguagePinnedVectorstopkg/frost/roast/coordinator_test.go, pinning concreteSelectCoordinatoroutputs for fixed(members, seed, attempt)tuples.Why
The Rust signer (PR #4005) ports Go's
math/randshuffle semantics inpkg/tbtc/signer/src/go_math_rand.rsand pins exact expected coordinators inselect_coordinator_matches_known_keep_core_vectors(seed6879463052285329321). The Go suite, however, only asserted properties (determinism, input-order independence, seed/attempt sensitivity) — never concrete outputs.That asymmetry means a Go-side semantic change (e.g. migrating to
math/rand/v2, changing the shuffle, or altering theattemptSeed + attemptNumbercomposition) would pass the entire Go suite while silently breaking coordinator agreement with the Rust engine — a network-fracturing liveness failure that would only surface in mixed-version soak testing.This PR pins the exact same vectors as the Rust test (verified locally that current Go code produces them), plus pins the previously value-free
(seed=333, attempt=4)case to its concrete result. Either side drifting now fails its own unit suite.Review note (no code change)
While verifying parity I noticed the two layers derive the legacy
int64shuffle seed differently today:foldAttemptSeed(SHA256(DkgGroupPublicKey || SessionID || MessageDigest))(first 8 bytes, BE), 0-basedAttemptNumber.roast_attempt_seed_from_message_digest_hex): first 8 bytes of the raw message digest, with a 1-basedattempt_number.Not a live bug — keep-core does not yet send
attempt_contextover the FFI, and Rust strict mode is opt-in — but when a later phase wires RFC-21 attempt contexts into the Rust engine'svalidate_attempt_context, the two expected-coordinator computations will disagree unless one side is aligned first. Flagging so it lands on the integration checklist rather than in a testnet incident.