unify(tbtc/signer): adopt RFC-21 Annex A coordinator-seed derivation + cross-language conformance vectors#4031
Conversation
…+ 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>
|
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 |
…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>
|
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 input — Why the conformance vectors didn't catch it: the end-to-end leg called
Full suite: 246 passed, clippy/rustfmt clean. |
15a7a3e
into
extraction/frost-signer-mirror-2026-05-26
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 legacysigningAttemptSeedconvention), while the Go RFC-21 layer derivesfold(SHA256(KeyGroup ‖ SessionID ‖ MessageDigest))with 0-based attempt numbers. At Phase-7 wiring, every Go-derived attempt context would fail the engine's strict-modevalidate_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'sattempt.DeriveAttemptSeed+foldAttemptSeedcomposition, including the strict 32-byte digest requirement.validate_attempt_contextnow takes the session's key group (threaded fromdkg.key_groupat StartSignRound and the session'sDkgResultat FinalizeSignRound) and composes the shuffle source with the 0-based RFC-21 attempt number. The FFI wire encoding stays 1-based (attempt_number >= 1still 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_vectorspins, 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-modevalidate_attempt_contextacceptance of a context built from the wire encoding. Either language drifting now fails its own unit suite.docs/roast-coordinator-seed-derivation.mdmirrors the normative annex for signer-side readers, with the regen/copy procedure.Notes
roast-attempt-context-v1.json) is unaffected: it pins fingerprint/attempt-id domains with the coordinator as an input.Tests
Full suite: 245 passed, 0 failed; clippy and rustfmt clean. New conformance test exercises all ten cross-language vectors.
🤖 Generated with Claude Code