FROST/ROAST readiness branch#3866
Conversation
## Summary - cut over the `frost_tbtc_signer` bootstrap path to return coarse tbtc-signer signature output on successful `RunDKG -> StartSignRound -> FinalizeSignRound` - keep legacy signing fallback only for verified coarse-path failures (bridge errors, decode failures, or structural divergence) - wire `BuildTaprootTx` through the transitional native tbtc-signer orchestration path - gate `BuildTaprootTx` signing substitution on strict native-vs-legacy transaction input/output equivalence checks - add coarse success/fallback telemetry and observer-registration guards - expand unit and integration coverage for coarse cutover, retry/attempt-variation behavior, and `BuildTaprootTx` substitution safety ## Stack Context - base branch: `feat/frost-schnorr-migration-scaffold` (`#3866`) - recommended review order: 1. review `#3866` for scaffold/runtime seams 2. review this PR as the cutover + hardening delta ## Review Guide (hot paths) - coarse cutover + fallback semantics: - `pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go` - `pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go` - `BuildTaprootTx` wiring and substitution gating: - `pkg/tbtc/wallet.go` - `pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go` - `pkg/bitcoin/transaction_builder.go` - coverage for tx assembly/substitution and bridge safety: - `pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go` - `pkg/bitcoin/transaction_builder_test.go` - `pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go` ## Scope Boundaries - in scope: bootstrap/coarse-path cutover hardening and safe `BuildTaprootTx` integration - out of scope: full production signer-runtime replacement and later migration phase gates
|
Readiness evidence update for the tBTC Schnorr FROST/ROAST migration stack, 2026-05-20. From a clean worktree at PR head go test -timeout 20m -tags 'integration frost_native frost_tbtc_signer' ./pkg/frost/... ./pkg/tbtcObserved package coverage:
This narrows the keep-core evidence gap for FROST/tBTC focused package behavior, but it is not a production-readiness substitute for full keep-core integration/testnet coverage. The following remain open blockers for the tBTC FROST/ROAST readiness gate:
The corresponding tBTC evidence docs were pushed in tlabs-xyz/tbtc#402. |
… at init (#3958) ## Summary Addresses three FFI-safety findings from an independent review of #3866: - **H3 (init-time panic)**: `RegisterNativeExecutionFFISigningPrimitiveForBuild` and `registerNativeExecutionAdapterForBuild` (frost_native) panic on registration failure. Both are invoked from `pkg/frost/signing/native_adapter_registration.go`'s package `init()`, so a transient registration failure crashes the binary at startup. Downstream code (`pkg/frost/signing/backend.go`) already returns `ErrNativeCryptographyUnavailable` when no native adapter is registered, so the legacy execution backend remains the safe-by-default path — panicking at init turned a recoverable degradation into an outage. Replace panics with structured `logger.Warnf` plus a package-level `lastRegistrationError` and `LastNativeRegistrationError()` accessor. Callers that want to fail startup on a registration error can opt in by checking that accessor after `RegisterNativeExecutionAdapterForBuild`; default callers continue booting with the legacy backend, exactly as if `frost_native` was never enabled. The existing `TestRegisterNativeExecutionFFISigningPrimitiveForBuild_ProviderErrorPanics` becomes `..._ProviderErrorIsRecordedNotPanicked` and asserts the new behavior. - **M1 (nil ptr free)**: `parseBuildTaggedTBTCSignerResult` unconditionally deferred `C.tbtc_signer_free_buffer(result.buffer.ptr, result.buffer.len)` even when the C wrapper's status-code -1 path returned `result.buffer.ptr == NULL`. The C wrapper checks the `frost_tbtc_free_buffer` symbol for NULL but does not check the buffer pointer, so a future Rust-side change that dereferenced its ptr argument without a NULL guard would crash. Skip the defer when `result.buffer.ptr == nil`. - **M6 (unbounded length)**: `unmarshalSignerMaterialFromPersistence` accepted any uvarint length within the data buffer. A corrupted state file or hostile peer carrying a multi-hundred-MiB envelope would allocate that many bytes before the existing bounds check ran. Cap the format length at 256 bytes and the payload length at 256 KiB — comfortably above any real signer material envelope — and reject earlier with a clear error. New regression tests `TestUnmarshalSignerMaterialFromPersistence_RejectsOversizedFormatLength` and `..._RejectsOversizedPayloadLength`. ## Out of scope (deferred) The remaining placeholder-fencing findings from the same review (H1: \`KeyGroupSource == \"legacy-wallet-pubkey\"\` fallback; H2: DKG placeholder participant pubkeys; H4: silent key-group substitution when source is legacy) require maintainer policy alignment on whether to gate the \`frost_tbtc_signer\` build behind an opt-in flag or refuse-by-default. Not included here. Several MED findings around Bitcoin witness preservation, FROST message channel back-pressure, and replay-error string matching also require behavior decisions and are not included in this safety-hygiene slice. ## Verification Local (GOCACHE under \`/private/tmp\`): - \`go test ./pkg/frost/...\` — PASS - \`go test -tags 'frost_native frost_tbtc_signer' ./pkg/frost/...\` — PASS - \`go test ./pkg/tbtc -run 'TestUnmarshalSignerMaterial|TestMarshalSigner|TestSignerMarshalling|TestFuzzDecodeNativeSignerMaterial'\` — PASS - \`go test -tags 'frost_native frost_tbtc_signer' ./pkg/tbtc -run 'TestConfigureFrostSigningBackend|TestNewNode_ConfiguresFrostSigningBackend|TestSigningExecutor_Sign|TestRegisterSignerMaterialResolverForBuild'\` — PASS - \`go vet ./pkg/frost/... ./pkg/tbtc\` — clean
… message hygiene (#3959) ## Summary Bundles four findings from the independent PR #3866 review that all sit in the same code seam (frost_native scaffold path + receive loops). Stacked on #3958. ### H1+H4 — scaffold key-group must be opt-in (was silently accepted) \`signer_material_resolver_build_frost_native_tbtc_signer.go\` built signer material with \`KeyGroupSource: \"legacy-wallet-pubkey\"\` (a sha256 placeholder, not a DKG output) and the FFI primitive in \`native_ffi_primitive_transitional_frost_native.go\` silently substituted the Rust signer's RunDKG key group when the source was that placeholder. Production deployments with placeholder material would have signed through whatever key group the Rust side returned without operator-facing signal. Add a refuse-by-default opt-in: \`KEEP_CORE_FROST_TBTC_SIGNER_ACCEPT_SCAFFOLD_KEY_GROUP=1\`. The new \`signing.AcceptScaffoldKeyGroupEnabled\` helper is per-call (not cached), so flipping the env unset recovers fail-closed behavior without restart. Both the resolver and the FFI primitive check the flag; both refuse with a clear error that names the env var and the placeholder source. New regression test pins the refuse-by-default path; existing scaffold-using tests opt in via \`t.Setenv\`. ### M2+M3 — Bitcoin witness restoration refuses unsupported shapes \`ReplaceUnsignedTransaction\`'s restoration path handled only single-element previous witnesses (P2WSH redeem script). Multi-element witnesses (P2TR script-path) were silently dropped. Replace with an explicit switch: 0 elements → leave empty, 1 → restore as before, ≥2 → fail loudly. Removes the tautological inner \`len(replacedInput.X) == 0\` checks that the outer refusals already guarantee. New regression test \`TestTransactionBuilder_ReplaceUnsignedTransaction_RejectsMultiElementPreviousWitness\`. ### M5 — first-write-wins on peer messages Three round-message receive loops (tbtc-signer contribution, FROST round one, FROST round two) did last-write-wins, letting a peer mutate its own contribution after first send. Switch to first-write-wins with byte-equal retransmissions idempotent and conflicting retransmissions logged via a new \`protocolLogger\` channel. Three message-equality helpers cover the three message types. ## Out of scope (deferred to separate PRs) - **H2** — DKG placeholder participant pubkeys (\`buildTaggedTBTCSignerDKGPlaceholderPublicKeyHex\`) needs either wiring real \`MembershipValidator\` pubkeys through or fencing under the same env flag. - **M4** — ROAST-compliant bounded transition evidence for the non-blocking message channel. Multi-PR effort. - **M7** — Real ROAST-aware retry replacing the byte-identical tECDSA shuffle in \`pkg/frost/retry/retry.go\`. Multi-PR effort. - **L5** — FFI status-code semantics for replay detection. Paired with a tbtc-signer follow-up. ## Verification Local (GOCACHE under \`/private/tmp\`): - \`go test ./pkg/frost/... ./pkg/bitcoin\` — PASS - \`go test -tags 'frost_native frost_tbtc_signer' ./pkg/frost/... ./pkg/bitcoin\` — PASS - \`go test -tags 'frost_native frost_tbtc_signer' ./pkg/tbtc -run 'TestConfigureFrostSigningBackend|TestNewNode_ConfiguresFrostSigningBackend|TestSigningExecutor_Sign|TestRegisterSignerMaterialResolverForBuild|TestBuildTaggedTBTCSignerRoundKeyGroup|TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive|TestTransactionBuilder_ReplaceUnsignedTransaction'\` — PASS
…3962) ## Summary Adds **RFC-21** as the design doc that scopes the M4 (transition evidence) and M7 (ROAST-aware retry) findings from the independent review of #3866 into a single layered design and a phased, PR-sized implementation plan. This PR is **doc-only**. It introduces no behaviour change. Subsequent implementation PRs reference RFC-21 in their descriptions. Stacked on #3961. ## Why one design, not two M4 and M7 share the same notion of *attempt context* and *transition evidence*: - Fixing M4 alone produces evidence that no consumer reads. - Fixing M7 alone gives the consumer nothing to drive retry decisions on. The RFC treats them as one design split into linear phases. ## Phasing - **Phase 0** -- this RFC. - **Phase 1** -- `AttemptContext` type + canonical hash; protocol messages carry attempt-context binding (optional during migration). - **Phase 2** -- receiver overflow tracking (M4 layer A) plumbed through the three `select { default }` drop sites, default no-op. - **Phase 3** -- coordinator state machine: `BeginAttempt`, `RecordEvidence`, `NextAttempt`. Deterministic `(AttemptContext, TransitionEvidence) -> AttemptContext` map. - **Phase 4** -- wire receiver to coordinator behind `frost_roast_retry` build tag. - **Phase 5** -- retry adapter + `EvaluateRoastRetryForSigning`; migrate first call site behind the build tag with readiness-gate guard. - **Phase 6** -- migrate remaining call sites; delete the byte-identical-to-tECDSA shuffle once unused. - **Phase 7** -- flip the readiness manifest to `present` once Phase 6 ships and integration tests run against a real testnet (only then; no early flip). ## Open questions called out explicitly The RFC lists four open design questions that need cross-team review before Phase 3 lands: 1. Cross-process coordinator agreement -- gossip topic choice. 2. Persistence across signer restart. 3. FFI surface (Rust signer error-code style; follows the L5 pattern from #425 / #3961). 4. Backward-compat horizon for the `AttemptContextHash` field. ## Out of scope - DKG retry (separate RFC). - Bitcoin transaction-builder changes. - Operator UX changes (CLI, dashboards) -- land alongside Phase 5/6. - Cross-domain ROAST between keep-core and tbtc-signer. ## Test plan - [ ] Reviewer reads RFC end-to-end. - [ ] Reviewer flags any phase that should be split further or reordered before Phase 1 begins. - [ ] Reviewer answers the four open questions or marks them defer-to-Phase-3. No code change in this PR, so no CI test run is meaningful beyond asciidoc rendering.
…ild tag (#3965) ## Summary Forward-fix for #3866 CI: the Phase 1B binding file and test referenced message types defined in \`//go:build frost_native\` files but were themselves untagged. Untagged staticcheck on the integration branch (#3866) then reported \`undefined: nativeFROSTRoundOneCommitmentMessage\` and the client-lint job failed. Adds \`//go:build frost_native\` to: - \`pkg/frost/signing/attempt_context_binding.go\` - \`pkg/frost/signing/attempt_context_binding_test.go\` The helpers and tests are only exercised by gated code paths (the three message-type methods all live behind \`frost_native\`), so the build tag is the right locus. ## Why now PRs #3963 (Phase 1A) and #3964 (Phase 1B) were merged into the \`feat/frost-schnorr-migration-scaffold\` branch before #3866's integration CI ran. Once the merges landed, #3866's \`client-lint\` job rebuilt under the untagged staticcheck pass and exposed the missing tag. This PR is the smallest possible fix. ## Verification Locally with module-pinned staticcheck 2025.1.1: \`\`\` go build ./... go build -tags 'frost_native frost_tbtc_signer' ./pkg/frost/... go test -tags 'frost_native frost_tbtc_signer' ./pkg/frost/signing/ staticcheck -checks \"-SA1019\" ./... # whole repo, silent staticcheck -checks \"-SA1019\" ./pkg/frost/signing # silent \`\`\` ## Test plan - [ ] CI green: client-lint, client-vet, client-scan, client-build-test-publish all pass. - [ ] #3866 lint job recovers once this merges into \`feat/frost-schnorr-migration-scaffold\`.
…3988) ## Summary Closes the **M4 gap** from the original PR #3866 review by adding the two evidence categories the RFC-21 Phase-2 work left as future work: **validation-rejection evidence** and **first-write-wins-conflict evidence**. With this PR, the \`NextAttempt\` policy can permanently exclude misbehaving peers on all four ROAST blame channels -- transport-overflow, validation-reject, equivocation-conflict, and silence -- instead of just overflow + silence. ## Why this matters A peer that only sends **malformed messages** (validation rejects, never overflows the channel) was previously indistinguishable from a silent peer. The transient silence-parking policy would bench-and-reinstate them indefinitely, never permanently excluding the malicious behaviour. Same for a peer **equivocating mid-attempt**: the existing first-write-wins assembly correctly dropped the conflicting retransmission but only logged the event -- the bundle carried no structured evidence the coordinator's policy could act on. ## What lands ### Recorder API | Surface | Notes | |---|---| | \`RecordReject(sender, reason)\` | reason captured verbatim; per-reason quota counter | | \`RecordConflict(sender)\` | saturates at conflict quota | | \`RejectQuotaDefault = 8\`, \`ConflictQuotaDefault = 4\` | matches RFC-21 Layer A categoryQuota | | Per-reason quotas independent | peer cannot saturate one reason to mask another | ### Wire types | Type | Sort order | Cap | |---|---|---| | \`RejectEntry{Sender, Reason, Count}\` | asc by Sender, then asc by Reason | per-attempt evidence size bounded by Σ quotas | | \`ConflictEntry{Sender, Count}\` | asc by Sender | per-attempt evidence size bounded by Σ quotas | Both fields use \`omitempty\` so pre-PR snapshots round-trip without the new fields. \`Validate()\` enforces sorted-ascending invariants. ### NextAttempt policy | Threshold | Value | Source | |---|---|---| | \`RejectExclusionThreshold\` | 1 | RFC-21 Layer B ("any non-transport reject is sufficient cause") | | \`ConflictExclusionThreshold\` | 1 | A single conflict is byzantine evidence | \`computeNextAttempt\` merges \`overflowBlamed\`, \`rejectBlamed\`, \`conflictBlamed\` into the permanent ExcludedSet. The \`blamedSenders\` helper is factored out so all three categories share the deterministic sort + threshold-comparison logic. ### Receive-loop wiring Three reject sites and three conflict sites updated across the two files that house the three FROST/tbtc-signer receive loops: | Site | Was | Now | |---|---|---| | \`shouldAcceptNativeFROSTMessage\` returns false | silent drop | \`evidence.RecordReject(senderID, "validation_gate_rejected")\` + drop | | First-write-wins conflict in assembly loop | warn log only | \`evidence.RecordConflict(senderID)\` + warn log | ## Test coverage (15 new cases) - 7 recorder tests: accumulation, per-reason quota saturation, per-reason independence, conflict saturation, all-categories-present, NoOp-inert, RFC-constant assertions - 5 policy tests: single reject excludes, single conflict excludes, reject+conflict on different senders, empty evidence (sanity), threshold-constant assertions - Receive-loop wiring is covered indirectly by the recorder unit tests; the NoOp default keeps pre-RFC-21 receive semantics observably unchanged so no integration-level test is required. ## Verification | Command | Result | |---|---| | \`go build ./...\` + \`go build -tags 'frost_native frost_tbtc_signer frost_roast_retry' ./...\` | both clean | | \`go test ./pkg/frost/...\` + race | pass | | \`go test -tags 'frost_native frost_tbtc_signer frost_roast_retry' ./pkg/frost/...\` | pass (5 packages) | | \`staticcheck -checks '-SA1019' ./pkg/frost/...\` | silent | | \`go vet ./pkg/frost/...\` + \`gofmt -l ./pkg/frost/\` | clean | ## RFC-21 status With this PR, all four ROAST evidence categories are operational. M4 from the original PR #3866 review is **fully closed**. The keep-core code arc for RFC-21 is now feature-complete; remaining work is operations-side (integration testnet, manifest flip). ## Test plan - [ ] CI green. - [ ] Reviewer confirms the per-reason quota independence is the right semantics (alternative: single per-sender reject counter). - [ ] Reviewer confirms threshold = 1 for both reject and conflict (alternative: higher to absorb noise; trade-off is faster vs slower exclusion of misbehaving peers).
#3993) ## Why The RFC-21 Phase 6 review decided which orchestration errors are fallback-eligible (static config errors → safe to fall back to legacy retry path) and which must hard-fail (runtime per-attempt errors → no fallback, since per-participant divergence creates split-brain group fracture). The rationale lived in commit messages, the RFC text, and inline comments on individual sentinels — distributed enough that a future maintainer reading just \`roast_retry_orchestration.go\` could miss the load-bearing constraint. This PR adds a top-of-file design-rationale block that centralises the decision in the place that enforces it. ## What changed - One file changed: \`pkg/frost/signing/roast_retry_orchestration.go\` - Pure documentation: no behavior change, no test changes, no API change - 49 lines added (one comment block) ## What it captures 1. **STATIC vs RUNTIME classification** — explicit definitions, with the sentinel (\`ErrNoRoastRetryCoordinatorRegistered\`) and detection mechanism (\`errors.Is\` in \`signing_loop_roast_dispatcher.go\`) named. 2. **Why static-error fallback is safe** — every honest signer observes the same node-local config at startup, so the fallback decision is deterministic across the group. 3. **Why runtime-error fallback is unsafe** — per-attempt protocol state errors can be observed by some participants and not others within the same attempt; fallback would put some operators on new code and others on legacy for the same attempt. 4. **Enforcement rule** — any error surfaced from this package that is intended to permit fallback MUST be the sentinel; wrapping ANY runtime error in the sentinel is a safety regression that PR reviewers should reject. 5. **Historical redirect** — the earlier design had \`BeginAttempt\` failures fall back, on the assumption that BeginAttempt was cheap idempotent setup. Review identified that BeginAttempt mutates per-attempt state and can fail from races with concurrent receives; the taxonomy was tightened so only true configuration errors are fallback-eligible. ## Lineage Surfaced in the cross-PR review re-evaluation following PR #3866 follow-up landings. Originally tracked as "Document static-vs-runtime classification canonically" — initially flagged as "available if you want," now elevated because the rationale was the most important architectural decision in the RFC-21 stack and is currently the easiest piece of design context to lose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary - add FROST WalletRegistry and FrostDkgValidator bindings plus config and chain attachment - implement v4 FROST DKG result digest assembly with full vs active member types and fixture-backed parity tests - add the native FROST DKG engine boundary, P2P round protocol, result signing, coordinator lifecycle, challenge monitoring, and wallet ID handling for x-only output keys ## Notes - Stacked on #3866 / `feat/frost-schnorr-migration-scaffold`. - Runtime DKG still requires the concrete native DKG engine registration from the frost-uniffi-sdk UDL/Rust export work. - The digest fixture now records the tBTC TypeScript generator source and regeneration command. A paired tBTC PR should still commit the mirror fixture at `docs/test-vectors/frost-dkg-result-digest-v1.json` and add the TS-side emitter/test; until then, the keep-core test verifies the pinned bytes and metadata but does not compare against a checked-in tBTC mirror file. ## Validation - `go test ./pkg/frost/registry ./pkg/chain/ethereum ./pkg/chain/ethereum/frost/gen/...` - `go test ./pkg/tbtc -run "TestFrostDKGSignatureThreshold|TestBoundedFrostDKGRecoveryStartBlock|TestFrostDKGRecoveryLookBackBlocks" -count=1` - `go test -tags "frost_native frost_tbtc_signer" ./pkg/tbtc -run "TestLowestLocalActiveMemberIndex|TestFrostMisbehavedMemberIndices|TestFrostDKGSignatureThreshold|TestBoundedFrostDKGRecoveryStartBlock|TestFrostDKGRecoveryLookBackBlocks" -count=1` - CI `client-build-test-publish` passes on the prior pushed commit; rerunning for the latest follow-up commit after push. ## Local Note - Full local `go test ./pkg/tbtc` currently fails in standalone `TestWatchCoordinationWindows`; this reproduces when run by itself and appears unrelated to the FROST DKG coordinator changes.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
## Summary Stacked on #3866. This PR implements Taproot-native key-path wallet signing for the FROST migration path. It adds P2TR script handling, BIP341 SIGHASH_DEFAULT computation, BIP340 Schnorr signature verification, and single-element Taproot witness application in the Bitcoin transaction builder. The wallet transaction executor now routes all-P2TR transactions through the Schnorr/Taproot witness path. Mixed Taproot plus legacy inputs are rejected before signing, so this does not introduce a dual-signing model. ## Details - Add P2TR script helpers and x-only output key extraction. - Add Taproot key-path sighash generation without a repo-wide btcd upgrade. - Add `AddTaprootKeyPathSignatures` for 64-byte BIP340 signatures. - Preserve canonical 32-byte FROST signing messages when `big.Int` strips leading zero bytes. - Add builder and wallet tests covering all-P2TR signing and mixed-input rejection. ## Validation - `go test ./pkg/bitcoin ./pkg/tbtc` - `go test -tags=frost_native ./pkg/frost/signing`
Fold of two Codex #4101 P2 findings: - P2-1 (suppress outer fallbacks): the interactive-only guard lived only inside the FFI adapter, so when the native FFI path was unavailable (ErrNativeCryptographyUnavailable before the adapter's guard) the OUTER buildTaggedNativeExecutionBridge/Adapter still delegated to the legacy backend, because nativeExecutionFallbackAllowed() stayed true. Gate that single function on the flag: interactive-only now returns false there, closing every outer legacy/coarse fallback (the bridge + adapter consult it before delegating). New backend test asserts the suppression. - P2-2 (terminal classification): the adapter's refusal returned a plain error, so the tBTC signingRetryLoop (which only aborts on ErrTerminalSigningFailure) treated this deterministic configuration failure as retryable and spun to timeout. Wrap the refusal with %w ErrTerminalSigningFailure; the adapter test now asserts errors.Is. Also folds my own review's scope notes into the gate doc: interactive-only is format-agnostic (refuses coarse for every signer format the native executor handles), closes both the inner FFI primitive and the outer fallbacks, and fails all native signing closed in a build without the interactive engine - so enable it only on a frost_native node with the audit gate on. Builds across all tag combos; full default + frost_native/frost_roast_retry suites pass; gofmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…coarse flag Fold of a third round of Codex #4101 P2 findings - the flag's fail-closed behavior still had two holes, both now enforced at the backend Execute (the action) so they cannot be bypassed by a caller: - The DEFAULT backend fails OPEN. KEEP_CORE_FROST_INTERACTIVE_SIGNING_ONLY was checked only in nativeExecutionFallbackAllowed + the native FFI adapter. A node left at the documented default (""/legacy) signs straight through legacyExecutionBackend.Execute (the tECDSA/coarse signer), never touching those guards - so the safety switch failed open under the default config. legacyExecutionBackend.Execute now refuses with a terminal error when the flag is on. - Outer native refusals were retryable. When the native path is unavailable before the FFI adapter's terminal refusal can run (no FFI executor, or the bridge returns ErrNativeCryptographyUnavailable with the fallback suppressed), the bridge/adapter return a bare ErrNativeCryptographyUnavailable; the tBTC signingRetryLoop only aborts on ErrTerminalSigningFailure, so it retried this deterministic failure to timeout. nativeExecutionBackend.Execute now promotes that unavailable error to terminal when the flag is on (and leaves it untouched when off). Tests: legacy terminal refusal; native unavailable->terminal promotion plus a flag-off pass-through (no regression). Builds across all tag combos; full default + frost_native/frost_roast_retry suites pass; gofmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ult off) (#4101) ## What The **reversible, un-gated** first half of coarse-path retirement (RFC-21 Phase 7.3). Adds a default-**off** `KEEP_CORE_FROST_INTERACTIVE_SIGNING_ONLY` gate; when set, the executor **refuses** to fall through to the coarse signing primitive — if interactive signing did not run (its audit gate off, or no engine registered), the attempt fails **closed** rather than silently signing over the retired coarse path. Only the `(nil signature, nil error)` "interactive not enabled → coarse" fall-through becomes a refusal; the hard-fail on a *committed* interactive failure is unchanged. ## Safety / sequencing - **Default off → production unchanged.** Coarse stays the path until an operator flips this on. - Flipping it on **is** the tECDSA→FROST cutover for that node (no coarse fallback), so it stays off until the `frost-secp256k1-tr` external audit clears and the recovery-leaf decision lands. - The **irreversible** part — deleting the transitional coarse FFI primitive + the deterministic-nonce path — is the deliberate follow-up, not in this PR. ## Tests - `TestEntry_InteractiveOnly_RefusesCoarseFallback` — orchestration active + interactive audit gate off + this flag on → the executor returns a refusal naming the env var, no signature. - `TestEntry_InteractiveSigningOnlyEnabled_ParsesFlag` — flag parsing. - Existing static-fallback executor tests unchanged (flag defaults off). Builds clean across tag combos; vet + gofmt clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… real engine Item 1 of the pre-production handoff (design locked via Codex consult, shape A): the union of the two half-faked e2es. roast_runner_bus_net_e2e proves runner+transport with a FAKE engine; roast_real_cgo_interactive_e2e proves the REAL engine with no runner and no transport. Neither exercises the real-engine <-> real-pkg/net-transport <-> runner seam together. This does: n interactive signing runners, each over its own real pkg/net BroadcastChannel bus, driving the real cgo engine to a real BIP-340 signature, in one process (full-included 2-of-2 and t-of-included 3/threshold-2 subset). Key wiring discovery (no engine change needed): the Go RFC-21 coordinator election seeds on SHA256(dkgGroupPublicKey || sessionID || messageDigest), and the Rust engine seeds the same shuffle on SHA256(keyGroup || sessionID || messageDigest) where keyGroup is the DKG handle string. Passing []byte(keyGroup) as the binding's dkgGroupPublicKey makes the two independent derivations agree, so the runner's engine-vs-binding coordinator cross-check (never before exercised against the real engine) passes. RunDKG persists a resolvable key group, so there is no loadInteractiveKeyGroup gap on this path. Shape (A) shares ONE process-global engine across all seats (ENGINE_STATE is a process-global OnceLock<Mutex>; the multi-seat fix #4098 is what lets one engine serve every local member). The seam surfaced a real, correct boundary: the engine's aggregate completion marker is per-ATTEMPT (attempt_id, message, root), not per-member, so the first seat to aggregate produces the signature and co-resident seats observe interactive_attempt_already_aggregated. That is faithful — in production each node has its own engine — and every seat still drives its FULL transport (commitments + shares broadcast/collected over real pkg/net) before aggregate. Assertion: exactly one seat aggregates a real 64-byte signature and reaches Succeeded; every other seat reached aggregate and hit the per-attempt idempotency marker (no other failure). Skip-guarded (absent/stale libfrost_tbtc skips at the up-front DKG). Verified vs the rebuilt lib: both tests pass, -race -count=3 clean (exactly one winner, no races), skip without the lib, cgo vet + gofmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…crypto tests Item 2 of the pre-production handoff (design locked via Codex consult). CI builds NONE of the frost_native/frost_tbtc_signer/cgo tags, so the real-crypto interactive tests (incl. the multi-node e2e in this PR) are validated locally only and can silently rot. Adds .github/workflows/frost-cgo-integration.yml: it checks out the Go branch, checks out a PINNED mirror commit (ci/frost-signer-pin.env - an explicit SHA, not the moving tip, so an engine/bridge drift fails CI loudly in one reviewed unit instead of silently turning the real-crypto tests into skips), builds libfrost_tbtc, verifies the exported frost_tbtc_* ABI symbols, and runs the cgo-tagged tests with skips FORBIDDEN. The require-cgo gate is KEEP_CORE_FROST_REQUIRE_CGO=true, which flips skipFrostUnavailable's lib-unavailable SKIP to a FATAL - so an absent/stale/unloadable lib fails the job rather than quietly dropping coverage. Linker note (Codex): -Wl,--no-as-needed retains the DT_NEEDED on the lib even though the bridge references it only via dlsym(RTLD_DEFAULT). The Go interactive code and the signer crate live on separate branches today, so the gate checks the pinned mirror commit out into a side path and builds from there; after the branches merge, swap the cross-branch checkout for an in-tree cargo build and keep the gate. Locally validated: require-mode FAILS without the lib (was a skip), SKIPS when unset, PASSES with the lib; the workflow YAML parses (7 steps); the verified ABI symbols are present in the built lib. A CI workflow can only be fully exercised by running it on a PR. Production binary packaging + a structured ABI-version assertion are deliberately left as follow-ups (release pipeline / mirror), noted in the handoff. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The interactive signing runner surfaces outcomes only via return values - no logs or
metrics - so before the coordinated tECDSA->FROST flip there is no operator-visible
signal for "is interactive signing working, and is anything failing closed?". Adds
process-wide cumulative counters following the roast_retry_metrics.go pattern:
- frost_interactive_signing_success_total / _failure_total: whether the interactive
path, once driven, produces signatures or fails on a committed attempt. Emitted at
the single executor-entry chokepoint where the drive's (signature, error) outcome
is interpreted (the inactive fall-through - both nil - counts as neither).
- frost_interactive_signing_coarse_fallback_refused_total: the FLIP-SAFETY signal -
increments whenever no-coarse-fallback mode (KEEP_CORE_FROST_INTERACTIVE_SIGNING_ONLY)
terminally refuses a would-be coarse/legacy signing, at all three #4101 refusal
sites (legacy backend, native backend promotion, FFI adapter). During a staged flip
a misconfigured node - interactive-only on but interactive signing not running -
shows up here instead of silently failing every attempt.
RegisterInteractiveSigningMetrics mirrors RegisterRoastRetryMetrics; operators wire it
at startup (alongside the existing one, when the registration wiring lands). Emitted in
every build, stays at zero until the gated interactive path runs - inert by default.
This is the one genuinely buildable, non-gated item from the handoff's §6 production-
integration review: §6.1 (signer material) is mostly already built (DKG+persistence;
resolver fail-closed is tested), §6.2 activation wiring + §6.4 wallet lifecycle are
gated, and the rest of §6.3 is ops/runbooks.
Builds across all tag combos; new tests pass (incl. an end-to-end legacy-backend
refusal bumping the counter); no regression; cgo vet + gofmt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… startup
Self-review fold (the one valid finding; Codex + Gemini both approved the PR with no
findings): the counters incremented internally but RegisterInteractiveSigningMetrics
was never called at startup - so they never reached the Prometheus scrape, leaving the
PR's "operator-visible signal" latent. (The pre-existing RegisterRoastRetryMetrics had
the same gap.)
Wire both Phase 7.3 metric families at the existing tbtc.Initialize clientInfo
registration site (right beside ObserveApplicationSource("tbtc", ...)). This is inert:
the sources report zero until the gated interactive path runs, and registering a scrape
callback does not activate any signing behavior - safe regardless of the cutover gates.
Wiring both (not only the new one) avoids leaving the sibling roast-retry counters
unexposed next to a freshly-exposed interactive family.
Builds clean (default + frost_native); vet + gofmt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…off Items 1+2) (#4102) Pre-production handoff **Items 1 and 2**, bundled because the CI gate's whole purpose is to run the new e2e (and the gate's require-mode enforcement lives in the e2e's helper file). ## Item 1 — real-crypto multi-node e2e The union of the two half-faked e2es — `roast_runner_bus_net_e2e` (real runner+transport, **fake** engine) and `roast_real_cgo_interactive_e2e` (**real** engine, no runner/transport). Neither exercises the **real-engine ⇄ real-`pkg/net`-transport ⇄ runner** seam together; this does: n runners, each over its own real `pkg/net` bus, driving the real cgo engine to a **real BIP-340 signature** in one process (full-included 2-of-2 and t-of-included 3/threshold-2 subset). - **Wiring discovery (no engine change):** the Go RFC-21 election seeds on `SHA256(dkgGroupPublicKey ‖ sessionID ‖ messageDigest)` and the Rust engine on `SHA256(keyGroup ‖ …)`; passing `[]byte(keyGroup)` as the binding's `dkgGroupPublicKey` makes them agree, so the runner's engine-vs-binding coordinator cross-check — **never before run against the real engine** — passes. - **Faithful shared-engine boundary surfaced by the seam:** one process-global engine ⇒ the per-attempt aggregate idempotency marker lets exactly one seat aggregate the signature; co-resident seats observe `interactive_attempt_already_aggregated` *after* completing their full transport. In production each node has its own engine. Asserted precisely (exactly one winner + Succeeded; all others reached aggregate and hit the marker). ## Item 2 — pre-merge cgo CI gate `.github/workflows/frost-cgo-integration.yml`: checks out the Go branch + a **pinned** mirror commit (`ci/frost-signer-pin.env`), builds `libfrost_tbtc`, verifies exported `frost_tbtc_*` symbols, and runs the cgo-tagged tests with **skips forbidden** (`KEEP_CORE_FROST_REQUIRE_CGO=true` flips `skipFrostUnavailable`'s lib-unavailable skip → fatal). An explicit SHA pin (not the moving tip) makes an engine/bridge drift fail loudly in one reviewed unit instead of silently dropping coverage. `-Wl,--no-as-needed` retains the `DT_NEEDED` under the bridge's `dlsym(RTLD_DEFAULT)` model. ## Validation - Item 1: against the rebuilt lib both tests pass, `-race -count=3` clean (exactly one winner, no races), skip without the lib, cgo vet + gofmt clean. - Item 2: require-mode **fails** without the lib (was a skip), **skips** when unset, **passes** with the lib; workflow YAML parses (7 steps); the verified ABI symbols are present in the built lib. - **A CI workflow can only be fully exercised by running it on a PR** — the require-mode mechanism + YAML are locally validated; the job itself runs here. ## Deliberately deferred (noted in the handoff) - **Production binary packaging** (ship `.so` alongside `keep-client`, rpath, fail-closed startup preflight) — release-pipeline / MacLane. - **Structured ABI-version assertion** (the current `frost_tbtc_version` string isn't an ABI contract) — mirror. > CI does not otherwise build the cgo tags; without this gate the path is inert in CI. The gate is the fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
#4103) ## What The one genuinely **buildable, non-gated** item from the handoff's §6 production-integration review. The interactive signing runner surfaces outcomes only via return values — no logs or metrics — so before the coordinated tECDSA→FROST flip there's no operator-visible signal for *"is interactive signing working, and is anything failing closed?"* Adds process-wide cumulative counters following the existing `roast_retry_metrics.go` pattern: - **`frost_interactive_signing_success_total` / `_failure_total`** — whether the interactive path, once driven, produces signatures or fails on a committed attempt. Emitted at the **single executor-entry chokepoint** where the drive's `(signature, error)` outcome is interpreted (the inactive fall-through — both nil — counts as neither). - **`frost_interactive_signing_coarse_fallback_refused_total`** — the **flip-safety signal**: increments whenever no-coarse-fallback mode (`KEEP_CORE_FROST_INTERACTIVE_SIGNING_ONLY`) terminally refuses a would-be coarse/legacy signing, at all three #4101 refusal sites (legacy backend, native-backend promotion, FFI adapter). During a staged flip a misconfigured node — interactive-only on but interactive signing not running — shows up here instead of silently failing every attempt. `RegisterInteractiveSigningMetrics` mirrors `RegisterRoastRetryMetrics`; operators wire it at startup (alongside the existing one, when the registration wiring lands). Emitted in every build, stays at zero until the gated interactive path runs — **inert by default**. ## Why this is the only code item here From the §6 review (verified against code, in `FROST_PRODUCTION_HANDOFF.md`): §6.1 signer-material is **mostly already built** (material flows via DKG + persistence; the resolver fail-closed is already tested), §6.2 activation wiring and §6.4 wallet lifecycle are **gated** (audit/recovery-leaf/MacLane), and the rest of §6.3 is **ops/runbooks**. Observability is the one un-gated code slice — and it's exactly what the coordinated flip needs. ## Validation Builds across all tag combos; new tests pass (incl. an end-to-end legacy-backend refusal bumping the counter); no regression in backend/executor-entry suites; cgo vet + gofmt clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…t version The Go-side companion to the mirror's frost_tbtc_abi_version export. The bridge and the lib are linked at runtime via dlsym; a symbol that resolves but changed MEANING is silent corruption. This adds a fail-closed preflight that asserts the linked lib's FFI contract version before any contract-sensitive call. - Pure, fully-unit-tested rule (native_tbtc_signer_abi_version.go, untagged): lib abi_major must EQUAL requiredTBTCSignerABIMajor (1) and abi_minor must be >= requiredTBTCSignerABIMinMinor (0). Default-build tests cover every branch (wrong major either direction, too-old minor, higher-minor-additive-ok) via a parameterized helper. - cgo preflight (native_tbtc_signer_abi_version_frost_native.go): fetches the version via the new frost_tbtc_abi_version FFI, runs ONCE per process (sync.Once - the lib is process-global, not hot-swapped) before the first engine op. MISSING symbol keeps ErrNativeCryptographyUnavailable in the chain (explicit message; lib predates ABI versioning) so it SKIPS in dev and is FATAL under the require-cgo gate like any stale lib; PRESENT-but-wrong (malformed/wrong-major/too-old-minor) is ErrTBTCSignerABIIncompatible and fails loudly ALWAYS. - Gate: callBuildTaggedTBTCSignerOperation (the single chokepoint every request-taking engine op funnels through) runs the preflight first. The no-arg version/abi-version calls bypass it, so no recursion. - Bumps ci/frost-signer-pin.env to the mirror commit that adds frost_tbtc_abi_version (25e4f27), and adds that symbol to the CI gate's verified set, so the gate builds a lib whose ABI the preflight accepts. Design via Codex consult (major.minor; exact major + min minor; init-time fail-closed; explicit missing-symbol error; no upper bound; frozen {abi_major,abi_minor} surface). Builds across tag combos; untagged rule tests + cgo preflight test pass; require-cgo full run green against the ABI lib; no-lib skips; default suite unchanged; vet + gofmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex #4105 P2: the ABI preflight failed closed at the FFI op level, but the coarse engine path (signWithTBTCSignerCoarseEngine) treats engine-op errors as ordinary tbtc-signer failures and calls fallbackTBTCSignerLegacySigning - which, if the material carries a legacy private key share, signs via legacy tECDSA anyway. So an incompatible lib was NOT actually failing closed end to end: the ABI error got swallowed into a legacy signature. Fix at the single chokepoint - the top of signWithTBTCSignerCoarseEngine, before any fallback: a guard that fails closed on ErrTBTCSignerABIIncompatible (a PRESENT-but-wrong lib). A MISSING/absent lib is ErrNativeCryptographyUnavailable, NOT this sentinel, and still falls back to legacy as the transitional design intends - only a responding-but- incompatible lib is refused. The check is a build-split helper (tbtcSignerABIIncompatibility: real cgo preflight when linked, no-op otherwise), indirected through a var (tbtcSignerABIIncompatibilityCheck) so the no-fallback behavior is testable in the frost_native build. New test (mirrors _InvalidAttemptPolicy_DoesNotFallback): injects an incompatible ABI verdict + a valid attempt + a legacy-key-share payload, asserts Sign returns ErrTBTCSignerABIIncompatible, no signature, and ZERO fallback events. Bumps the signer pin to the mirror tip (6e3718b, which also adds the header decl). Builds across all tag combos; new test passes; existing fallback tests unchanged; cgo vet + gofmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The P2-2 fail-closed test (a present-but-incompatible lib must not fall back to legacy signing) is frost_native-tagged and uses the test seam, so it does not need an actually-incompatible lib - but it was matched by neither standard CI (no frost_native tags) nor the cgo gate's TestRealCgoInteractiveSigning -run filter, so its proof ran only locally. This gate is the only CI that builds these tags; broaden -run to include it so the no-fallback guarantee is enforced in CI alongside the real-crypto suite. Verified locally: the broadened -run matches both groups and all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex #4105 re-review P2: Go's json.Unmarshal silently zero-fills a missing field, so a present-but-partial frost_tbtc_abi_version response like {"abi_major":1} left abi_minor at 0 - and since the required minimum minor is also 0, the compatibility check ACCEPTED it. A broken/partial lib could thus bypass the fail-closed guard. Extract the decode into a pure parseTBTCSignerABIVersion (untagged, unit-testable in the default build) that uses pointer fields to distinguish "absent" from a legitimate zero and rejects a payload missing abi_major and/or abi_minor as ErrTBTCSignerABIIncompatible. Malformed JSON is rejected too; extra/unknown fields are still tolerated (an additive minor bump may add fields old bridges ignore). The cgo assertTBTCSignerABICompatible now calls it instead of a lenient inline Unmarshal. Tests: parser accepts valid (incl. present zero minor) + extra fields; rejects missing abi_minor, missing abi_major, empty object, malformed json, non-object. Builds across tag combos; cgo preflight still passes against the real lib; gofmt + vet clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t version (#4105) ## What The Go-side companion to the mirror's `frost_tbtc_abi_version` export (#4104). The bridge and `libfrost_tbtc` are linked at runtime via `dlsym`; a symbol that resolves but changed **meaning** is silent corruption. This adds a **fail-closed preflight** that asserts the linked lib's FFI contract version before any contract-sensitive call. > **Depends on #4104** (the mirror PR adding `frost_tbtc_abi_version`). This PR bumps `ci/frost-signer-pin.env` to that mirror commit (`25e4f277a`), so the CI gate builds a lib whose ABI the preflight accepts. Merge #4104 first (or together). ## Design (Codex-validated) - **Rule** (`major.minor`): lib `abi_major` must **equal** the bridge's required major (a higher major broke something newer; a lower major is too old), and lib `abi_minor` must be **>=** the required minimum minor (additive is backward-compatible; higher minor's extras are ignored). Required = `1.0`. - **Missing symbol** (old/absent lib) → keeps `ErrNativeCryptographyUnavailable` in the chain (explicit message): **skips** in dev, **fatal** under the require-cgo gate — same as any stale lib. - **Present-but-wrong** (malformed / wrong major / too-old minor) → `ErrTBTCSignerABIIncompatible`, **fails loudly always**. A node must not sign through a lib whose contract diverges. - **Once-per-process** preflight (`sync.Once`; the lib is process-global, not hot-swapped), gated at `callBuildTaggedTBTCSignerOperation` — the single chokepoint every request-taking engine op funnels through. The no-arg version calls bypass it (no recursion). ## Tests - Untagged unit tests cover **every branch** of the rule via a parameterized helper (default build, no cgo needed). - cgo preflight test asserts the linked lib is compatible (skips without it). - Validated: builds across tag combos; require-cgo full run green against the ABI lib; no-lib skips; default suite unchanged; vet + gofmt clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…lution # Conflicts: # pkg/chain/ethereum/ethereum.go # pkg/chain/ethereum/ethereum_integration_test.go # pkg/chain/ethereum/tbtc.go
## Summary Fixes a ROAST evidence-retention gap found during the Codex Security scan of PR #3866. `VerifyBundle` used to verify every bundled snapshot signature before checking whether the bundle mutated this node's own previously submitted snapshot. That meant a coordinator-signed bundle carrying a mutated self snapshot returned `ErrSignatureInvalid` before `own_snapshot_mutated_in_bundle` evidence could be emitted. This PR moves the self-observation check immediately after the coordinator bundle signature is verified and before the generic per-snapshot signature loop. The bundle still fails closed, but the submitted-vs-bundled evidence is retained first. ## Validation - `go test ./pkg/frost/roast -run TestVerifyBundle_RetainsMutatedSelfSnapshotEvidenceBeforeSignatureFailure -count=1` - `go test ./pkg/frost/roast -count=1` - `git diff --check origin/feat/frost-schnorr-migration-scaffold...HEAD` ## Notes The new regression test failed before the ordering fix with: `expected ErrCensorshipDetected, got coordinator: bundle[0]: roast: signature is invalid` After the fix, it passes and verifies that `own_snapshot_mutated_in_bundle` evidence is retained.
…uted DKG Two separate-process (RFC-21 "shape B") real-crypto e2es over real libp2p, each re-execing the test binary as N worker processes that each link libfrost_tbtc: - shape-B (roast_shapeb_libp2p_multiproc_e2e): dealer DKG run once in a bootstrap subprocess, the encrypted key group copied into each worker's own state dir; every worker drives the ROAST interactive-signing runner over real libp2p and independently aggregates the same BIP-340 signature (n winners, vs the shared- engine in-process shape-A's one). Proves per-node engine/state isolation + the libp2p outer framing for the runner + ROAST + transport seam. - distributed-DKG (roast_distributed_dkg_libp2p_multiproc_e2e): every worker runs the real distributed FROST DKG (part1/2/3) over libp2p, with round-2 per-recipient secret shares sealed via secp256k1 ECDH + AES-256-GCM (cleartext round-2 over a broadcast bus would let any node reconstruct a peer's share), so each node holds ONLY its own key package; then threshold-signs via the stateless low-level path. Closes the dealer-DKG custody gap end to end. Both run green standalone and under the full -run TestRealCgoInteractiveSigning cgo gate with KEEP_CORE_FROST_REQUIRE_CGO=true (skips forbidden). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uted DKG (#4110) Stacks on the FROST/ROAST readiness branch (#3866). Adds two separate-process ("shape B") real-crypto e2es over **real libp2p**, complementing the in-process shape-A multinode test. Each re-execs the test binary as N worker processes that each link `libfrost_tbtc`. ## What's added - **shape-B** (`roast_shapeb_libp2p_multiproc_e2e_…`): dealer DKG run once in a bootstrap subprocess, the encrypted key group copied into each worker's own state dir; every worker drives the **ROAST interactive-signing runner** over real libp2p and independently aggregates the same BIP-340 signature (n winners, vs the shared-engine shape-A's one). Covers per-node engine/state isolation + the libp2p outer framing for the runner + ROAST + transport seam. - **distributed-DKG** (`roast_distributed_dkg_libp2p_multiproc_e2e_…`): every worker runs the **real distributed FROST DKG** (`part1/2/3`) over libp2p, with round-2 per-recipient secret shares **sealed via secp256k1 ECDH + AES-256-GCM** (cleartext round-2 over a broadcast bus would let any node sum `f_i(j)` and reconstruct a peer's share), so each node holds **only its own key package**; then threshold-signs via the stateless low-level path. Closes the dealer-DKG key-custody gap end to end. ## Verification Both green standalone (stable across repeated runs) and under the full cgo gate, linking `libfrost_tbtc` built from the pinned signer ref (`ci/frost-signer-pin.env` = `6e3718ba0`, unchanged): ``` CGO_ENABLED=1 KEEP_CORE_FROST_REQUIRE_CGO=true \ go test -tags "frost_native frost_tbtc_signer" -run TestRealCgoInteractiveSigning ./pkg/frost/signing/ ``` All 6 real-crypto tests pass together on this branch (4 shape-A + shape-B + distributed-DKG). ## CI The `frost-cgo-integration` gate already exercises both new tests — its `-run 'TestRealCgoInteractiveSigning'` matches them by prefix, and the pinned crate already exports the `dkg_part1/2/3` + low-level sign FFI they use. No workflow change needed. (Heads-up: these are multi-process + gossipsub tests; robust locally via retransmission + warmup, but mesh-convergence time varies — if shape-B ever flakes on a slow runner, move it to a non-blocking job.) ## Note Building the distributed-DKG test confirmed `dkg_part1/2/3` interoperate over the transport — but the node's DKG path (`executeTBTCSignerFROSTDKG`) still uses the dealer `RunDKGWithSeed`. Wiring distributed DKG into the node remains the readiness-gate-blocking work. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Current State (as of 2026-05-17)
This draft PR is the umbrella readiness branch for
feat/frost-schnorr-migration-scaffold.It is being kept current with
mainso it can become a direct merge target if the FROST/ROAST stack is approved for activation.It remains in draft until the remaining phase-gate, governance, and cross-repository readiness items are closed.
Canonical Status Sources
docs/frost-migration/external-repository-tracking.md(intlabs-xyz/tbtc)docs/reviews/frost-roast-production-readiness-2026-05-16.md(intlabs-xyz/tbtc)Latest Refresh
maininto this branch.frost_native.Remaining Cross-Repo Closure Items
Notes