feat(tbtc/signer): mirror FROST/ROAST Rust signer from tBTC monorepo#4005
feat(tbtc/signer): mirror FROST/ROAST Rust signer from tBTC monorepo#4005mswilkison wants to merge 61 commits into
Conversation
Lands the Rust signer at pkg/tbtc/signer/ alongside the existing Go DKG coordinator. Mirrors the signer slice of tlabs-xyz/tbtc:feat/frost-schnorr-migration (PR #10) at frozen tag frost-extraction-source-v1. Per extraction plan v38 §3.1, the signer co-locates with keep-core because: (a) HSM enforcement is external to signer code (standard PKCS#11/KMIP client), doesn't force a separate audit lifecycle; (b) coordinator coupling dominates (B-2 DKG coordinator already lives in this repo via PR #3866); (c) TEE adoption later is reversible without forcing a repo split. Layout - pkg/tbtc/signer/ — Rust crate with own Cargo.toml - pkg/tbtc/signer/docs/ — signer + ROAST + TEE specs - pkg/tbtc/signer/docs/formal/models/ — ROAST + TEE TLA+ models - pkg/tbtc/signer/scripts/formal/ — ROAST vector + TLA runner - pkg/tbtc/signer/test/vectors/ — roast-attempt-context-v1.json Files (49 total) - 47 mirror status (Rust source, signer docs, ROAST docs, TLA models, test vector, etc.) - 2 allowlisted-divergence: - pkg/tbtc/signer/scripts/formal/check_roast_attempt_context_vectors.mjs (path normalization to signer-repo paths) - pkg/tbtc/signer/scripts/formal/run_tla_models.sh (MODELS_PATH env var refactor; default pkg/tbtc/signer/docs/formal/models/) Provenance - Source repository: tlabs-xyz/tbtc - Source branch: feat/frost-schnorr-migration - Source tag (frozen): frost-extraction-source-v1 - Source commit (H): 52389bd5cccb5daeef195671feb7ca46be6e2f37 - Source manifest: extraction/frost-extraction-source-manifest.json (manifestSha256: f7295fb738104501eb6c0c2447a42122ceb5f684c7a7c5dfb50ecb0bde3a0ea0) - Source PR(s): #425 (tbtc-signer error codes) + the full FROST migration series; complete list per source manifest's commit range over tools/tbtc-signer/ and signer-adjacent docs/scripts. Build wiring (follow-up) This PR lands the signer source code, docs, vectors, and scripts. Rust toolchain CI integration (cargo build/test/clippy/fmt as a separate CI job alongside the existing Go jobs) is a follow-up — Go workspace ignores non-Go subdirectories automatically; the cargo crate at pkg/tbtc/signer/Cargo.toml is independently buildable. Known TBD (resolved pre-merge) - 2 allowlisted-divergence files have expectedTargetSha256 = <TBD> in the source manifest. Path normalization transformations need to be applied to make the scripts run in pkg/tbtc/signer/ context; this PR ships the source verbatim, follow-up commits on this branch apply the transformations before merge. - Dual signoff required on each allowlisted-divergence entry per plan v38 §4.2 (extraction lead + canonical repo maintainer). Verification (pre-merge per plan v38 §7.2) - 47 mirror files: sha256 equality between git show frost-extraction-source-v1:<sourcePath> and this PR's content at pkg/tbtc/signer/<targetPath>. - 2 allowlisted-divergence files: sha256 == expectedTargetSha256 (recorded post-transformation in source manifest). - PR-scoped rogue-file check: git diff --name-only main...HEAD only contains files in manifest's fileMap with targetKey "signer". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR introduces ChangesCore Signer Implementation
Admission Checking & Policy Enforcement
Benchmarking & Formal Test Vectors
TLA+ Formal Verification Models
Design Documentation & Specifications
Build Configuration & Scripts
🎯 4 (Complex) | ⏱️ ~75 minutes
✨ Finishing Touches🧪 Generate unit tests (beta)
|
…ripts
Per extraction plan v38 §4.4, allowlisted-divergence files require
content normalization for the canonical context. This commit applies
the transformations declared in the source manifest for the 2 signer-
side allowlisted-divergence entries.
Transformations
- pkg/tbtc/signer/scripts/formal/check_roast_attempt_context_vectors.mjs:
Rewrite vector path from
`docs/frost-migration/test-vectors/roast-attempt-context-v1.json`
to canonical signer layout
`test/vectors/roast-attempt-context-v1.json`
(both relative to rootDir which is two levels up from the script
location; in canonical context that's pkg/tbtc/signer/)
- pkg/tbtc/signer/scripts/formal/run_tla_models.sh:
Rewrite MODEL_DIR default from
`$ROOT_DIR/docs/frost-migration/formal-verification/models`
to canonical signer layout
`$ROOT_DIR/docs/formal/models`
Plus MODELS_PATH env-var override for alternate environments (CI
matrices, local dev trees). ROOT_DIR is unchanged
(`$(dirname $BASH_SOURCE)/../..` resolves to pkg/tbtc/signer/ here).
Verification
- Both files retain identical behavior to their monorepo counterparts
when invoked from canonical signer layout
- Comments added documenting the path normalization with reference back
to the source manifest's allowlisted-divergence status
Recompute expectedTargetSha256 for both entries in the source manifest
and collect dual signoff before merge per plan v38 §4.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (3)
pkg/tbtc/signer/src/api.rs (1)
196-202: 💤 Low valueMissing
#[serde(default, skip_serializing_if)]onscript_tree_hex.All other
Option<T>fields in this file use#[serde(default, skip_serializing_if = "Option::is_none")], butscript_tree_hexdoes not. This inconsistency means the field will serialize asnullwhen absent, rather than being omitted.Suggested fix
pub struct BuildTaprootTxRequest { pub session_id: String, pub inputs: Vec<TxInput>, pub outputs: Vec<TxOutput>, + #[serde(default, skip_serializing_if = "Option::is_none")] pub script_tree_hex: Option<String>, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/tbtc/signer/src/api.rs` around lines 196 - 202, The BuildTaprootTxRequest struct's script_tree_hex Option field lacks the serde attributes used elsewhere; update the declaration of BuildTaprootTxRequest so the script_tree_hex field is annotated with #[serde(default, skip_serializing_if = "Option::is_none")] to match other Option<T> fields (preserving Clone/Debug/Deserialize/Serialize behavior) so it is omitted from serialized output when None instead of serializing as null.pkg/tbtc/signer/src/lib.rs (1)
391-421: 💤 Low value
std::env::set_varandstd::env::remove_varare not thread-safe.These functions are unsound in multi-threaded contexts and deprecated since Rust 1.66. While the tests appear to serialize access via
lock_test_state(), this guard must be held across all env mutations and checks within a test to prevent races with parallel test threads.Current usage appears safe given the locking pattern, but this is fragile. Consider using a dedicated test configuration mechanism that doesn't rely on process-wide environment mutation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/tbtc/signer/src/lib.rs` around lines 391 - 421, EnvVarGuard's methods (EnvVarGuard::set, EnvVarGuard::unset) and its Drop rely on std::env::set_var/remove_var which are process-wide and not thread-safe; replace this pattern with a test-scoped, non-global solution such as using a crate that provides scoped environment variables (e.g., temp_env or similar) or refactor tests to accept an injected configuration object instead of mutating process env; update usages to acquire and hold the new scoped guard for the entire duration of tests that need env changes (or pass a Config struct into functions under test) and remove direct calls to std::env::set_var/remove_var and the EnvVarGuard Drop behavior to avoid races.pkg/tbtc/signer/src/bin/admission_checker.rs (1)
257-276: 💤 Low valueConsider cleaning up the temp file if rename fails.
If
fs::renamefails (e.g., cross-filesystem move or permissions issue), the temp file remains on disk. Adding a cleanup attempt in the error path would improve robustness.♻️ Proposed cleanup on error
fs::write(&tmp_path, serialized).map_err(|error| { format!( "failed to write override replay registry temp file [{}]: {error}", tmp_path.display() ) })?; - fs::rename(&tmp_path, path).map_err(|error| { - format!( - "failed to persist override replay registry [{}]: {error}", - path.display() - ) - }) + fs::rename(&tmp_path, path).map_err(|error| { + let _ = fs::remove_file(&tmp_path); // Best-effort cleanup + format!( + "failed to persist override replay registry [{}]: {error}", + path.display() + ) + }) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/tbtc/signer/src/bin/admission_checker.rs` around lines 257 - 276, persist_override_replay_registry currently leaves the temporary file (tmp_path) if fs::rename fails; modify the rename error path to attempt cleanup of tmp_path before returning the error. Specifically, call fs::remove_file(&tmp_path) (ignoring or logging its result) inside the Err branch that handles the rename failure so the function still returns the original formatted error for fs::rename but also tries to remove the leftover tmp file; reference persist_override_replay_registry, path, tmp_path, and fs::rename when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/tbtc/signer/docs/formal/models/README.md`:
- Around line 32-51: Update the incorrect repository paths in the traceability
matrix of pkg/tbtc/signer/docs/formal/models/README.md so links point to the
actual implementation and docs in this repo: change references to
tools/tbtc-signer/src/engine.rs to pkg/tbtc/signer/src/engine.rs for the entries
mentioning RoastAttemptStateMachine.tla (validate_attempt_context, replay
guards) and StateKeyProviderPolicy.tla (decode_encrypted_state_envelope,
encode_encrypted_state_envelope); change
docs/frost-migration/tee-whitelisted-signer-enforcement-plan.md to
pkg/tbtc/signer/docs/tee-whitelisted-signer-enforcement-plan.md for
TeeEnforcementModes.tla; and change
docs/frost-migration/roast-phase-5-security-rollout-gates.md to
pkg/tbtc/signer/docs/roast-phase-5-security-rollout-gates.md for
RoastRolloutPolicy.tla so readers can locate the referenced code and policy
docs.
In `@pkg/tbtc/signer/docs/roast-implementation-plan.md`:
- Line 93: Update the broken doc links in
pkg/tbtc/signer/docs/roast-implementation-plan.md by replacing repo-external
paths like `docs/frost-migration/roast-phase-0-spec-freeze.md` and any
`tools/tbtc-signer/...` references with the correct repo-local paths under
pkg/tbtc/signer/docs (or use correct relative paths from this markdown file);
search the file for all occurrences of `docs/frost-migration/...` and
`tools/tbtc-signer/...` (including the instances similar to the shown
`docs/frost-migration/roast-phase-0-spec-freeze.md`) and normalize each link so
it points to the new location in this package, preserving anchor fragments and
updating any link text as needed.
In `@pkg/tbtc/signer/docs/roast-phase-5-rollout-runbook.md`:
- Around line 26-29: Update the incorrect crate path used in the runbook
commands: replace occurrences of "cd tools/tbtc-signer" with "cd
pkg/tbtc/signer" for the benchmark command (`cargo bench --features
bench-restart-hook --bench phase5_roast`) and the chaos suite script invocation
(`./scripts/run_phase5_chaos_suite.sh`) so the commands run from the correct
crate directory.
In `@pkg/tbtc/signer/docs/roast-phase-5-security-rollout-gates.md`:
- Around line 92-99: Update stale repository paths in
roast-phase-5-security-rollout-gates.md: replace any occurrences of the old
tooling path "tools/tbtc-signer" with the new location "pkg/tbtc/signer" (e.g.,
update the run command `cd tools/tbtc-signer && cargo bench --features
bench-restart-hook --bench phase5_roast` to `cd pkg/tbtc/signer ...`), and
update any links referencing
`docs/frost-migration/roast-phase-5-baseline-calibration.md` to the document’s
new location in the repo (search for the exact link text and swap to the correct
path). Ensure all instances at the reported locations (around the run command
and the listed links) are changed consistently so operators following the
runbook hit the correct files and commands.
In `@pkg/tbtc/signer/docs/rust-rewrite-bootstrap.md`:
- Around line 10-13: The documentation still references the old crate path
"tools/tbtc-signer" and the validation command using that path; update every
occurrence to "pkg/tbtc/signer" in rust-rewrite-bootstrap.md (including the
header lines that list the crate, the C ABI include path `include/frost_tbtc.h`,
and any validation/build commands) so links and commands point to the colocated
pkg/tbtc/signer location; search for "tools/tbtc-signer" and replace with
"pkg/tbtc/signer" and verify the validation command and any examples reference
the new path.
In `@pkg/tbtc/signer/docs/signer-api-contract-decision-brief.md`:
- Around line 43-47: Update the two referenced paths in
signer-api-contract-decision-brief.md so they point to the mirrored crate
locations: replace `docs/frost-migration/rust-rewrite-bootstrap.md` with
`pkg/tbtc/signer/docs/frost-migration/rust-rewrite-bootstrap.md` and replace
`tools/tbtc-signer/src/lib.rs` with `pkg/tbtc/signer/src/lib.rs` (look for the
occurrences shown around the paragraph mentioning the bootstrap Rust crate and
file: `tools/tbtc-signer/src/lib.rs` and update those strings accordingly).
In `@pkg/tbtc/signer/docs/tbtc-signer-secret-material-hardening-plan.md`:
- Around line 6-7: The doc still uses the old crate path string
`tools/tbtc-signer`; update that scope reference to `pkg/tbtc/signer` throughout
the file (tbtc-signer-secret-material-hardening-plan.md) so the plan points to
the mirrored crate location, and scan for any other occurrences of
`tools/tbtc-signer` in this document to replace with `pkg/tbtc/signer`.
In `@pkg/tbtc/signer/docs/tee-whitelisted-signer-enforcement-plan.md`:
- Around line 296-298: Update the two broken cross-doc links in
pkg/tbtc/signer/docs/tee-whitelisted-signer-enforcement-plan.md (currently
referencing docs/frost-migration/roast-phase-5-security-rollout-gates.md and
docs/frost-migration/roast-phase-5-rollout-runbook.md on lines ~296–297) to
point to their correct locations inside this PR:
pkg/tbtc/signer/docs/roast-phase-5-security-rollout-gates.md and
pkg/tbtc/signer/docs/roast-phase-5-rollout-runbook.md so cross-document
navigation resolves correctly.
In `@pkg/tbtc/signer/README.md`:
- Around line 53-54: Update the README.md occurrence(s) that reference the old
path string "tools/tbtc-signer" to the new crate location "pkg/tbtc/signer":
search for and replace that path in all command snippets, code blocks, and file
references (e.g., cargo build/cd commands and any path bullets) so every
instance uses "pkg/tbtc/signer" consistently; ensure both shell commands and
prose file paths are updated, and run a quick grep for "tools/tbtc-signer" to
confirm no remaining references.
In `@pkg/tbtc/signer/scripts/admission-policy-v1.sample.json`:
- Line 8: Replace the non-hex placeholder value for the JSON key
dao_override_trust_root_pubkey_hex with a syntactically valid 32-byte hex string
(64 lowercase hex characters) so sample files and copy/paste validation don't
break; update the sample value to something like a 64-character hex placeholder
and leave replacement guidance in the README or nearby comment explaining it
must be replaced with the real x-only pubkey hex.
In `@pkg/tbtc/signer/scripts/formal/check_roast_attempt_context_vectors.mjs`:
- Around line 15-18: vectorsPath currently resolves to
"docs/frost-migration/test-vectors/roast-attempt-context-v1.json" and will fail
because the vectors live under "test/vectors"; update the path.join call that
constructs vectorsPath (using rootDir and the filename) to point to
"test/vectors/roast-attempt-context-v1.json" so the script loads the correct
file; ensure the change is made where vectorsPath is declared and used in this
module.
In `@pkg/tbtc/signer/scripts/formal/run_tla_models.sh`:
- Around line 4-5: The MODEL_DIR assignment in run_tla_models.sh is pointing to
the wrong path; update the MODEL_DIR variable (currently computed relative to
ROOT_DIR) to "$ROOT_DIR/docs/formal/models" so the directory existence check in
the script (around the directory existence test near line 20) succeeds; modify
the MODEL_DIR definition in the script (look for the MODEL_DIR variable
assignment and references) to use the corrected path and ensure any subsequent
uses of MODEL_DIR still reference this updated variable.
In `@pkg/tbtc/signer/tests/p2tr_signature_fraud_vectors.rs`:
- Around line 375-376: The test constructs vectors_path using the vectors_path
variable in pkg/tbtc/signer/tests/p2tr_signature_fraud_vectors.rs which
currently joins
"../../docs/frost-migration/test-vectors/p2tr-signature-fraud-v0.json" relative
to CARGO_MANIFEST_DIR and points to a non-existent file; fix by either adding
the missing p2tr-signature-fraud-v0.json to the repo at that path or update the
vectors_path join to the correct relative path where the JSON actually lives
(adjust the "../" segments or point to the canonical test-vectors location),
ensuring the variable name vectors_path and its usage remain unchanged.
---
Nitpick comments:
In `@pkg/tbtc/signer/src/api.rs`:
- Around line 196-202: The BuildTaprootTxRequest struct's script_tree_hex Option
field lacks the serde attributes used elsewhere; update the declaration of
BuildTaprootTxRequest so the script_tree_hex field is annotated with
#[serde(default, skip_serializing_if = "Option::is_none")] to match other
Option<T> fields (preserving Clone/Debug/Deserialize/Serialize behavior) so it
is omitted from serialized output when None instead of serializing as null.
In `@pkg/tbtc/signer/src/bin/admission_checker.rs`:
- Around line 257-276: persist_override_replay_registry currently leaves the
temporary file (tmp_path) if fs::rename fails; modify the rename error path to
attempt cleanup of tmp_path before returning the error. Specifically, call
fs::remove_file(&tmp_path) (ignoring or logging its result) inside the Err
branch that handles the rename failure so the function still returns the
original formatted error for fs::rename but also tries to remove the leftover
tmp file; reference persist_override_replay_registry, path, tmp_path, and
fs::rename when making the change.
In `@pkg/tbtc/signer/src/lib.rs`:
- Around line 391-421: EnvVarGuard's methods (EnvVarGuard::set,
EnvVarGuard::unset) and its Drop rely on std::env::set_var/remove_var which are
process-wide and not thread-safe; replace this pattern with a test-scoped,
non-global solution such as using a crate that provides scoped environment
variables (e.g., temp_env or similar) or refactor tests to accept an injected
configuration object instead of mutating process env; update usages to acquire
and hold the new scoped guard for the entire duration of tests that need env
changes (or pass a Config struct into functions under test) and remove direct
calls to std::env::set_var/remove_var and the EnvVarGuard Drop behavior to avoid
races.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c0224eef-499a-42a2-a346-f06ef353278b
⛔ Files ignored due to path filters (1)
pkg/tbtc/signer/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (48)
pkg/tbtc/signer/.gitignorepkg/tbtc/signer/Cargo.tomlpkg/tbtc/signer/README.mdpkg/tbtc/signer/benches/phase5_roast.rspkg/tbtc/signer/build.shpkg/tbtc/signer/docs/formal/models/README.mdpkg/tbtc/signer/docs/formal/models/RoastAttemptStateMachine.cfgpkg/tbtc/signer/docs/formal/models/RoastAttemptStateMachine.tlapkg/tbtc/signer/docs/formal/models/RoastRolloutPolicy.cfgpkg/tbtc/signer/docs/formal/models/RoastRolloutPolicy.tlapkg/tbtc/signer/docs/formal/models/StateKeyProviderPolicy.cfgpkg/tbtc/signer/docs/formal/models/StateKeyProviderPolicy.production.cfgpkg/tbtc/signer/docs/formal/models/StateKeyProviderPolicy.tlapkg/tbtc/signer/docs/formal/models/TeeEnforcementModes.cfgpkg/tbtc/signer/docs/formal/models/TeeEnforcementModes.tlapkg/tbtc/signer/docs/permissioned-signer-hardening-rfc.mdpkg/tbtc/signer/docs/roast-implementation-plan.mdpkg/tbtc/signer/docs/roast-phase-0-spec-freeze.mdpkg/tbtc/signer/docs/roast-phase-1.5-consumed-registry-integration.mdpkg/tbtc/signer/docs/roast-phase-2-coordinator-policy-enforcement.mdpkg/tbtc/signer/docs/roast-phase-3-attempt-transcript-replay-hardening.mdpkg/tbtc/signer/docs/roast-phase-4-liveness-policy-recovery.mdpkg/tbtc/signer/docs/roast-phase-5-baseline-calibration.mdpkg/tbtc/signer/docs/roast-phase-5-rollout-runbook.mdpkg/tbtc/signer/docs/roast-phase-5-security-rollout-gates.mdpkg/tbtc/signer/docs/rust-rewrite-bootstrap.mdpkg/tbtc/signer/docs/signer-api-contract-decision-brief.mdpkg/tbtc/signer/docs/tbtc-signer-secret-material-hardening-plan.mdpkg/tbtc/signer/docs/tee-whitelisted-signer-enforcement-plan.mdpkg/tbtc/signer/docs/true-late-t-of-n-finalize-considerations.mdpkg/tbtc/signer/include/frost_tbtc.hpkg/tbtc/signer/scripts/admission-candidate.sample.jsonpkg/tbtc/signer/scripts/admission-existing.sample.jsonpkg/tbtc/signer/scripts/admission-override-registry.sample.jsonpkg/tbtc/signer/scripts/admission-override.sample.jsonpkg/tbtc/signer/scripts/admission-policy-v1.sample.jsonpkg/tbtc/signer/scripts/formal/check_roast_attempt_context_vectors.mjspkg/tbtc/signer/scripts/formal/run_tla_models.shpkg/tbtc/signer/scripts/run_phase5_chaos_suite.shpkg/tbtc/signer/src/api.rspkg/tbtc/signer/src/bin/admission_checker.rspkg/tbtc/signer/src/engine.rspkg/tbtc/signer/src/errors.rspkg/tbtc/signer/src/ffi.rspkg/tbtc/signer/src/go_math_rand.rspkg/tbtc/signer/src/lib.rspkg/tbtc/signer/test/vectors/roast-attempt-context-v1.jsonpkg/tbtc/signer/tests/p2tr_signature_fraud_vectors.rs
| - Added `tools/tbtc-signer` Rust crate that builds a `cdylib` named | ||
| `libfrost_tbtc`. | ||
| - Added a C ABI contract in `tools/tbtc-signer/include/frost_tbtc.h`. | ||
| - Implemented coarse request/response operations keyed by `session_id`: |
There was a problem hiding this comment.
Replace tools/tbtc-signer path references with pkg/tbtc/signer.
This doc is now colocated in pkg/tbtc/signer/docs, but key references and the validation command still use tools/tbtc-signer. That will cause immediate command/link failures for anyone following this bootstrap guide.
Suggested doc patch
-- Added `tools/tbtc-signer` Rust crate that builds a `cdylib` named
+- Added `pkg/tbtc/signer` Rust crate that builds a `cdylib` named
-- Added a C ABI contract in `tools/tbtc-signer/include/frost_tbtc.h`.
+- Added a C ABI contract in `pkg/tbtc/signer/include/frost_tbtc.h`.
-- Added post-finalize signing-material cleanup in `tools/tbtc-signer` session
+- Added post-finalize signing-material cleanup in `pkg/tbtc/signer` session
-- Added finalized-session guardrails in `tools/tbtc-signer`: subsequent
+- Added finalized-session guardrails in `pkg/tbtc/signer`: subsequent
- cd tools/tbtc-signer
+ cd pkg/tbtc/signerAlso applies to: 138-143, 264-266
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pkg/tbtc/signer/docs/rust-rewrite-bootstrap.md` around lines 10 - 13, The
documentation still references the old crate path "tools/tbtc-signer" and the
validation command using that path; update every occurrence to "pkg/tbtc/signer"
in rust-rewrite-bootstrap.md (including the header lines that list the crate,
the C ABI include path `include/frost_tbtc.h`, and any validation/build
commands) so links and commands point to the colocated pkg/tbtc/signer location;
search for "tools/tbtc-signer" and replace with "pkg/tbtc/signer" and verify the
validation command and any examples reference the new path.
| "required_attestation_status": "approved", | ||
| "min_patch_sla_days_remaining": 14, | ||
| "require_incident_response_contact": true, | ||
| "dao_override_trust_root_pubkey_hex": "REPLACE_WITH_XONLY_PUBKEY_HEX", |
There was a problem hiding this comment.
Use a parseable sample value for dao_override_trust_root_pubkey_hex.
"REPLACE_WITH_XONLY_PUBKEY_HEX" is not valid hex, so this sample can break copy/paste flows in docs or local validation. Prefer a syntactically valid 32-byte hex placeholder (and keep replacement guidance in README).
Suggested change
- "dao_override_trust_root_pubkey_hex": "REPLACE_WITH_XONLY_PUBKEY_HEX",
+ "dao_override_trust_root_pubkey_hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "dao_override_trust_root_pubkey_hex": "REPLACE_WITH_XONLY_PUBKEY_HEX", | |
| "dao_override_trust_root_pubkey_hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pkg/tbtc/signer/scripts/admission-policy-v1.sample.json` at line 8, Replace
the non-hex placeholder value for the JSON key
dao_override_trust_root_pubkey_hex with a syntactically valid 32-byte hex string
(64 lowercase hex characters) so sample files and copy/paste validation don't
break; update the sample value to something like a 64-character hex placeholder
and leave replacement guidance in the README or nearby comment explaining it
must be replaced with the real x-only pubkey hex.
Adds a focused workflow that runs the Rust signer's formal-invariant test suite + TLA model checks. Moved from threshold-network/tbtc-v2/.github/workflows/ci-formal-verification.yml (jobs `signer-formal-invariants` + `tla-model-checks`) per extraction plan v38 §3.1 — the signer code lives here at pkg/tbtc/signer/, not in tbtc-v2, so the CI jobs that exercise it belong here too. Jobs - signer-formal-invariants: cargo test --manifest-path pkg/tbtc/signer/ Cargo.toml formal_verification_ (filter to formal-only cases) - tla-model-checks: pkg/tbtc/signer/scripts/formal/run_tla_models.sh (iterates over .cfg files in pkg/tbtc/signer/docs/formal/models/ and runs TLC against each; MODELS_PATH env var allows override per the path-normalization commit b84b574c on this branch) Triggers - pull_request on pkg/tbtc/signer/** changes + this workflow file - schedule nightly at 05:23 UTC (mirrors monorepo's pattern of running formal invariants both on PRs and nightly) - workflow_dispatch for manual runs Related changes in companion PR threshold-network/tbtc-v2#971: - Removed these jobs from canonical tbtc-v2's ci-formal-verification.yml - Added a comment in that file pointing here Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/tbtc-signer-formal.yml:
- Around line 25-26: The checkout steps using actions/checkout@v4 persist git
credentials by default; update each Checkout step (the uses: actions/checkout@v4
entries) to add with: persist-credentials: false so credentials are not stored
in the runner after checkout. Ensure both occurrences of actions/checkout@v4 in
the workflow are modified accordingly.
- Line 26: Update the GitHub Actions workflow to pin action versions and harden
checkout credentials: replace occurrences of actions/checkout@v4 with
actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 and add with:
persist-credentials: false to both checkout steps that use checkout, replace
dtolnay/rust-toolchain@stable with
dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8, and replace
actions/setup-java@v4 with
actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 so the workflow pins
SHA-based commits and disables persisting credentials on checkout.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 88553e79-6db4-4e95-98fe-00c56e1bc640
📒 Files selected for processing (1)
.github/workflows/tbtc-signer-formal.yml
| - name: Checkout | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/tbtc-signer-formal.yml"
echo "== File exists? =="
ls -l "$FILE"
echo "== Lines 1-120 with numbers =="
nl -ba "$FILE" | sed -n '1,120p'Repository: threshold-network/keep-core
Length of output: 272
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/tbtc-signer-formal.yml"
echo "== File exists? =="
ls -l "$FILE"
echo "== Lines 1-120 with numbers =="
nl -ba "$FILE" | sed -n '1,120p'Repository: threshold-network/keep-core
Length of output: 272
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/tbtc-signer-formal.yml"
echo "== Showing lines 1-80 =="
awk 'NR>=1 && NR<=80 {printf "%4d: %s\n", NR, $0}' "$FILE"
echo "== Showing checkout-related lines (search) =="
rg -n "name:\s*Checkout|uses:\s*actions/checkout|persist-credentials" "$FILE" || trueRepository: threshold-network/keep-core
Length of output: 2302
Set persist-credentials: false on both actions/checkout@v4 steps
The current workflow checks out without disabling persisted git credentials; add with: persist-credentials: false to reduce credential exposure.
Suggested patch
- name: Checkout
uses: actions/checkout@v4
+ with:
+ persist-credentials: false
@@
- name: Checkout
uses: actions/checkout@v4
+ with:
+ persist-credentials: false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false |
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 25-26: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/tbtc-signer-formal.yml around lines 25 - 26, The checkout
steps using actions/checkout@v4 persist git credentials by default; update each
Checkout step (the uses: actions/checkout@v4 entries) to add with:
persist-credentials: false so credentials are not stored in the runner after
checkout. Ensure both occurrences of actions/checkout@v4 in the workflow are
modified accordingly.
| timeout-minutes: 30 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for repo in actions/checkout actions/setup-java dtolnay/rust-toolchain; do
echo "=== $repo ==="
git ls-remote "https://github.com/${repo}.git" | head -n 5
echo
doneRepository: threshold-network/keep-core
Length of output: 535
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect workflow around the referenced lines
echo "=== Workflow file context ==="
nl -ba .github/workflows/tbtc-signer-formal.yml | sed -n '1,120p'
# 2) Resolve refs to commit SHAs (avoid SIGPIPE by capturing then slicing)
resolve_ref() {
local repo="$1" ref="$2"
local url="https://github.com/${repo}.git"
local tmp="/tmp/lsremote_${repo//\//_}_${ref//\//_}"
git ls-remote "$url" "$ref" > "$tmp" 2>/dev/null || true
echo "--- ${repo} ${ref} ---"
cat "$tmp" || true
echo
rm -f "$tmp" || true
}
echo "=== Resolve action refs to SHAs ==="
resolve_ref "actions/checkout" "refs/tags/v4"
resolve_ref "actions/checkout" "refs/heads/v4"
resolve_ref "actions/setup-java" "refs/tags/v4"
resolve_ref "actions/setup-java" "refs/heads/v4"
resolve_ref "dtolnay/rust-toolchain" "refs/heads/stable"
resolve_ref "dtolnay/rust-toolchain" "refs/tags/stable"Repository: threshold-network/keep-core
Length of output: 162
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Workflow file context (numbered) ==="
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/tbtc-signer-formal.yml")
lines = p.read_text().splitlines()
start, end = 1, min(len(lines), 140)
for i in range(start, end+1):
print(f"{i}\t{lines[i-1]}")
PY
echo
echo "=== Resolve action refs to commit SHAs ==="
resolve_ref() {
local repo="$1" ref="$2"
local url="https://github.com/${repo}.git"
echo "--- ${repo} ${ref} ---"
git ls-remote "$url" "$ref" || true
echo
}
resolve_ref "actions/checkout" "refs/tags/v4"
resolve_ref "actions/checkout" "refs/heads/v4"
resolve_ref "actions/setup-java" "refs/tags/v4"
resolve_ref "actions/setup-java" "refs/heads/v4"
resolve_ref "dtolnay/rust-toolchain" "refs/heads/stable"
resolve_ref "dtolnay/rust-toolchain" "refs/tags/stable"Repository: threshold-network/keep-core
Length of output: 2438
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Total lines: $(python3 - <<'PY'\nimport pathlib\np=pathlib.Path('.github/workflows/tbtc-signer-formal.yml')\nprint(len(p.read_text().splitlines()))\nPY)"
echo
echo "=== uses: occurrences ==="
rg -n "uses:" .github/workflows/tbtc-signer-formal.yml || true
echo
echo "=== persist-credentials occurrences ==="
rg -n "persist-credentials" .github/workflows/tbtc-signer-formal.yml || trueRepository: threshold-network/keep-core
Length of output: 133
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Total lines: $(python3 - <<'PY'
import pathlib
p=pathlib.Path('.github/workflows/tbtc-signer-formal.yml')
print(len(p.read_text().splitlines()))
PY)"
echo
echo "=== uses: occurrences ==="
rg -n "uses:" .github/workflows/tbtc-signer-formal.yml || true
echo
echo "=== persist-credentials occurrences ==="
rg -n "persist-credentials" .github/workflows/tbtc-signer-formal.yml || trueRepository: threshold-network/keep-core
Length of output: 424
Pin GitHub Actions to commit SHAs and harden checkout credentials.
actions/checkout@v4(lines 26, 44) ->actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5; also addwith: persist-credentials: falseto both checkout steps.dtolnay/rust-toolchain@stable(line 29) ->dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8.actions/setup-java@v4(line 47) ->actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9.
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 25-26: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/tbtc-signer-formal.yml at line 26, Update the GitHub
Actions workflow to pin action versions and harden checkout credentials: replace
occurrences of actions/checkout@v4 with
actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 and add with:
persist-credentials: false to both checkout steps that use checkout, replace
dtolnay/rust-toolchain@stable with
dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8, and replace
actions/setup-java@v4 with
actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 so the workflow pins
SHA-based commits and disables persisting credentials on checkout.
Resolves CI failures on PR #4005 (signer mirror): 1. TLA model checks: run_tla_models.sh lacked executable bit at canonical HEAD. CI ran the script directly (no `bash` prefix), which fails with `Permission denied`. Fixed via `git update-index --chmod=+x`. 2. Signer formal invariants: engine.rs's formal_verification_roast_attempt_context_shared_vectors_match_ expected_values test referenced vectors at a path stale from the umbrella's docs/frost-migration/test-vectors/ layout. The manifest places the vector at the canonical-signer test/vectors/ subdir (pkg/tbtc/signer/test/vectors/roast-attempt-context-v1.json per the source-to-target map). Updated the `PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(...)` argument from `../../docs/frost-migration/test-vectors/roast-attempt-context-v1.json` (umbrella-relative) to `test/vectors/roast-attempt-context-v1.json` (signer-CARGO_MANIFEST_DIR-relative, where the vector actually lives at canonical HEAD). Verified locally: - ls -l shows executable bit set on run_tla_models.sh - engine.rs path now resolves to the correct mirror location - Vector exists at pkg/tbtc/signer/test/vectors/roast-attempt-context-v1.json Same fix needs to be applied to PR #4007 (stacked on #4005) in a follow-up commit on its branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #4005 signer formal invariants test formal_verification_p2tr_signature_fraud_vectors_match_bitcoin_crate was failing because: 1. The umbrella source manifest only mapped p2tr-signature-fraud-v0.json to the tbtc-v2 target (docs/test-vectors/). It was not mirrored to the keep-core (signer) target. 2. tests/p2tr_signature_fraud_vectors.rs referenced the vector at the umbrella-relative path `../../docs/frost-migration/test-vectors/p2tr-signature-fraud-v0.json` (CARGO_MANIFEST_DIR-relative -> repo-root + docs/frost-migration/...). That directory does not exist on canonical keep-core. This is a structural omission in the manifest, not a divergence: the cross-language vector test exists in both Solidity (tbtc-v2 side) and Rust (signer side), and the vector is genuinely needed in both places to verify cross-implementation consistency. Fix - Mirror p2tr-signature-fraud-v0.json (598 lines, byte-identical content from tbtc-v2 mirror at docs/test-vectors/) to pkg/tbtc/signer/test/vectors/p2tr-signature-fraud-v0.json. - Update the test path in tests/p2tr_signature_fraud_vectors.rs from `../../docs/frost-migration/test-vectors/p2tr-signature-fraud-v0.json` to `test/vectors/p2tr-signature-fraud-v0.json` (CARGO_MANIFEST_DIR = pkg/tbtc/signer/, so test/vectors/... resolves correctly). Two stale comment references remain in - pkg/tbtc/signer/docs/roast-implementation-plan.md:265 - pkg/tbtc/signer/scripts/formal/check_roast_attempt_context_vectors.mjs:19 Both are comment-only doc pointers to the source layout; they do not affect runtime. Left as-is to preserve the umbrella -> canonical provenance trail. Manifest update follows in a stacked PR on tlabs-xyz/tbtc#10: - Add p2tr-signature-fraud-v0.json -> signer target mapping (test/vectors/p2tr-signature-fraud-v0.json). - Reclassify tests/p2tr_signature_fraud_vectors.rs from mirror to allowlisted-divergence (path differs from umbrella). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stacked on extraction/frost-signer-mirror-2026-05-26 / PR #4005. Adds optional taproot_merkle_root_hex to start/finalize signing rounds, binds it into request fingerprints and round IDs, signs and aggregates with frost-secp256k1-tr Taproot tweaks, and verifies tweaked aggregates in tests. Verification: cargo test in pkg/tbtc/signer.
The widened round-nonce-v2 binding mixes encodings: the participants set serializes big-endian while participant_identifier keeps the v1 little-endian encoding. Harmless (fixed-width parts, length-framed by deterministic_seed) but part of the derived value -- note that any encoding change requires a new seed domain, never an in-place edit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes a completeness gap in the v2 RoundNonceBinding found in third-pass review (P1). The seed bound only the *group* verifying key, not the individual verifying shares. In the transitional flow every member re-derives ALL participants' round-1 commitments from the held key packages, so each other participant's verifying share enters the commitment list -> this member's binding factor and challenge. Two key packages can share a group verifying key while differing in a non-target share (any threshold t>=3 admits two polynomials with identical f(0) and target share but a different non-target share). Consequence under the old binding: a rolled-back/restored/cloned state (exactly #4028's threat model) could present an identical nonce seed under a *different* challenge -> the same member signs two different challenges with one deterministic nonce -> share extraction. The in-process run_dkg SessionConflict guard does not cover this, by design: nonce safety must not depend on registry integrity, since durable state can be rolled back or replicated. The production hard-gate still blocks this transitional flow in production, so the exposure is confined to the dealer-DKG dev/staging path; the interactive production path draws from OS randomness and is unaffected. Fix: bind the full serialized PublicKeyPackage (group key AND every verifying share); domain round-nonce-v2 -> round-nonce-v3. Regression: deterministic_round_nonce_and_commitment_binds_full_transcript now includes a variant with the baseline group key but a non-target verifying share swapped; it produces an identical seed (and asserts an identical group key) under the old binding, a different commitment under the new one. Full signer suite 246 pass; clippy/rustfmt clean. Mirror note: v3 domain + the widened binding port back to the tBTC monorepo signer with the next extraction sync, alongside the rest of #4028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up (F2). Sync the byte-identical 648-case corpus (adds the +/-MaxInt32 source-seed normalization collision from the Go side) and document the two go_math_rand port branches the differential corpus cannot reach -- int63n (dead for any u16 member set) and the int31n_fast rejection loop (fires with probability ~set_size/2^31 per draw) -- as accepted faithful 1:1 ports of Go's math/rand covered by Go's own stdlib tests. Full signer suite passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inal (#4033) Stacked on #4005 (base: `extraction/frost-signer-mirror-2026-05-26`). Implements the dependency-pin item from the review feedback ("get off the `=3.0.0-rc.0` pin"). `frost-secp256k1-tr 3.0.0` final (plus `frost-core`/`frost-rerandomized` 3.0.0) is published and unyanked on crates.io; the engine was anchored to the release candidate, which receives no post-release fixes. This moves the exact pin to the final release — same exact-pin discipline, correct anchor. Verified: full signer suite passes unchanged against the final (244 tests, clippy clean), confirming no rc.0→final API or behavior drift. Remaining half of the review item for the rollout gates: record which ZF/external audit reports cover `frost-core` 3.x and the `secp256k1-tr` ciphersuite specifically (the audited lineage claim in the readiness docs should cite the exact report and version range). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…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)
…+ 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)
…transitional signing out of production (#4028) Stacked on #4005 (base: `extraction/frost-signer-mirror-2026-05-26`). Implements item 1 of the review feedback on the FROST/ROAST stack (nonce rollback safety). ## Problem The transitional `StartSignRound`/`FinalizeSignRound` flow derives round-1 nonces deterministically from `H("round-nonce", signing_share, session_id, round_id, message, participant_id)`. Nonce-reuse safety therefore rested on two indirect properties: 1. **`round_id` schema integrity** — the participants set, Taproot tweak, and attempt context were bound only through `derive_round_id`. Any future change to that derivation (or an encoding collision in it) could let two different FROST transcripts share a nonce seed, which is the share-extraction condition. 2. **Consumed-round registry integrity** — the on-disk registries were the replay boundary. A VM snapshot restore, backup restore, or state replicated to a second host silently re-arms consumed rounds. ## What changed **1. Total direct binding (`RoundNonceBinding`, domain `round-nonce-v2`).** The nonce seed now directly binds every value that enters the FROST binding factor, challenge, Lagrange interpolation set, or key-material selection: group verifying key, Taproot merkle root, canonical signing-participant set — in addition to the existing signing share, session, round, message, and participant id. The struct carries a documented invariant so the next transcript input added to this flow gets added to the seed in the same change. Consequence: a rolled-back or cloned state can only ever repeat an *identical* transcript (producing the identical signature — no new information), never the same nonce under a different challenge. Nonce safety no longer depends on `round_id` schema or registry integrity at all. **2. Production hard-gate on the deterministic-nonce entry points.** Dealer DKG was already blocked in production, but that gate only fires at session *creation*: persisted state created under a development profile could be carried into a production-profile process and signed with. `StartSignRound`/`FinalizeSignRound` now reject in the production profile with reason `transitional_deterministic_signing_disabled_in_production`, making the OS-random interactive FROST path the only production signing path regardless of how on-disk state was created. ## Why not the per-boot RAM-only salt suggested in the review Exploration showed the transitional flow has every member independently derive **all** participants' commitments with zero round-1 exchange (members exchange only signature shares), so cross-machine determinism is load-bearing: a per-machine salt would break every multi-member bootstrap session. The two changes above implement the same goal — *rollback costs liveness, never keys* — within the flow's actual architecture: (1) removes the nonce-reuse class structurally, (2) removes production exposure structurally. The stateless interactive path (`GenerateNoncesAndCommitments`) already draws from OS randomness and holds nonces only in caller RAM. ## Tests - `deterministic_round_nonce_and_commitment_binds_full_transcript`: identical binding re-derives identical commitments; each of 7 binding inputs (message, tweak root, participants set, group key, session, round, participant) independently changes the commitment. - `start_sign_round_rejects_transitional_signing_in_production_profile` / `finalize_sign_round_rejects_transitional_signing_in_production_profile`: the state-smuggling scenario — dev-created dealer session, production-profile process — rejects at both entry points, even with the strict-mode env flag explicitly disabled. - `production_profile_forces_roast_strict_mode_without_env_flag` repurposed to assert the strict-mode forcing at the helper level (the FFI-level path it previously exercised is now unreachable in production by design). - Full suite: 246 passed, 0 failed; clippy and rustfmt clean. ## Notes for the mirror - The seed domain bump (`round-nonce` → `round-nonce-v2`) changes derived commitments for identical inputs; a mixed-version fleet cannot co-sign transitional rounds mid-rollout. Dev/staging-only flow, so the cost is a failed attempt until the fleet converges. - Port back to the tBTC monorepo signer alongside the next extraction sync.
…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)
…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)
…submodules Post-merge follow-up #2 from the June 2026 review stack (#4028-#4035): engine.rs absorbed four merges plus the round-nonce-v3 fix and every new PR was contending for the same 18,248-line file. Pure code move - no behavior change: - production code -> 16 thematic submodules under src/engine/ (state, persistence, config, policy, provenance, telemetry, lifecycle, audit, codec, frost_ops, nonce, roast, dkg, signing, transaction, testsupport); formerly-private items widened to pub(crate), and `mod engine` itself stays private in lib.rs, so the crate-external surface is identical - `mod tests` moved verbatim to engine/tests.rs: the module path engine::tests::* is unchanged, so run_phase5_chaos_suite.sh --exact filters and phase-doc test references stay valid - only semantic edit: the coordinator-seed-vectors include_str! path gains one ../ (the file now sits one directory deeper) Verified: cargo fmt --check; clippy --all-targets -D warnings; full suite 223 passed + 1 ignored / 24 / 1 - counts identical to the pre-split HEAD; formal_verification_ filter passes; all five chaos-suite --exact paths pass; testdata untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- point roast-coordinator-seed-derivation.md and formal/models/README.md at the functions' new submodule homes (roast.rs, signing.rs, persistence.rs); the formal README lines also dropped their stale monorepo tools/tbtc-signer/ path prefix - drop the per-file "Split from the former single-file engine.rs" provenance comments (mod.rs and git history record the split); keep the one-line module descriptions - tests.rs header now states the constraint instead of provenance: the file stays a single module because the chaos suite pins engine::tests::<name> paths with cargo test -- --exact Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…submodules (#4036) Post-merge follow-up #2 from the June 2026 review stack (#4028–#4035). `engine.rs` was deferred-split to avoid conflicting with the open stack; with the stack merged it had grown to 18,248 lines (absorbing four merges plus the round-nonce-v3 fix), and every new PR contends for the same file. This lands the split before anything new piles onto the monolith. ## What this is A **pure code move** — no behavior change, no API change, no test-path change. | Module | Lines | Contents | |---|---|---| | `state` | 434 | in-memory engine/session state, state-file lock, registry capacity guards | | `persistence` | 1,421 | encrypted state envelope, key providers/commands, corruption recovery, persisted↔live conversions | | `config` | 392 | the `TBTC_SIGNER_*` env surface: const names, defaults, parsers, profile detection | | `policy` | 633 | admission, signing-policy firewall, rate limiting, auto-quarantine config | | `provenance` | 353 | runtime provenance attestation gate | | `telemetry` | 313 | hardening latency trackers + metrics | | `lifecycle` | 468 | canary rollout, refresh cadence/shares, emergency rekey, quarantine status | | `audit` | 376 | transcript audit, blame-proof verification, differential fuzzing | | `codec` | 430 | hex/struct codecs, Go↔frost identifier conversions | | `frost_ops` | 303 | stateless `dkg_part1..3`, nonces, signing package, share, aggregate | | `nonce` | 99 | **`RoundNonceBinding` + deterministic round-nonce derivation (round-nonce-v3), isolated for audit** | | `roast` | 1,003 | RFC-21 attempt machinery: request fingerprints, round/attempt ids, attempt-context + transition-evidence validation | | `dkg` | 257 | `run_dkg` flow + transitional-dealer production gates | | `signing` | 970 | `start_sign_round` / `finalize_sign_round` flows, bootstrap synthetic contributions | | `transaction` | 227 | taproot tx building | | `testsupport` | 88 | cfg(test) cross-module helpers (`lock_test_state`, `reset_for_tests`, …) | | `tests` | 10,558 | the former inline `mod tests`, moved **verbatim** | ## Design decisions - **`engine::tests::*` paths are preserved.** `mod tests` moved as a single child module (`engine/tests.rs`), so `scripts/run_phase5_chaos_suite.sh`'s five `cargo test … -- --exact` filters and every `engine::tests::<name>` reference in the phase docs remain valid. Splitting tests further would force rewriting those contracts — left as an explicit team decision. - **Visibility:** formerly-private items are now `pub(crate)`; each submodule opens with `use super::*;` against glob re-exports in `mod.rs`. Since `lib.rs` keeps `mod engine;` **private**, the crate-external surface is byte-identical. Per-module visibility tightening can happen incrementally later. - **`config.rs` deliberately concentrates the env surface** — it pre-stages follow-up #3 (move `TBTC_SIGNER_*` env vars into an init-time FFI config struct) as a mostly-one-file change. - **Only semantic edit in the whole diff:** the `include_str!("../testdata/coordinator_seed_vectors.json")` in the tests gains one `../` because the file now sits one directory deeper. ## Verification - `cargo fmt --check` ✅, `cargo clippy --all-targets -- -D warnings` ✅ - Full suite: **223 passed + 1 ignored / 24 / 1 — counts identical to the pre-split HEAD** (verified by stashing the split and re-running on d47f009) - `cargo test formal_verification_` ✅ (5/5); all five chaos-suite `--exact` paths ✅ - `testdata/` untouched — seed vectors and shuffle corpus remain byte-identical - Review aid: `git diff d47f009 --color-moved=zebra --color-moved-ws=ignore-all-space` renders nearly the entire diff as moved lines; `git blame -C -C` follows history across the split. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Post-merge follow-up #3 from the June 2026 review stack: shrink the ops/audit surface by letting the host install the signer's operational configuration once at startup (frost_tbtc_init_signer_config) instead of exporting ~40 TBTC_SIGNER_* environment variables. Design (parity by construction): - every operational env read now goes through one chokepoint, engine::signer_env_var; with no config installed it falls through to the process environment, so existing env-driven behavior (and the entire pre-existing test suite) is unchanged - an installed config wins wholesale: the environment is no longer consulted for covered knobs, and an unset field means the built-in default - no per-knob source mixing - typed InitSignerConfigRequest (field = lowercased env suffix) converts to the same canonical strings the existing parsers consume, so every clamp/warn/reject path runs unchanged on identical inputs - deny_unknown_fields: a typo'd knob fails the init instead of silently running on defaults; enforcement-gated policy combinations (admission, firewall, auto-quarantine) are validated at install with rollback - re-init is idempotent for an identical request, rejected on conflict - secrets never ride the config FFI: TBTC_SIGNER_STATE_ENCRYPTION_KEY_HEX stays on the dedicated env/command key-provider channel (deliberate std::env::var exception, commented) - deletes lib.rs's duplicated profile/truthy parsing in favor of the engine's single implementation Verified: fmt --check; clippy --all-targets -D warnings; full suite 235 passed + 1 ignored / 24 / 1 (11 new tests incl. FFI round-trip); --features bench-restart-hook builds; chaos suite and formal_verification_ filter pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the converged Codex/Gemini review finding plus two findings from my own review of #4037: - candidate configs are now validated through a thread-local resolver override visible only to the validating thread, and published to the global slot only after validation succeeds. Previously the candidate was installed first and rolled back on failure, so a concurrent identical init could report idempotent success while the twin rolled the slot back to None, and concurrent readers could briefly act on a config that never legally installed. Both impossible now: failed init has no observable side effects, and idempotent success is only ever reported against a validated, installed config. - init now validates state_file_path(): a production config (explicit, or by profile-omission default) without state_path fails at init instead of installing and then failing at first state access with an env-var-oriented message; the state-path error now also names the state_path config field. - new end-to-end test: installed config's state_path is honored by run_dkg persistence after a process (re)start, and the existing in-process state-path-switch refusal is pinned as the contract for installing a config after state has been touched. - README: do not inline key material into state_key_command (the command string rides the config FFI); documented the no-side-effects init guarantee and the production state_path requirement. Suite: 237 passed + 1 ignored / 24 / 1; clippy -D warnings, fmt, chaos suite, bench-restart-hook feature build all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses Codex's re-review P2: the init validated state_path but not the adjacent key-provider knobs, so a production config that omitted state_key_provider (wholesale-defaulting to the env provider, which production forbids) - or that selected the command provider without a command - installed successfully and then failed at the first state access. - extract the structural head of state_encryption_key_material into resolve_state_key_provider_plan (provider selection, the production env-provider prohibition, command-spec presence; error strings unchanged) and have both the runtime key path and init validation consume it - one source of truth, no drift - init validation rejects: production defaulting to the env provider, command provider without state_key_command, unknown provider values; all WITHOUT reading the secret or executing the key command (pinned by a test whose key command points at a nonexistent binary) - README documents that production configs must carry the command key-provider pair Suite: 241 passed + 1 ignored / 24 / 1 (4 new tests); clippy -D warnings, fmt, chaos suite, bench-restart-hook build all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses Codex's third-round P2 (the family member my own review had deferred): production forces the provenance gate, so a production config without a complete, verifiable attestation set installed successfully and then failed every protected operation. - validate_candidate_config now runs enforce_provenance_gate(): self- gating (no-op when unenforced, so dev configs are unaffected), reads only candidate values plus local crypto - no secrets, no command execution, no network. Full verification at init (status, payload signature against trust root, runtime-version minimum, TTL); runtime calls still re-check, so an init-time pass does not exempt TTL aging. - production-config tests now carry a complete signed attestation (reusing the existing build_signed_provenance_attestation fixture); new tests pin: production-without-attestation rejected at init, enforced-gate-with-unparseable-trust-root rejected, and a complete production config (state path + command key provider + valid attestation + min version) installs. - README documents the production attestation requirement and the TTL caveat. Suite: 244 passed + 1 ignored / 24 / 1 (3 new tests); clippy -D warnings, fmt, chaos suite all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ig (#4037) Post-merge follow-up **#3** from the June 2026 review stack (#4028–#4035; #4036 landed the engine split that stages this change): move the `TBTC_SIGNER_*` env-var surface into an init-time FFI config struct, shrinking the ops/audit surface from ~40 scattered `std::env::var` reads to one explicit, validated installation at startup. ## What this adds New FFI entry `frost_tbtc_init_signer_config(request_ptr, request_len)` taking a typed JSON `InitSignerConfigRequest` (40 optional fields; field name = lowercased `TBTC_SIGNER_*` suffix). The host installs it once at startup. ## Semantics - **Wholesale source of truth.** Once installed, the environment is *not consulted* for any covered knob; an unset field means the built-in default. No per-knob mixing of config and env — split-brain configs can't exist. - **Fail-closed init.** `deny_unknown_fields` rejects typo'd knobs; enforcement-gated policy combinations (admission, signing-policy firewall, auto-quarantine) are validated at install by running the same loaders the runtime gates use, with rollback on rejection — a misconfigured signer fails at startup, not at first signing. - **Idempotent re-init** for an identical request (fingerprint match); conflicting re-init rejected. - **Secrets never ride the config FFI.** `TBTC_SIGNER_STATE_ENCRYPTION_KEY_HEX` is read exclusively from the dedicated env/command key-provider channel even when a config is installed (the one deliberate `std::env::var` left outside the chokepoint, commented at the read). - **Transitional compatibility.** With no config installed, `engine::signer_env_var` falls through to the process environment — existing hosts and the entire pre-existing test suite run unchanged; non-development profiles log a one-time warning suggesting the init FFI. ## Why parity is safe by construction The typed request converts to the same canonical strings the existing env parsers consume (`"true"`/`"false"`, decimal ints, comma-joined identifier lists), and every existing clamp/warn/reject path runs unchanged on identical inputs. The diff swaps `std::env::var(X)` → `signer_env_var(X)` at 31 sites and changes nothing else about how values are interpreted. Also deletes `lib.rs`'s duplicated profile/truthy parsing in favor of the engine's single implementation. Thanks to #4036, this lands as one new ~400-line module (`engine/init_config.rs`) plus one-line touches across `config/lifecycle/persistence/policy/provenance/state` — not an 18k-line-file churn. `engine/config.rs` remains the single home of the env-name constants. ## Verification - `cargo fmt --check` ✅; `cargo clippy --all-targets -- -D warnings` ✅ - Full suite **235 passed + 1 ignored / 24 / 1** — all 224 pre-existing tests pass unchanged (env-fallback parity), plus 11 new tests: config-over-env precedence, wholesale env-ignoring for unset fields, idempotent/conflicting re-init, invalid-profile rejection, install rollback on incomplete firewall policy, complete-admission-policy validation, secret-stays-on-env-channel, production-profile-forces-strict via config, `reset_for_tests` clearing, `deny_unknown_fields`, list/bool canonicalization, and an FFI round-trip - `--features bench-restart-hook` builds; chaos suite (5/5 `--exact` paths) and `formal_verification_` filter pass - `include/frost_tbtc.h` gains the symbol; README documents the contract ## Notes for reviewers - Knobs the runtime warn-and-defaults on (e.g. out-of-range timeouts) keep that behavior under config values — init validation only rejects what the runtime gates would reject. Tightening init further is possible later without breaking the contract. - Go-host adoption is a follow-up: this is additive ABI; nothing changes for hosts until they call the new entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ence Post-merge follow-up #5 from the June 2026 review stack (the remaining half of #4033's item): cite the exact external audit coverage of the pinned FROST stack in the readiness/rollout docs, and record the attestation-rotation operational requirement surfaced by #4037. - rollout gates doc gains a "Cryptographic Dependency Audit Status" section: NCC Group's Zcash FROST Security Assessment (2023-10-20) covered v0.6.0 (commit 5fa17ed) of frost-core and five ciphersuites; upstream states explicitly that frost-secp256k1-tr and rerandomized FROST are NOT included; Least Authority's Q1 2025 audit covered demo tooling only. Bottom line recorded honestly: the pinned frost-secp256k1-tr =3.0.0 and the v0.6.0->3.0.0 frost-core evolution have no external audit coverage, so Gate 1 must either commission an audit or record written risk acceptance scoped to canary - a team decision this section now gives a factual basis. - rollout runbook prerequisites gain the attestation rotation cadence: init-time config is immutable per process and attestation TTL caps at 7 days, so signers must restart with fresh attestation within every window; live re-attestation is deliberately unsupported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ence (#4042) Post-merge follow-up **#5** from the June 2026 review stack — the remaining half of #4033's item: cite the exact external audit report and version range covering the pinned FROST stack in the readiness/rollout docs. Also records the attestation-rotation operational requirement that #4037's review surfaced. ## Audit status (researched against upstream sources, 2026-06-12) The new "Cryptographic Dependency Audit Status" section in `roast-phase-5-security-rollout-gates.md` records, with citations: - **NCC Group, "Zcash FROST Security Assessment"** (report 2023-10-20): audited **v0.6.0** (commit `5fa17ed`) of `frost-core` + five ciphersuites — trusted-dealer and DKG key generation plus signing; all findings addressed and re-reviewed. - The upstream README's explicit exclusion, quoted verbatim: *"This does not include frost-secp256k1-tr and rerandomized FROST."* - **Least Authority's Q1 2025 FROST Demo audit** covered `frost-client`/`frostd` tooling only — not the library crates this signer consumes. - No 2.x/3.x release notes mention further audit coverage. **The honest bottom line, now on the record:** the exact ciphersuite this signer uses for production signatures (`frost-secp256k1-tr =3.0.0`, released 2025-04-23) and the v0.6.0 → 3.0.0 evolution of `frost-core` have **no external audit coverage**. Gate 1 sign-off must therefore either commission/await an audit covering that range (the checklist item-8 "audit as ECDSA-retirement merge gate" decision) or record a written, canary-scoped risk acceptance. The section gives that team decision its factual basis instead of letting "FROST was audited" stand unqualified. ## Attestation rotation cadence (runbook prerequisite 6) From #4037's design: init-time config is immutable for the process lifetime and attestation TTL caps at 7 days, so production signers must restart with fresh attestation material within every window — rollout stage scheduling has to absorb that cadence. Live re-attestation without restart is deliberately unsupported (it would need a dedicated narrow FFI; general config mutation reopens the split-brain risk the immutable design closed). Doc-only change; no code. Sources verified via the upstream README, NCC's published report PDF, zfnd.org announcements, and the ZcashFoundation/frost releases page. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- external audit covering frost-core 3.x + frost-secp256k1-tr is a HARD GATE for the ECDSA-retirement phases (resolves the Gate 1 fork recorded earlier today) - sidecar signer process chosen over in-process cgo as the target architecture; dlopen bridge stays transitional; unblocks #4007 scoping - script-tree commitment vs timelocked recovery leaf: explicitly open, no assumption may be baked in yet - proof-carrying blame deferred until production WITH a binding retention condition: keep enough signed bytes at detection points to diagnose targeted equivocation - t-of-included finalize scheduled as the first Phase 7 item: the transitional flow binds shares to the full included set's commitment list at StartSignRound (finalize enforces contributions == included set), so first-t-responsive requires the interactive two-round exchange that is Phase 7 itself Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etion Decision 6 (2026-06-12, MacLane): the transitional deterministic-nonce path is dev/staging-only behind the production gate, and its nonce safety rests on RoundNonceBinding transcript completeness - the F1 finding showed one missing field is a key-extraction-class bug that an experienced review missed. No production benefit justifies carrying that invariant indefinitely. - deletion trigger: interactive production path validated end to end; then the transitional StartSignRound/FinalizeSignRound deterministic flow and the nonce-binding machinery are removed - until then the transitional flow is FROZEN: no new transcript inputs (each must extend RoundNonceBinding; omission recreates F1) - nonce.rs carries the freeze marker at the point of hazard - item 6 interaction recorded: the Phase 7 interactive session flow is designed t-of-included-native from the start; no retrofit of the transitional finalize contract Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion log Codex and Gemini both flagged (P1): the item-7 deferral parenthetical asserted evidence retention "is implemented in the Go RFC-21 layer", but the base branch only detects a conflict and drops the envelope - the retention logic lives in the unmerged PR #4044. Because item 7's deferral is conditioned on retention being present, that false claim created a false sense of diagnosability. Reworded: the deferral is now explicitly contingent on retention landing; retention is attributed to PR #4044 (scaffold branch); and the base layer's drop-the-envelope behavior until that merges is stated plainly, so the deferral does not read as already in force. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…doc (#4043) Records MacLane's decisions on the follow-up checklist's open questions: audit = hard gate for ECDSA retirement; sidecar over in-process cgo; script-tree vs timelock explicitly open; item 7 deferred with a binding evidence-retention condition; item 6 scheduled as the first Phase 7 item (with the architectural reason: transitional shares are bound to the full included set at StartSignRound, so first-t-responsive requires the interactive exchange that is Phase 7 itself). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Decision Log entry 7: an unmet init-config demand (TBTC_SIGNER_INIT_CONFIG_PATH set but the FROST-native engine did not come up) is process-fatal in every profile, replacing the continue-on-legacy-bridge degradation from keep-core PR #4041. Runbook prerequisite 7 captures the operational consequence: config pushes and validation-tightening library upgrades are canaried node-by-node, and the attestation rotation cadence from prerequisite 6 becomes enforced rather than advisory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-ups: the decision-log entry now cites the concrete implementation PR (#4045), and runbook prerequisite 7 tells operators to scope TBTC_SIGNER_INIT_CONFIG_PATH to the signer service unit rather than the host-global environment - every binary importing the signing package honors the demand, so a global export plus a broken config would also kill maintenance tooling on the host (Gemini review crash-radius finding, addressed operationally; the fatality semantics themselves are the recorded decision). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## What Records 2026-06-12 decision 7 in the gates-doc Decision Log: an unmet init-config demand (`TBTC_SIGNER_INIT_CONFIG_PATH` set but the FROST-native engine did not come up) is **process-fatal in every profile**, replacing the continue-on-legacy-bridge degradation from keep-core #4041. Implementation: keep-core #4045 (scaffold). Adds runbook prerequisite 7 with the operational consequences: config pushes and validation-tightening library upgrades are canaried node-by-node (a bad fleet push now produces a visible, correlated outage instead of silent capability loss), and prerequisite 6's attestation-rotation cadence becomes enforced rather than advisory — a node restarted with expired attestation material will not start until re-attested. ## Rationale recorded in the entry The scaffold ships to production only when FROST is a production duty, so "running but FROST-dead" is the dangerous state: a silently half-alive node erodes FROST wallet fault budgets invisibly, while threshold redundancy is designed for loud, full, bounded outages. Fatality cannot be profile-conditional (unreadable file cannot reveal its profile; production-by-omission), so path-set is the only non-circular trigger. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Specifies the hardened interactive two-round FROST signing session - the production signing path - per the 2026-06-12 Decision Log: t-of-included-native finalize (decision 5), no transitional retrofit (decision 6), sidecar-shaped API contract (decision 2). The load-bearing design change: secret nonce custody moves inside the engine. Today's stateless primitives return serialized SigningNonces to the Go host and accept them back at sign_share, so nonces cross the FFI twice, live in host memory between rounds, and single-use is caller discipline only. The session layer keeps nonces in session-scoped engine memory behind an opaque handle, consumes atomically (durable marker before share release), never persists them - making restart/clone nonce reuse structurally impossible and removing all secret signing material from the FFI boundary. Also specified: the session API and registry semantics carried over from the coarse path's hardening inventory; t-of-included subset verification at Round2 (membership, subset-of-included, size t) so safety never depends on coordinator honesty; signed-body package envelopes extending the #4044 equivocation-evidence retention; the precise deletion trigger for the frozen transitional deterministic-nonce path (nonce.rs freeze marker now points here); reserved room for bounded n-t+1 concurrency; PR-sized phasing 7.0-7.6; and four open questions with proposed defaults for the freeze sign-off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four findings from the adversarial self-review pass: Round2 gains check (f): the member's own commitment entry in the signing package must be byte-identical to its Round1 output. Without it a malicious coordinator substitutes an honest member's commitment, the member's correctly-computed share fails verification at aggregation, and the blame machinery manufactures re-checkable evidence against an honest party - the same missing-field class as the F1 nonce-binding finding. Section 6 now states that share-verification blame is sound only because of (f), and that blame re-checking must run against the retained package envelope, never a reconstruction. Verification-before-consumption is now explicit: an invalid package leaves the nonce handle live. Live-state bounds added to section 5: open sessions and unconsumed handles are secret-bearing engine memory and get the registry discipline - hard cap, fail closed at capacity, TTL sweep that aborts and zeroizes abandoned sessions. The section 4 FFI claim is scoped to the signing path: dkg_part1/2 still hand secret round packages to the host (verified in frost_ops.rs), DKG custody is a named follow-up, and the audit scope must describe the DKG boundary as-is. Phasing 7.5 notes the manifest's V1-migration verification is an independent flip condition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner sign-off 2026-06-12 with review converged (adversarial-pass findings applied in 73dc594; Codex and Gemini clean). The four forced questions are decided and recorded as Decision Log entry 8: dedicated operator-key-signed topic for signing packages, members-to-coordinator round-1 transport, strict first-t responsive subset, markers-only durability. Spec status flips Proposed -> FROZEN; implementation starts at Phase 7.1 against this contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## What Phase 7.0: the spec-freeze candidate for the hardened interactive two-round FROST signing session — the production signing path — assembling the already-settled decisions (t-of-included-native per decisions 5/6, sidecar-shaped API per decision 2, OS-randomness-only production signing) into a precise contract, plus the deletion plan for the frozen transitional deterministic-nonce path. ## The load-bearing design change: nonce custody moves inside the engine Verified current state: the stateless primitives (`frost_ops.rs`) return serialized `SigningNonces` to the Go host and accept them back at `sign_share` — secret nonces cross the FFI twice, live in host memory between rounds, and single-use is enforced by caller discipline only. Calling `sign_share` twice with one nonce pair is the canonical FROST key-extraction failure and nothing prevents it today. The session layer (spec §4): nonces are generated and held in session-scoped engine memory behind an opaque handle, consumed atomically (durable consumption marker before the share leaves the engine), zeroized on use, and **never persisted and never exported**. Restart loses in-flight nonces by construction (attempt fails safe); the cloned-state nonce-reuse class becomes structurally impossible; and after Phase 7 no secret signing material transits the FFI in either direction — which is also the audit story for that boundary. ## Also specified - **Session API** (§5): `InteractiveSessionOpen/Round1/Round2/Aggregate/Abort`, idempotent-or-fail-closed, strict-mode attempt contexts only, consumed-registry semantics carried from the coarse path, transport-agnostic for the dlopen→sidecar swap. - **t-of-included semantics** (§6): engine-side subset verification at Round2 (own membership, subset-of-included, size t) so safety never depends on coordinator honesty; signing packages ride #4040-style signed-body envelopes extending the #4044 equivocation-evidence retention; silent members cost zero attempts. - **Deletion trigger made precise** (§7): three conditions defining "interactive production path validated e2e" (Phase-5-equivalent suites incl. consumed-nonce-marker persist-fault cases; a real testnet t-of-included finalize through the full retry machinery; pinned cross-language vectors). The `nonce.rs` freeze marker now points at this section — the only code change in this PR, comment-only on the frozen file. - **Reserved, not built** (§8): bounded n−t+1 concurrency hooks (attempt-scoped keys/handles). - **Phasing** (§9): PR-sized 7.0–7.6 with repo-side mapping; #4007 sidecar scoping folds into 7.0 as an addendum. - **Open questions with proposed defaults** (§10): package-distribution channel, round-1 transport shape, responsive-subset policy, markers-only durability — to be decided at freeze sign-off and recorded in the Decision Log. ## Freeze process Status is Proposed; it freezes on signer + keep-core owner sign-off per §11, with the §10 defaults ratified or overridden in the gates-doc Decision Log. The audit scope statement should reference this document and name the §5 API as in-scope (decision 1 interaction). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
Lands the Rust FROST/ROAST signer at
pkg/tbtc/signer/alongside the existing Go DKG coordinator. The initial import was extracted fromtlabs-xyz/tbtc:feat/frost-schnorr-migrationat frozen signed tagfrost-extraction-source-v1, then this PR applied targeted canonical-path, CI, and reviewer hardening follow-ups in the canonical keep-core repo.This is no longer a pure byte-for-byte mirror PR. The post-freeze divergences are intentional canonical keep-core changes and are recorded below. After this PR lands,
threshold-network/keep-coreis the source of truth for the signer; the extracted monorepo tag is provenance for the initial import only.Source-PR Provenance
tlabs-xyz/tbtcfeat/frost-schnorr-migrationfrost-extraction-source-v1(signed annotated tag by maclane)52389bd5cccb5daeef195671feb7ca46be6e2f37The monorepo provenance above is used only to audit the initial import. Ongoing signer development, fixes, CI, and reviews happen in
threshold-network/keep-core.Why This Lives In keep-core
Per extraction plan v38 section 3.1, the signer co-locates with keep-core because:
Layout Introduced
Expanded Divergence Register
These are the intentional post-freeze divergences from
frost-extraction-source-v1.pkg/tbtc/signer/scripts/formal/check_roast_attempt_context_vectors.mjs7ceb3929b8403ebde515e7ac290414e2aa1fe6920085f1f4930dd2219d93a406pkg/tbtc/signer/scripts/formal/run_tla_models.sh64474409f2682fb903e26fee30613bc3d2a3542df4cebec6d86992112ab26b8ctla2tools.jarrelease-asset checksum.pkg/tbtc/signer/README.md3fe9046df81442a95139d9ced977df3ec05cca468bf77fdd185be48e73426539tools/tbtc-signertopkg/tbtc/signer; fixed local docs link.pkg/tbtc/signer/src/engine.rse149510ec9eca52290f09d26f5d16c684d0a3941134cc46d4c3484947d8bf0ddpkg/tbtc/signer/src/ffi.rs02bd12a7aab5d40e9e0eb55f8011caf86a3586eb795ab910179a4604b4335c14ptr::slice_from_raw_parts_mutinfree_buffer.pkg/tbtc/signer/src/lib.rse04f05b30ffa6bd9f282e63482c465945ebaacb31a989c6c045cb6dae418b24cpkg/tbtc/signer/tests/p2tr_signature_fraud_vectors.rs1c852f79ab683c03bf4846dd2ac58c37cf73d1645f1c4813034a16a0a39a04ddtest/vectorslayout..github/workflows/tbtc-signer-formal.yml623e9c8fab66497339c5ec88800c20afc68075a76680f4a8eb9ea7e327d0ce28pkg/tbtc/signer/test/vectors/p2tr-signature-fraud-v0.json4edc25113ef5a022af347eb0c6752b06f61f4baace94a13b91cf15042065b002Dual signoff for allowlisted divergences remains required before merge per plan v38 section 4.2:
CI Coverage
This PR now runs:
cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --checkcargo clippy --manifest-path pkg/tbtc/signer/Cargo.toml --all-targets -- -D warningscargo test --manifest-path pkg/tbtc/signer/Cargo.tomlcargo test --manifest-path pkg/tbtc/signer/Cargo.toml formal_verification_pkg/tbtc/signer/scripts/formal/run_tla_models.shVerification
Latest local verification after review fixes:
cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --checkcargo clippy --manifest-path pkg/tbtc/signer/Cargo.toml --all-targets -- -D warningsTBTC_SIGNER_STATE_PATH=/tmp/tbtc-signer-ci-state-local-4005.json cargo test --manifest-path pkg/tbtc/signer/Cargo.tomlcargo test --manifest-path pkg/tbtc/signer/Cargo.toml formal_verification_Latest GitHub CI on this branch is green, including
Signer Rust checks,Signer formal invariants, andTLA model checks.Relationship To PR #3866
PR #3866 lands the Go-side DKG coordinator. This PR lands the Rust-side signer. They are complementary protocol implementations:
PR #3866 and this PR can land independently in either order.
Plan Context
This is one of the mergeable mirror PRs comprising the FROST extraction, alongside tbtc-v2, tbtc-subgraph, and tbtc-v3-indexer mirror PRs.