Skip to content

unify(tbtc/signer): adopt RFC-21 Annex A coordinator-seed derivation + cross-language conformance vectors#4031

Merged
mswilkison merged 2 commits into
extraction/frost-signer-mirror-2026-05-26from
unify/roast-coordinator-seed-rust-2026-06-11
Jun 11, 2026
Merged

unify(tbtc/signer): adopt RFC-21 Annex A coordinator-seed derivation + cross-language conformance vectors#4031
mswilkison merged 2 commits into
extraction/frost-signer-mirror-2026-05-26from
unify/roast-coordinator-seed-rust-2026-06-11

Conversation

@mswilkison

Copy link
Copy Markdown
Contributor

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 spec(frost/roast): normative coordinator-seed derivation (RFC-21 Annex A) + cross-language conformance vectors #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

…+ cross-language vectors

The engine's attempt-context validation derived the coordinator-shuffle
seed from the first 8 bytes of the raw message digest 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 (divergence flagged in #4026). At Phase-7 wiring every
Go-derived attempt context would have failed strict-mode validation.

Adopt the RFC-21 Annex A derivation as normative:

- roast_attempt_shuffle_seed(key_group, session_id, message_digest)
  replaces roast_attempt_seed_from_message_digest_hex; the key-group
  handle's UTF-8 bytes feed the hash as an opaque string, exactly
  matching keep-core's attempt.DeriveAttemptSeed + foldAttemptSeed.
- validate_attempt_context takes the session's key group and composes
  the shuffle source with the 0-based attempt number (wire encoding
  stays 1-based; the engine subtracts one), so both layers select the
  same coordinator for the same logical attempt.
- testdata/coordinator_seed_vectors.json is a byte-identical copy of
  the canonical vector file generated from the Go implementation;
  coordinator_seed_derivation_matches_cross_language_vectors pins the
  seed, the coordinator, the wire mapping, and full strict-mode
  validate_attempt_context acceptance for all ten vectors (including
  negative folded seeds and the n=100 production set).
- docs/roast-coordinator-seed-derivation.md mirrors the normative
  annex for signer-side readers.

The coordinator-mismatch test now derives the provably-wrong
coordinator instead of hardcoding one, so it stays valid under any
seed derivation.

Pairs with the Go-side PR on feat/frost-schnorr-migration-scaffold
(RFC-21 Annex A + canonical vector file + Go conformance test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f1f323f-a259-4f7e-b8dc-75ed00195056

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch unify/roast-coordinator-seed-rust-2026-06-11

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…ssage, not the transcript digest

Review finding on the seed unification: validate_attempt_context fed
roast_attempt_shuffle_seed with the engine's internal transcript
digest (hash_hex(message_bytes) = SHA256(message)), but the Go RFC-21
layer derives the seed from messageDigestFromBigInt(request.Message)
-- the raw 32-byte signing digest itself. Valid Phase-7 Go attempt
contexts would have selected a different coordinator and been
rejected in strict mode; the conformance test missed it because its
end-to-end leg called validate_attempt_context directly, bypassing
start_sign_round's digest computation.

- rfc21_message_digest mirrors messageDigestFromBigInt exactly:
  leading zero bytes insignificant (big.Int round-trip), big-endian
  left-pad to 32 bytes, more than 32 significant bytes rejected.
- validate_attempt_context now takes the raw message bytes and seeds
  the shuffle from the padded message; the SHA256 transcript digest
  keeps feeding only the attempt_id check. StartSignRound passes its
  request message; FinalizeSignRound passes the cached
  sign_message_bytes stored by the same StartSignRound.
- start_sign_round_accepts_go_derived_attempt_context_in_strict_mode
  reimplements the Go-side derivation inline (DeriveAttemptSeed +
  foldAttemptSeed + 0-based SelectCoordinator) and proves acceptance
  through the real strict-mode StartSignRound call path -- the test
  shape that would have caught the original finding.
- Cross-language conformance test now treats the vector digest as the
  raw message (the exact production relationship) and binds attempt_id
  to the transcript digest, exercising the two-digest split.
- Attempt-context proptests constrained to 32-byte messages, matching
  the RFC-21 bound the Go bridge enforces.

Annex A on the Go branch is amended in lockstep to state the input
unambiguously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mswilkison

mswilkison commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Codex review finding confirmed and fixed in 710e59a: the original change unified the seed derivation function but the engine call site fed it the wrong inputhash_hex(message_bytes) (the engine's SHA256 transcript digest) instead of the padded raw message that Go's messageDigestFromBigInt produces. Since the Go transitional executor passes the 32-byte signing digest as the FFI message, the engine was effectively seeding from SHA256(digest) while the Go RFC-21 layer seeds from digest — strict-mode Phase-7 contexts would still have selected divergent coordinators.

Why the conformance vectors didn't catch it: the end-to-end leg called validate_attempt_context directly with the vector digest, bypassing start_sign_round's digest computation. Fixes:

  • rfc21_message_digest mirrors messageDigestFromBigInt exactly (leading-zero insignificance via the big.Int round-trip, big-endian left-pad to 32, >32 significant bytes rejected) and now feeds the shuffle seed; the SHA256 transcript digest feeds only attempt_id/round_id, as before.
  • start_sign_round_accepts_go_derived_attempt_context_in_strict_mode reimplements the Go derivation inline (independent of the engine helpers) and proves acceptance through the real strict-mode StartSignRound call path — the test shape that would have caught this.
  • The conformance test now treats the vector digest as the raw message (the exact production relationship) and exercises the two-digest split end to end.
  • Attempt-context proptests constrained to 32-byte messages, matching the bound the Go bridge enforces.
  • Annex A on spec(frost/roast): normative coordinator-seed derivation (RFC-21 Annex A) + cross-language conformance vectors #4030 amended in lockstep to state the MessageDigest input unambiguously (padded raw message, never a transcript hash).

Full suite: 246 passed, clippy/rustfmt clean.

@mswilkison mswilkison merged commit 15a7a3e into extraction/frost-signer-mirror-2026-05-26 Jun 11, 2026
19 checks passed
@mswilkison mswilkison deleted the unify/roast-coordinator-seed-rust-2026-06-11 branch June 11, 2026 23:09
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