Skip to content

Commit e719210

Browse files
mswilkisonclaude
andcommitted
fix(frost): fail closed at the legacy + native backends under the no-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>
1 parent 5fae0df commit e719210

3 files changed

Lines changed: 86 additions & 1 deletion

File tree

pkg/frost/signing/backend_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,62 @@ func TestNativeExecutionFallbackAllowed_SuppressedByInteractiveOnly(t *testing.T
196196
}
197197
}
198198

199+
func TestLegacyExecutionBackend_InteractiveOnlyRefusesTerminal(t *testing.T) {
200+
// Coarse-path retirement (Codex #4101 P2): the DEFAULT backend is the legacy
201+
// coarse/tECDSA signer, which the native bridge/adapter guards never touch. The
202+
// flag must fail closed here too, terminally, so it cannot fail open under the
203+
// documented default config.
204+
t.Setenv(InteractiveSigningOnlyEnvVar, "true")
205+
206+
_, err := newLegacyExecutionBackend().Execute(
207+
context.Background(), log.Logger("test"), &Request{Message: big.NewInt(1)},
208+
)
209+
if !errors.Is(err, ErrTerminalSigningFailure) {
210+
t.Fatalf("interactive-only must terminally refuse the legacy backend: %v", err)
211+
}
212+
}
213+
214+
type unavailableNativeAdapter struct{}
215+
216+
func (unavailableNativeAdapter) Execute(
217+
context.Context, log.StandardLogger, *Request,
218+
) (*Result, error) {
219+
return nil, ErrNativeCryptographyUnavailable
220+
}
221+
222+
func (unavailableNativeAdapter) RegisterUnmarshallers(net.BroadcastChannel) {}
223+
224+
func TestNativeExecutionBackend_InteractiveOnlyPromotesUnavailableToTerminal(t *testing.T) {
225+
// Coarse-path retirement (Codex #4101 P2): when the native interactive path is
226+
// unavailable and every fallback is suppressed, the outer refusal surfaces as a
227+
// bare ErrNativeCryptographyUnavailable - promote it to terminal so the retry loop
228+
// aborts instead of spinning.
229+
backend, err := newNativeExecutionBackend(unavailableNativeAdapter{})
230+
if err != nil {
231+
t.Fatalf("unexpected backend setup error: %v", err)
232+
}
233+
234+
t.Setenv(InteractiveSigningOnlyEnvVar, "true")
235+
_, err = backend.Execute(
236+
context.Background(), log.Logger("test"), &Request{Message: big.NewInt(1)},
237+
)
238+
if !errors.Is(err, ErrTerminalSigningFailure) {
239+
t.Fatalf("interactive-only must promote the native unavailable error to terminal: %v", err)
240+
}
241+
242+
// With the flag OFF the unavailable error passes through unpromoted (no regression).
243+
t.Setenv(InteractiveSigningOnlyEnvVar, "")
244+
_, offErr := backend.Execute(
245+
context.Background(), log.Logger("test"), &Request{Message: big.NewInt(1)},
246+
)
247+
if errors.Is(offErr, ErrTerminalSigningFailure) {
248+
t.Fatalf("must not promote to terminal when the flag is off: %v", offErr)
249+
}
250+
if !errors.Is(offErr, ErrNativeCryptographyUnavailable) {
251+
t.Fatalf("flag off must pass the native unavailable error through: %v", offErr)
252+
}
253+
}
254+
199255
func TestSetExecutionBackendByName_NativeFailureRestoresPreviousMode(
200256
t *testing.T,
201257
) {

pkg/frost/signing/legacy_backend.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ func (leb *legacyExecutionBackend) Execute(
3131
return nil, fmt.Errorf("request is nil")
3232
}
3333

34+
if InteractiveSigningOnlyEnabled() {
35+
// Interactive-only mode (coarse-path retirement): the legacy backend is the
36+
// tECDSA/coarse signer the flag retires. Refuse at the action itself so the
37+
// safety switch holds even under the DEFAULT backend selection, where the
38+
// native bridge/adapter guards never run. Terminal so the retry loop aborts.
39+
return nil, fmt.Errorf(
40+
"%w: interactive-only signing mode (%s) is set but the legacy (coarse "+
41+
"tECDSA) backend is selected; the coarse path is retired",
42+
ErrTerminalSigningFailure, InteractiveSigningOnlyEnvVar,
43+
)
44+
}
45+
3446
if request.Attempt != nil {
3547
logger.Infof(
3648
"[member:%v] executing FROST signing attempt [%v] "+

pkg/frost/signing/native_backend.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package signing
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
"github.com/ipfs/go-log/v2"
@@ -50,7 +51,23 @@ func (neb *nativeExecutionBackend) Execute(
5051
return nil, fmt.Errorf("request is nil")
5152
}
5253

53-
return neb.adapter.Execute(ctx, logger, request)
54+
result, err := neb.adapter.Execute(ctx, logger, request)
55+
if err != nil &&
56+
InteractiveSigningOnlyEnabled() &&
57+
errors.Is(err, ErrNativeCryptographyUnavailable) &&
58+
!errors.Is(err, ErrTerminalSigningFailure) {
59+
// Interactive-only mode (coarse-path retirement): the native interactive path
60+
// could not produce a signature and every coarse/legacy fallback is suppressed,
61+
// so an outer refusal surfaces as a bare ErrNativeCryptographyUnavailable.
62+
// Promote it to TERMINAL so the tBTC signingRetryLoop aborts immediately rather
63+
// than retrying a deterministic configuration failure until timeout.
64+
return nil, fmt.Errorf(
65+
"%w: interactive-only signing mode (%s) and the native interactive path is "+
66+
"unavailable; refusing the coarse fallback: %v",
67+
ErrTerminalSigningFailure, InteractiveSigningOnlyEnvVar, err,
68+
)
69+
}
70+
return result, err
5471
}
5572

5673
func (neb *nativeExecutionBackend) RegisterUnmarshallers(

0 commit comments

Comments
 (0)