From 33bae6773d5b8d9bf0c5e748b4a5d0ae886d9874 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 19 Jun 2026 19:29:44 -0400 Subject: [PATCH 1/2] feat(frost): register the cgo engine as the interactive signing provider (gated) The cgo buildTaggedTBTCSignerEngine satisfies interactiveSigningEngine but was intentionally NOT registered as the interactive provider, deferred until the blame/evidence bridge + stable ROAST session-key plumbing landed. Those have landed (PR2b-2 + the share-blame wiring, and the roastSessionID plumbing), so register it: registerBuildTaggedNativeFROSTSigningEngine now also calls RegisterInteractiveSigningEngineProvider with a factory returning a fresh cgo bridge handle. This is dev-behind-tags, not a production enablement. Registration alone changes nothing for an operator: the executor still requires the default-off KEEP_CORE_FROST_INTERACTIVE_SIGNING_ENABLED opt-in (read per call), so the interactive path stays dormant until explicitly enabled on a cgo build, and the coarse path remains the fallback. The frost-secp256k1-tr external audit gates the threshold-ECDSA -> FROST CUTOVER in production (turning that opt-in on for real wallets), NOT this registration. Extends the existing cgo registration test to assert the interactive provider is installed (and returns the cgo engine type) after registration. Validated: default + frost_native build/vet unaffected (the file is cgo-tagged); cgo build/vet + the registration test run green; gofmt clean. Co-Authored-By: Claude Opus 4.8 --- ...e_tbtc_signer_registration_frost_native.go | 28 ++++++++++++------- ...c_signer_registration_frost_native_test.go | 17 +++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index e9d5f55a69..6c03583c1f 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -589,16 +589,24 @@ func registerBuildTaggedNativeFROSTSigningEngine() error { // New FROST wallets in this build must use the coarse // `frost-tbtc-signer-v1` material path exclusively. // - // RFC-21 Phase 7.3: this same engine satisfies interactiveSigningEngine, but - // it is intentionally NOT registered as the interactive provider - // (RegisterInteractiveSigningEngineProvider) here yet. Wiring the gated - // interactive ROAST path into production is deferred until the blame/evidence - // bridge + stable ROAST session-key plumbing land AND the frost-secp256k1-tr - // engine external audit clears. Until then the executor's interactive path is - // unreachable in production BY CONSTRUCTION (no provider), on top of the - // default-off KEEP_CORE_FROST_INTERACTIVE_SIGNING_ENABLED gate -- two - // independent barriers, so an operator cannot enable a half-wired interactive - // flow ahead of the blame bridge. Production signs via the coarse path below. + // RFC-21 Phase 7.3: this same engine satisfies interactiveSigningEngine, and + // it IS registered as the interactive provider here. The prerequisites the + // registration waited on have landed -- the f+1 blame/evidence bridge and the + // stable ROAST session-key plumbing -- so the executor may drive the real cgo + // engine through the interactive ROAST path. Registration on its own changes + // nothing for an operator: the executor still requires the default-off + // KEEP_CORE_FROST_INTERACTIVE_SIGNING_ENABLED opt-in (read per call, see + // roast_interactive_signing_gate.go), so the interactive path stays dormant + // until explicitly enabled on a cgo build, and the coarse path remains the + // fallback. The frost-secp256k1-tr engine external audit gates the + // threshold-ECDSA -> FROST CUTOVER in production (turning that opt-in on for + // real wallets), NOT this registration. The provider is a factory: each call + // returns a fresh stateless bridge handle (interactive sessions live + // engine-side, keyed by session id). + RegisterInteractiveSigningEngineProvider(func() interactiveSigningEngine { + return &buildTaggedTBTCSignerEngine{} + }) + return RegisterNativeTBTCSignerEngine(engine) } diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 43c5d38b1c..22468e1a96 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -16,6 +16,7 @@ import ( func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { UnregisterNativeTBTCSignerEngine() t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(ResetInteractiveSigningEngineProviderForTest) err := registerBuildTaggedNativeFROSTSigningEngine() if err != nil { @@ -27,6 +28,22 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { t.Fatal("expected native tbtc-signer engine registration") } + // RFC-21 Phase 7.3: the same registration installs the cgo engine as the + // interactive signing provider, so the executor can obtain a real engine for + // the gated interactive ROAST path. The provider is a factory returning a + // fresh cgo bridge handle; the path itself stays dormant behind the default-off + // KEEP_CORE_FROST_INTERACTIVE_SIGNING_ENABLED opt-in. + interactive := registeredInteractiveSigningEngine() + if interactive == nil { + t.Fatal("expected the interactive signing provider to be registered") + } + if _, ok := interactive.(*buildTaggedTBTCSignerEngine); !ok { + t.Fatalf( + "interactive provider returned %T, want *buildTaggedTBTCSignerEngine", + interactive, + ) + } + _, err = engine.StartSignRound( "session-1", 1, From a533c788fc87a33c8fdfee8a6c4e4292bd40bd74 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 19 Jun 2026 19:48:49 -0400 Subject: [PATCH 2/2] test(frost): make the interactive-provider tests order-independent (fold) Codex review fold on #4096. Registering the cgo engine as the interactive provider made registerBuildTaggedNativeFROSTSigningEngine mutate a package-global provider, and that function runs from the default FFI registration path. Several tests trigger that path, so TestRegisterInteractiveSigningEngineProvider - which asserted "no provider yet" up front - failed under -shuffle / a focused -run when one of them ran first (reproduced: it saw a leaked *buildTaggedTBTCSignerEngine). Codex suggested resetting the provider in the default-registration cleanup; that alone is insufficient (verified: another default-registration test leaks it under a different interleaving). The robust fix is for the asserting test to establish its own precondition: it now resets the interactive provider up front, so it is order-independent regardless of which tests ran before it. Also reset it in TestRegisterNativeExecutionFFISigningPrimitiveForBuild_UsesDefaultProvider's cleanup (symmetric with its existing FFI executor/provider resets), so the test that mutates the global also tidies it. Verified: 10 -shuffle seeds of the cgo Register/Interactive tests pass (was an order-dependent FAIL before); the original repro order passes; non-cgo frost_native unaffected; cgo build/vet + gofmt clean. Co-Authored-By: Claude Opus 4.8 --- ...ive_ffi_primitive_registration_frost_native_test.go | 8 ++++++++ .../roast_interactive_signing_frost_native_test.go | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go index 16d9468b2e..76a4ad4c11 100644 --- a/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go @@ -41,8 +41,16 @@ func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_UsesDefaultProvider( ) { UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() UnregisterNativeExecutionFFIExecutor() + // Under the cgo build the default provider's registration installs the + // interactive signing provider as a side effect, so reset it here too - + // symmetric with the FFI executor/provider - both before (clean slate) and in + // cleanup. Otherwise a later test asserting no interactive provider is set + // (TestRegisterInteractiveSigningEngineProvider) fails under -shuffle or a + // focused -run. Resetting is a no-op on builds that register no provider. + ResetInteractiveSigningEngineProviderForTest() t.Cleanup(UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild) t.Cleanup(UnregisterNativeExecutionFFIExecutor) + t.Cleanup(ResetInteractiveSigningEngineProviderForTest) RegisterNativeExecutionFFISigningPrimitiveForBuild() diff --git a/pkg/frost/signing/roast_interactive_signing_frost_native_test.go b/pkg/frost/signing/roast_interactive_signing_frost_native_test.go index 77246c26c1..62ca2edcd4 100644 --- a/pkg/frost/signing/roast_interactive_signing_frost_native_test.go +++ b/pkg/frost/signing/roast_interactive_signing_frost_native_test.go @@ -44,10 +44,18 @@ func persistedTBTCSignerMaterial( } func TestRegisterInteractiveSigningEngineProvider(t *testing.T) { + // Establish a clean precondition rather than assume one: other tests in this + // package install an interactive provider as a global side effect (e.g. the + // default FFI-provider registration under the cgo build calls + // registerBuildTaggedNativeFROSTSigningEngine), so a bare "no provider yet" + // assertion is order dependent and fails under -shuffle / a focused -run. + // Resetting up front makes this test order-independent regardless of which + // tests ran before it. + ResetInteractiveSigningEngineProviderForTest() defer ResetInteractiveSigningEngineProviderForTest() if got := registeredInteractiveSigningEngine(); got != nil { - t.Fatalf("expected nil engine before registration, got %T", got) + t.Fatalf("expected nil engine after reset, got %T", got) } want := newFakeInteractiveSigningEngine()