Skip to content

feat(tbtc/signer): Rust FROST/ROAST signer (distributed DKG, interactive-only signing, FFI ABI 2.0)#4005

Open
mswilkison wants to merge 244 commits into
mainfrom
extraction/frost-signer-mirror-2026-05-26
Open

feat(tbtc/signer): Rust FROST/ROAST signer (distributed DKG, interactive-only signing, FFI ABI 2.0)#4005
mswilkison wants to merge 244 commits into
mainfrom
extraction/frost-signer-mirror-2026-05-26

Conversation

@mswilkison

@mswilkison mswilkison commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Lands pkg/tbtc/signer/, the Rust crate behind libfrost_tbtc: the FROST threshold-Schnorr signing engine for tBTC v2's migration from tECDSA to Taproot (BIP-340/341) wallets. The Go client consumes it over a versioned cgo/JSON FFI. The crate implements the per-participant distributed FROST DKG rounds, interactive ROAST signing sessions with member-custodied nonces, signature-share verification with attributable blame, Taproot transaction assembly under a signing-policy firewall, and encrypted signer-state persistence — plus the formal models, cross-language vectors, and CI gates that guard all of it.

Two properties define the current surface. Signing is interactive-only: the transitional "coarse" one-shot signing path that earlier revisions of this branch carried (frost_tbtc_run_dkg, frost_tbtc_start_sign_round, frost_tbtc_finalize_sign_round, frost_tbtc_generate_nonces_and_commitments, frost_tbtc_sign_share, frost_tbtc_aggregate) has been deleted. Removing exported symbols is an incompatible contract change, so the structured version reported by frost_tbtc_abi_version is now ABI 2.0 and Go bridges fail closed against an incompatible library. Key generation is a real distributed DKG: dkg_part1/2/3 plus gated key-package persistence; the dealer-style bootstrap path is gone from the exported surface and is rejected outright under the production profile.

This PR is self-contained: nothing in keep-core consumes the crate yet (the cgo bridge and node wiring live in #3866), so it can land independently and first.

Why this lives in keep-core

Per extraction plan v38 section 3.1:

Provenance

The initial import was extracted from tlabs-xyz/tbtc:feat/frost-schnorr-migration at frozen signed tag frost-extraction-source-v1 (commit 52389bd5cccb5daeef195671feb7ca46be6e2f37; manifest: https://github.com/tlabs-xyz/tbtc/blob/frost-extraction-source-v1/extraction/frost-extraction-source-manifest.json). That tag audits the initial import only. Since then, all signer development has happened in threshold-network/keep-core: this branch now carries ~200 commits, almost all landed through individually reviewed sub-PRs merged into it (e.g. #4011, #4018, #4028, #4031, #4036, #4051#4055, #4062, #4068, #4077, #4098, #4104, #4111#4114, #4123#4129, #4136, #4137). keep-core is the source of truth for the signer.

Scope

FFI contract and ABI (src/lib.rs, src/ffi.rs, src/api.rs, include/frost_tbtc.h)

  • 28 exported C symbols with deterministic JSON request/response envelopes, typed error codes for retry-safe orchestration, and an explicit buffer-ownership contract (frost_tbtc_free_buffer); FFI buffers are zeroized and the production panic hook never reflects raw panic payloads across the boundary.
  • frost_tbtc_abi_version returns a structured {abi_major, abi_minor} contract version with documented bump rules — currently 2.0 after the coarse-path deletion.
  • Init-time runtime configuration via frost_tbtc_init_signer_config (TBTC_SIGNER_* knobs), validated fail-closed: an unknown profile or degenerate signing window is fatal.

Distributed DKG (engine/dkg.rs, persistence gates in engine/persistence.rs / engine/provenance.rs)

  • dkg_part1/2/3 per-participant round functions over frost-secp256k1-tr (pinned to the 3.0.0 final release).
  • persist_distributed_dkg_key_package accepts a completed distributed-DKG key package as signing material only behind provenance, admission-policy, and operator-quarantine gates, with participant-count and canonical-ID validation, and verifies the signing share derives to its public share before accepting it.
  • Key material is resolved by key group across sessions and survives restart, so a wallet created in one DKG session is signable from later signing sessions.

Interactive ROAST signing (engine/interactive.rs, engine/roast.rs, engine/verify_share.rs, engine/frost_ops.rs)

  • Session lifecycle open → round1 (commitments) → round2 (share release) → aggregate/abort. Round-1 nonces live only in engine memory and never persist; per-node live-session caps and inactivity TTLs bound the registry; the open path is idempotent per (session_id, attempt_id, member_identifier) with durable consumption markers; multi-seat operators get member-keyed session state.
  • Fail-closed session invariants: signing gates (policy firewall, quarantine, lifecycle, emergency-rekey) are re-evaluated at the Round2 share release, not just at open; a session is bound to one key group for life and cross-wallet rebinding is refused; stateless nonce primitives are rejected under the production profile.
  • interactive_aggregate self-verifies the tweaked (key-path or script-path) signature, is idempotent via completion markers bound to the message and taproot root, and emits candidate-culprit blame for invalid shares. verify_signature_share backs the Go-side Round2 share verifier, including tweaked-root equivalence. derive_interactive_attempt_context gives the host a canonical attempt-context derivation so session ids stay in contract.
  • RFC-21 Annex A coordinator-seed derivation with cross-language conformance vectors and a 600-case Go/Rust coordinator-shuffle corpus replay (go_math_rand.rs pins bit-exact Go math/rand parity).

Taproot transactions and signing policy (engine/transaction.rs, engine/policy.rs)

  • build_taproot_tx assembles validated unsigned Taproot transactions from provided inputs/outputs.
  • A signing-policy firewall screens what may be signed; it is force-enabled with built-in defaults under the production profile.

State and secret-material hardening (engine/state.rs, engine/persistence.rs, engine/audit.rs, engine/telemetry.rs, src/bin/admission_checker.rs)

  • Signer state persists as an XChaCha20-Poly1305 AEAD envelope with AAD binding over the key id, key provider, and algorithm; pluggable state-key providers (environment, or an external command with timeout); plaintext state is gated out of production builds.
  • Zeroization throughout (RNG state, serde-owned secret strings, FFI buffers) and Debug redaction for secret-bearing structs.
  • Share-refresh cadence with fingerprint history (including legacy-state backfill), emergency rekey, auto-quarantine, a replay-protected admission-override registry, ROAST transcript audit and blame-proof verification, hardening-metrics counters, a differential-fuzzing harness, and canary rollout controls.
  • An admission_checker binary plus sample policy JSONs for operator-side admission validation.

Formal verification and vectors (docs/formal/models/, scripts/formal/, tests/, test/vectors/, testdata/)

  • Four TLA+ models model-checked in CI (RoastAttemptStateMachine, StateKeyProviderPolicy incl. a production config, RoastRolloutPolicy, TeeEnforcementModes — the latter two describe planned enforcement scope), plus a formal_verification_ invariant test suite.
  • Cross-language test vectors: BIP-341 P2TR signature-fraud vectors (shared with the tbtc-v2 contract tests), ROAST attempt-context v1, coordinator seed vectors, and the shuffle corpus.

CI (.github/workflows/tbtc-signer-formal.yml)

  • Four blocking jobs: Signer Rust checks (cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test, all with --locked), Signer dependency audit (blocking cargo-deny RustSec advisory gate), Signer formal invariants, and TLA model checks. Actions are pinned to commit SHAs with checkout credential persistence disabled.

Docs (pkg/tbtc/signer/docs/)

  • ROAST implementation plan and phase 0–5 specs/runbooks/security-rollout gates, the Phase 7 interactive-session spec freeze with sidecar-transport addendum and package-envelope design, secret-material hardening plan, permissioned-signer hardening RFC, and the FROST dependency audit status.

Misc

  • pkg/bitcoin/electrum: refresh the Fulcrum integration-test endpoint (incidental CI fix; the only change outside pkg/tbtc/signer/ and its workflow).

Validation

Relationship to companion PRs

These three PRs comprise the FROST extraction into the canonical repos (plan v38).

Review notes

The diff is +32,305/−2 across 77 files, but it slices cleanly:

  1. Contract first: include/frost_tbtc.h (102 lines), then src/api.rs and src/lib.rs — the exported symbols, JSON envelopes, error codes, and the ABI versioning rules.
  2. Shared engine state: engine/mod.rs, engine/lifecycle.rs, engine/state.rs, engine/codec.rs.
  3. DKG slice: engine/dkg.rs plus the persistence/provenance gates in engine/persistence.rs and engine/provenance.rs.
  4. Signing slice (the core): engine/interactive.rs, engine/roast.rs, engine/verify_share.rs, engine/frost_ops.rs.
  5. Policy and hardening: engine/policy.rs, engine/init_config.rs, engine/config.rs, engine/telemetry.rs, engine/audit.rs, src/bin/admission_checker.rs.
  6. Tests and vectors last: engine/tests.rs (~8.9k lines, organized by endpoint), tests/p2tr_signature_fraud_vectors.rs, and the vector/corpus JSON.

Roughly 9k lines are engine logic, ~9.5k are tests, and the remainder is Cargo.lock, docs, vectors, and CI. Nearly all commits landed via individually reviewed sub-PRs merged into this branch, so reviewing sub-PR-by-sub-PR is a practical alternative to reviewing the squashed diff.

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>
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces pkg/tbtc/signer, a Rust tbtc-signer bootstrap crate with a C ABI JSON-over-buffer interface, FFI translation helpers, API types, error semantics, deterministic coordinator selection, a comprehensive lib.rs FFI surface with tests, an admission-checker CLI, Criterion benchmarks, TLA+ formal models, conformance vectors/checkers, and extensive design/runbook documentation.

Changes

Core Signer Implementation

Layer / File(s) Summary
FFI & API contract definition
include/frost_tbtc.h, src/api.rs
C header declares TbtcBuffer and TbtcSignerResult structs plus ~18 frost_tbtc_* function signatures; Rust API types define serde-serialized request/response payloads for DKG, sign-round lifecycle, transcript audit, quarantine, refresh, emergency rekey, differential fuzzing, and canary rollout.
Error classification and recovery semantics
src/errors.rs
EngineError enum with thiserror integration; code() maps variants to stable string identifiers; recovery_class() classifies errors as "recoverable" or "terminal" with explicit handling for consumed attempt/round replays.
FFI translation layer
src/ffi.rs
C-compatible buffer structs, JSON serialization/deserialization helpers, ffi_entry for panic containment via catch_unwind, and safe free_buffer memory management for the FFI boundary.
Deterministic coordinator selection
src/go_math_rand.rs
Rust port of Go's math/rand with fixed cooked constants table, seedrand, bounded sampling, and shuffle; select_coordinator_identifier returns deterministic coordinators matching keep-core outputs per test vectors.
Lib.rs FFI endpoints & bootstrap gating
src/lib.rs
Module declarations, signer version, bootstrap-mode environment/profile parsing with strict production gating, test-only override, ~18 #[no_mangle] extern "C" endpoints (DKG, sign-round, transcript audit, emergency rekey, canary rollout, refresh-shares), and comprehensive test suite covering idempotency, lifecycle transitions, Taproot validation, and emergency/rollout semantics.

Admission Checking & Policy Enforcement

Layer / File(s) Summary
Admission checker binary
src/bin/admission_checker.rs
JSON-configured operator admission evaluator with policy constraints (per-provider/region caps, custody/attestation/SLA requirements), optional DAO override verification (Schnorr signature validation over SHA-256 payload digest), and replay registry persistence to prevent re-use of override decisions.

Benchmarking & Formal Test Vectors

Layer / File(s) Summary
Phase 5 ROAST benchmark suite
benches/phase5_roast.rs
Criterion benchmarks exercising DKG, start/finalize sign-round, state reload/recovery, and attempt transitions (timeout/invalid-share/stale-replay); includes FFI helpers, hashing/fingerprinting utilities, deterministic coordinator probing, and error validation.
Taproot signature fraud formal verification
tests/p2tr_signature_fraud_vectors.rs
Integration test loading p2tr-signature-fraud-v0.json vectors, computing BIP-341 key-path sighashes, verifying Schnorr signatures, deriving dual challenge identities, and asserting mutation-based identity uniqueness.
ROAST attempt-context test vectors
test/vectors/roast-attempt-context-v1.json
JSON schema and fixtures for deterministic fingerprint/attempt-id computation across multiple participant sets and edge cases, used by formal verification checkers.

TLA+ Formal Verification Models

Layer / File(s) Summary
ROAST attempt state machine model
docs/formal/models/RoastAttemptStateMachine.tla, .cfg
TLA+ module defining attempt numbering, active participants, deterministic coordinator, consumed-attempt registries (transient and durable), transitions (advance/restart/reload), and safety invariants for monotonicity and replay safety.
ROAST rollout policy state machine
docs/formal/models/RoastRolloutPolicy.tla, .cfg
TLA+ module specifying five-stage rollout progression (bootstrap→canary→broad→rollback↔halted) with control signals and temporal properties governing promotion/hold/rollback and emergency stop.
State-key provider policy & production gating model
docs/formal/models/StateKeyProviderPolicy.tla, .cfg, .production.cfg
TLA+ module modeling provider/key selection lifecycle and load outcomes; production-gate enforcement; properties asserting fail-closed behavior and exact binding enforcement.
TEE enforcement modes state machine
docs/formal/models/TeeEnforcementModes.tla, .cfg
TLA+ module for TE admission control with three enforcement modes, attestation states, break-glass override, and properties enforcing valid attestation and admission stability.
Formal models documentation & runner
docs/formal/models/README.md, scripts/formal/run_tla_models.sh
README documenting model coverage and bounds; Bash runner downloading/verifying TLA+ toolchain and executing TLC model checks.

Design Documentation & Specifications

Layer / File(s) Summary
ROAST protocol phase specifications
docs/roast-phase-{0-spec-freeze,1.5-consumed-registry,2-coordinator,3-transcript,4-liveness,5-baseline,5-runbook,5-security-gates}.md
Phase 0 spec defining attempt-context, coordinator validation, strict-mode gating, and error taxonomy; Phases 1.5–5 covering consumed-registry integration, coordinator enforcement, transcript replay hardening, liveness policy with recovery classification, baseline calibration, rollout runbook, and security gates.
ROAST implementation roadmap
docs/roast-implementation-plan.md
Complete ROAST migration plan covering baseline, goals/non-goals, design principles, threat model, target semantics (deterministic attempt/coordinator, authenticated retry, replay-safety), phased deliverables (Phase 0–5), cross-repo dependencies, and Definition-of-Done.
Hardening, security & operational plans
docs/permissioned-signer-hardening-rfc.md, docs/tbtc-signer-secret-material-hardening-plan.md, docs/tee-whitelisted-signer-enforcement-plan.md
RFC with P0–P2 hardening phases; secret-material plan (encrypted-at-rest XChaCha20-Poly1305 envelope); TEE operator enforcement covering admission tokens, runtime behavior, break-glass, and failure modes.
API contract and design decisions
docs/signer-api-contract-decision-brief.md, docs/rust-rewrite-bootstrap.md
Decision brief comparing round-level vs coarse session API (recommending session); bootstrap status documenting implemented features and production gates.
Future considerations documentation
docs/true-late-t_of_n_finalize_considerations.md
Discussion draft on true late t-of-n finalize tradeoffs and current recommendation to defer as future consideration.

Build Configuration & Scripts

Layer / File(s) Summary
Crate metadata and build setup
pkg/tbtc/signer/Cargo.toml, pkg/tbtc/signer/build.sh, pkg/tbtc/signer/.gitignore
Cargo.toml defines tbtc-signer crate (edition 2021, MIT), library config (cdylib+rlib), bench-restart-hook feature, frost-secp256k1-tr dependency; build.sh with strict bash and release cargo build; .gitignore ignores target/.
Comprehensive README documentation
pkg/tbtc/signer/README.md
Full README covering exposed C ABI endpoints, not-yet-implemented items, build/test commands, admission checker CLI, encrypted state provider contracts, benchmark/chaos suite invocation, FFI JSON contract, error codes, and buffer management.
Admission checker sample configurations
pkg/tbtc/signer/scripts/admission-*.sample.json
JSON samples for policy (constraints, DAO overrides), candidates, existing operators, override signatures, and replay registries.
Formal verification & testing scripts
pkg/tbtc/signer/scripts/formal/check_roast_attempt_context_vectors.mjs, pkg/tbtc/signer/scripts/run_phase5_chaos_suite.sh, pkg/tbtc/signer/scripts/formal/run_tla_models.sh
Node.js CLI for ROAST vector conformance; Bash runners for chaos suite scenarios and TLA+ model checking.

🎯 4 (Complex) | ⏱️ ~75 minutes

🐰 A bootstrap springs forth, with FFI calls so bright,
Formal models carved in TLA+ light,
ROAST coordinators dancing in Go-lang's stride,
Secrets encrypted with cryptographic pride,
From signer to harness, one system unified!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a Rust FROST/ROAST signer with DKG, signing, and a new FFI ABI.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch extraction/frost-signer-mirror-2026-05-26

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (3)
pkg/tbtc/signer/src/api.rs (1)

196-202: 💤 Low value

Missing #[serde(default, skip_serializing_if)] on script_tree_hex.

All other Option<T> fields in this file use #[serde(default, skip_serializing_if = "Option::is_none")], but script_tree_hex does not. This inconsistency means the field will serialize as null when 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_var and std::env::remove_var are 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 value

Consider cleaning up the temp file if rename fails.

If fs::rename fails (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

📥 Commits

Reviewing files that changed from the base of the PR and between 980587b and 335ce60.

⛔ Files ignored due to path filters (1)
  • pkg/tbtc/signer/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (48)
  • pkg/tbtc/signer/.gitignore
  • pkg/tbtc/signer/Cargo.toml
  • pkg/tbtc/signer/README.md
  • pkg/tbtc/signer/benches/phase5_roast.rs
  • pkg/tbtc/signer/build.sh
  • pkg/tbtc/signer/docs/formal/models/README.md
  • pkg/tbtc/signer/docs/formal/models/RoastAttemptStateMachine.cfg
  • pkg/tbtc/signer/docs/formal/models/RoastAttemptStateMachine.tla
  • pkg/tbtc/signer/docs/formal/models/RoastRolloutPolicy.cfg
  • pkg/tbtc/signer/docs/formal/models/RoastRolloutPolicy.tla
  • pkg/tbtc/signer/docs/formal/models/StateKeyProviderPolicy.cfg
  • pkg/tbtc/signer/docs/formal/models/StateKeyProviderPolicy.production.cfg
  • pkg/tbtc/signer/docs/formal/models/StateKeyProviderPolicy.tla
  • pkg/tbtc/signer/docs/formal/models/TeeEnforcementModes.cfg
  • pkg/tbtc/signer/docs/formal/models/TeeEnforcementModes.tla
  • pkg/tbtc/signer/docs/permissioned-signer-hardening-rfc.md
  • pkg/tbtc/signer/docs/roast-implementation-plan.md
  • pkg/tbtc/signer/docs/roast-phase-0-spec-freeze.md
  • pkg/tbtc/signer/docs/roast-phase-1.5-consumed-registry-integration.md
  • pkg/tbtc/signer/docs/roast-phase-2-coordinator-policy-enforcement.md
  • pkg/tbtc/signer/docs/roast-phase-3-attempt-transcript-replay-hardening.md
  • pkg/tbtc/signer/docs/roast-phase-4-liveness-policy-recovery.md
  • pkg/tbtc/signer/docs/roast-phase-5-baseline-calibration.md
  • pkg/tbtc/signer/docs/roast-phase-5-rollout-runbook.md
  • pkg/tbtc/signer/docs/roast-phase-5-security-rollout-gates.md
  • pkg/tbtc/signer/docs/rust-rewrite-bootstrap.md
  • pkg/tbtc/signer/docs/signer-api-contract-decision-brief.md
  • pkg/tbtc/signer/docs/tbtc-signer-secret-material-hardening-plan.md
  • pkg/tbtc/signer/docs/tee-whitelisted-signer-enforcement-plan.md
  • pkg/tbtc/signer/docs/true-late-t-of-n-finalize-considerations.md
  • pkg/tbtc/signer/include/frost_tbtc.h
  • pkg/tbtc/signer/scripts/admission-candidate.sample.json
  • pkg/tbtc/signer/scripts/admission-existing.sample.json
  • pkg/tbtc/signer/scripts/admission-override-registry.sample.json
  • pkg/tbtc/signer/scripts/admission-override.sample.json
  • pkg/tbtc/signer/scripts/admission-policy-v1.sample.json
  • pkg/tbtc/signer/scripts/formal/check_roast_attempt_context_vectors.mjs
  • pkg/tbtc/signer/scripts/formal/run_tla_models.sh
  • pkg/tbtc/signer/scripts/run_phase5_chaos_suite.sh
  • pkg/tbtc/signer/src/api.rs
  • pkg/tbtc/signer/src/bin/admission_checker.rs
  • pkg/tbtc/signer/src/engine.rs
  • pkg/tbtc/signer/src/errors.rs
  • pkg/tbtc/signer/src/ffi.rs
  • pkg/tbtc/signer/src/go_math_rand.rs
  • pkg/tbtc/signer/src/lib.rs
  • pkg/tbtc/signer/test/vectors/roast-attempt-context-v1.json
  • pkg/tbtc/signer/tests/p2tr_signature_fraud_vectors.rs

Comment thread pkg/tbtc/signer/docs/formal/models/README.md
Comment thread pkg/tbtc/signer/docs/roast-implementation-plan.md Outdated
Comment thread pkg/tbtc/signer/docs/roast-phase-5-rollout-runbook.md Outdated
Comment thread pkg/tbtc/signer/docs/roast-phase-5-security-rollout-gates.md Outdated
Comment thread pkg/tbtc/signer/docs/rust-rewrite-bootstrap.md Outdated
Comment thread pkg/tbtc/signer/README.md Outdated
Comment thread pkg/tbtc/signer/scripts/admission-policy-v1.sample.json Outdated
Comment thread pkg/tbtc/signer/scripts/formal/run_tla_models.sh Outdated
Comment thread pkg/tbtc/signer/tests/p2tr_signature_fraud_vectors.rs Outdated
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between bfd7658 and 551bd42.

📒 Files selected for processing (1)
  • .github/workflows/tbtc-signer-formal.yml

Comment thread .github/workflows/tbtc-signer-formal.yml Outdated
Comment thread .github/workflows/tbtc-signer-formal.yml Outdated
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>
mswilkison added a commit that referenced this pull request May 26, 2026
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.
## Stack

Rust signer review-fix stack 1 of 2.

- Base: `extraction/frost-signer-mirror-2026-05-26`
([#4005](#4005))
- Follow-up:
[#4156](#4156) —
admission-attestation validation

## What changed

- Retain the `RefreshShares` symbol but fail closed with terminal code
`cryptographic_refresh_not_supported` until a multi-round, zero-constant
FROST refresh protocol exists.
- Bump the FFI contract from ABI 3.3 to 4.0 so ABI-3 consumers reject
the incompatible success-to-error response change during startup
negotiation.
- Remove the SHA-256 pseudo-refresh path, its process-local
pending-operation machinery, and tests that encoded invalid
refresh-success semantics.
- Prove rejected refresh requests leave DKG key packages, the public
package, lifecycle state, and persisted bytes unchanged.
- Anchor the first refresh deadline and overdue metric to durable DKG
creation time.
- Treat metadata from the retired synthetic refresh stub as
non-authoritative: it cannot postpone cadence, increment valid refresh
counts, or claim key continuity.
- Mark persisted synthetic refresh-only sessions without a DKG anchor
immediately overdue in both status and telemetry, including when the
system clock saturates at the Unix epoch.

## Review findings addressed

- P1: implement cryptographic refresh instead of hashing shares — the
unsafe one-shot operation now rejects and returns no replacement
material.
- P2: anchor the first refresh deadline to DKG creation — status and
telemetry share the same durable deadline helper.
- P2: bump the ABI for the incompatible refresh response — the endpoint
now reports ABI 4.0 with its minor version reset to zero.
- P1: mark unanchored legacy refresh sessions overdue — untrusted
refresh artifacts now resolve to an explicit, unconditionally overdue
sentinel instead of a sliding deadline.

The C symbol, request shape, response envelope, and reserved response
type remain present; the major version makes the changed status and JSON
semantics explicit.

## Verification

- `cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --check`
- `cargo check --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets`
- `cargo clippy --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets -- -D warnings`
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml`
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml
formal_verification_`
- `pkg/tbtc/signer/scripts/run_phase5_chaos_suite.sh`
- `git diff --check`

The TLA+ runner was not available locally because this machine has no
Java runtime; CI remains the authoritative model-check gate.
## Stack

Rust signer review-fix stack 2 of 2.

- Depends on:
[#4155](#4155)
- Ultimate base: `extraction/frost-signer-mirror-2026-05-26`
([#4005](#4005))

## What changed

- Reject an empty or whitespace-only policy
`required_attestation_status` with
`required_attestation_status_missing`.
- Reject an empty or whitespace-only candidate `attestation_status` with
`attestation_status_missing`.
- Compare statuses only after both normalized values are non-empty,
avoiding a misleading mismatch reason for missing data.
- Preserve case-insensitive, whitespace-normalized matching for
legitimate non-empty values.

## Review finding addressed

- P2: reject empty attestation statuses before comparing them — an empty
policy and empty candidate can no longer normalize to equality and pass
admission.

## Verification

- `cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --check`
- `cargo check --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets`
- `cargo clippy --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets -- -D warnings`
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml`
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml --bin
admission_checker attestation`
- `git diff --check`
## Stack

Rust signer review-fix stack 3 of 3.

- Depends on: #4158, which depends on #4157
- Ultimate base: `extraction/frost-signer-mirror-2026-05-26` (#4005)

Merge #4157 and #4158 first, then retarget this PR down the stack.

## What changed

- Serialize the complete first fallible state load and migration behind
a dedicated process-local initialization mutex.
- Double-check the OnceLock after acquiring the initializer mutex so
concurrent first callers invoke the loader and any in-place migration
only once.
- Keep the initializer separate from `STATE_FILE_LOCK`, avoiding
lock-order recursion when the loader resolves the active state path.
- Recover the initializer token after mutex poisoning and leave the
OnceLock unset after load errors so later calls can retry.
- Add deterministic concurrency and failure-retry regressions against
the same helper used by production `state()`.

## Review finding addressed

- P2: serialize first-time state loading and migration.

## Verification

- `cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --check`
- `cargo check --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets`
- `cargo clippy --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets -- -D warnings`
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml` (263
passed, 1 ignored; the ignored multiprocess helper passes in its child
process; 28 binary and 1 integration test passed)
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml
formal_verification_`
- `pkg/tbtc/signer/scripts/run_phase5_chaos_suite.sh` (all 6 scenarios
passed)
- `git diff --check`

CI is authoritative for the TLA+ model and dependency-audit gates.
## Stack

Rust signer review-fix stack 2 of 3.

- Depends on: #4157
- Ultimate base: `extraction/frost-signer-mirror-2026-05-26` (#4005)
- Follow-up: #4159 — startup load serialization

Merge #4157 first, then retarget this PR to the extraction branch.

## What changed

- Introduce a transparent `SecretHex` holder backed by
`Zeroizing<String>` with redacted Debug output and zeroize-on-drop
semantics.
- Use it for DKG secret packages, confidential round-2 packages, and the
long-lived native FROST key package signing share.
- Preserve the existing JSON string wire shape and FFI ABI while
avoiding independent unwiped Rust-owned secret string allocations.
- Continue wiping decoded and serialized byte buffers and returned FFI
buffers through their existing paths.

## Review finding addressed

- P2: treat DKG package fields as secret-bearing types.

Caller-owned FFI request memory remains the caller responsibility;
Rust-owned request/result allocations and returned buffers now have
explicit wipe paths.

## Verification

- `cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --check`
- `cargo check --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets`
- `cargo clippy --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets -- -D warnings`
- DKG serde, Debug-redaction, zeroize-on-drop, distributed-DKG
persistence, and FFI round-trip tests
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml`
- `git diff --check`

The TLA+ runner and `cargo-deny` are unavailable locally; CI remains
authoritative for those gates.
## Stack

Rust signer review-fix stack 1 of 3.

- Base: `extraction/frost-signer-mirror-2026-05-26` (#4005)
- Follow-up: #4158 — DKG secret zeroization
- Final: #4159 — startup load serialization

## What changed

- Require Aggregate attempt IDs to be canonical SHA-256 hex, and bind
fixed-size Aggregate authorization to the validated attempt, signing
package, and taproot root.
- Allow the elected coordinator to aggregate a validated attempt without
requiring that coordinator to be part of the frozen signing subset.
- Keep consumed markers readable by the immediately previous schema-1
binary, and pin a retired session for the full unlocked Aggregate
operation.
- Reclaim completed per-message sessions through bounded retirement
while keeping every persisted schema-1 snapshot within the legacy total
`TBTC_SIGNER_MAX_SESSIONS` limit.
- Reserve new session slots by transactionally evicting the oldest
eligible retired tombstone; protect pending and in-flight Aggregate
sessions, and restore evicted replay state after pre-replacement
Build/DKG/Open failures.
- Persist the first per-message Open binding before success, and make
Abort plus full or partial TTL expiry crash-durable. Post-replacement
uncertainty remains fail-closed behind a repair marker.
- Stage retirement before registering a post-rename Aggregate repair, so
either the exact retry or an unrelated full-state writer durably covers
completion and retirement before clearing pending protection.
- Compact and immediately rewrite oversized intermediate schema-1 state
during load so emergency rollback remains available.

## Review findings addressed

- P1: reclaim per-message sessions after completion.
- P2: validate Aggregate attempt IDs before persistence.
- P1: allow non-signing coordinators to aggregate.
- P1: preserve consumed markers across binary rollback.
- P2: pin sessions while aggregation is in flight.
- P1: preserve the previous total-session bound on disk.
- P2: make Abort-driven retirement crash-durable.
- P2: retire the session after an Aggregate repair.

Retired per-message tombstones use only the unoccupied portion of the
shared legacy-compatible total session budget. Live wallet/DKG sessions
are never classified as retireable.

## Verification

- `cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --check`
- `cargo check --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets`
- `cargo clippy --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets -- -D warnings`
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml` (259
passed, 1 ignored; the ignored multiprocess helper passes in its child
process; 28 binary and 1 integration test passed)
- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml
formal_verification_`
- `pkg/tbtc/signer/scripts/run_phase5_chaos_suite.sh` (all 6 scenarios
passed)
- `git diff --check`

CI is authoritative for the TLA+ model and dependency-audit gates.
## Stack

1 of 3. Base: `extraction/frost-signer-mirror-2026-05-26` (#4005). Next:
#4161.

## Summary

- Normalize one-component signer state paths to `.` for directory
creation, fsync, and corrupt-backup enumeration.
- Make completed canary rollback retries true no-ops for rollout state,
config version, metrics, and promotion evidence.
- Repair uncertain post-rename directory durability on a restarted
rollback retry without rewriting the state file; keep fresh
missing-state no-ops side-effect free.

## Review findings addressed

- P1: bare state paths used an empty parent for directory operations.
- P2: completed canary rollback retries were not idempotent.

## Validation

- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml` on
this branch tip: 270 library tests passed, 1 ignored; 28
admission-checker tests passed; 1 integration test passed.
- Fault-injection regressions cover pre/post-rename behavior, restarted
retry repair, and no state rewrite on idempotent retry.
## Stack

2 of 3. Parent: #4160. Next: #4162.

## Summary

- Reject unknown top-level fields when deserializing
`AdmissionPolicyV1`.
- Keep supported policy files compatible while turning misspelled
security controls into explicit input errors instead of omitted limits.
- Add a malicious typo regression and a supported-field positive
control.

## Review finding addressed

- P2: unknown admission-policy fields could silently disable enforcement
and fail open.

## Security invariant

A policy field that the checker does not recognize cannot silently alter
or weaken admission behavior.

## Validation

- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml` on
this branch tip: 270 library tests passed, 1 ignored; 30
admission-checker tests passed; 1 integration test passed.
- The real checker rejects a misspelled `max_operator_per_provider`
field and accepts the shipped policy sample.
## Stack

3 of 3. Parent: #4161.

## Summary

- Track each live interactive signing member from its last accepted
activity using monotonic `Instant` state instead of wall-clock Open
time.
- Refresh activity for exact Open retries, accepted Round1 calls, and
retry-preserving Round2 failures; rejected traffic does not extend
lifetime.
- Add a forward-only test clock and regressions for active-at-boundary
attempts, idle siblings, invalid requests, retry-preserving failures,
expiry, and reset behavior.

## Review finding addressed

- P2: interactive inactivity TTL was measured from Open and could sweep
an attempt used moments earlier.

## Lifecycle invariant

Only accepted or retry-preserving member activity extends the TTL;
successful terminal operations and post-replacement failures still
retire the nonce handle.

## Validation

- `cargo test --locked --manifest-path pkg/tbtc/signer/Cargo.toml`: 273
library tests passed, 1 ignored; 30 admission-checker tests passed; 1
integration test passed.
- `cargo check --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets`.
- `cargo clippy --locked --manifest-path pkg/tbtc/signer/Cargo.toml
--all-targets -- -D warnings`.
- `cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --check` and
`git diff --check`.
- `pkg/tbtc/signer/scripts/run_phase5_chaos_suite.sh`: all 6
fault-injection scenarios passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants