Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/frost/signing/roast_retry_orchestration.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ func BeginOrchestrationForSession(
sessionID string,
ctx attempt.AttemptContext,
) (roast.AttemptHandle, func(), error) {
if err := EnsureRoastRetryReadinessOptIn(); err != nil {
return roast.AttemptHandle{}, nil, fmt.Errorf(
"roast orchestration: %w",
err,
)
}
deps, ok := RegisteredRoastRetryCoordinator()
if !ok {
return roast.AttemptHandle{}, nil, fmt.Errorf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func newOrchestrationTestContext(t *testing.T) attempt.AttemptContext {
}

func TestBeginOrchestrationForSession_HappyPath(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, "true")
ResetRoastRetryRegistrationForTest()
ResetSessionHandleRegistryForTest()
t.Cleanup(ResetRoastRetryRegistrationForTest)
Expand Down Expand Up @@ -71,11 +72,14 @@ func TestBeginOrchestrationForSession_HappyPath(t *testing.T) {
}

func TestBeginOrchestrationForSession_ErrorsWhenRegistryEmpty(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, "true")
ResetRoastRetryRegistrationForTest()
ResetSessionHandleRegistryForTest()
t.Cleanup(ResetRoastRetryRegistrationForTest)
t.Cleanup(ResetSessionHandleRegistryForTest)

// Readiness env var is set; the registry is empty -- we expect
// the registry-empty error, not the env-var error.
_, _, err := BeginOrchestrationForSession("session-X", newOrchestrationTestContext(t))
if err == nil {
t.Fatal("expected error when registry is empty")
Expand All @@ -85,7 +89,35 @@ func TestBeginOrchestrationForSession_ErrorsWhenRegistryEmpty(t *testing.T) {
}
}

func TestBeginOrchestrationForSession_ErrorsWhenReadinessOptInUnset(t *testing.T) {
// Explicitly unset, in case the test runner inherits the env var
// from outside.
t.Setenv(RoastRetryReadinessOptInEnvVar, "")
ResetRoastRetryRegistrationForTest()
ResetSessionHandleRegistryForTest()
t.Cleanup(ResetRoastRetryRegistrationForTest)
t.Cleanup(ResetSessionHandleRegistryForTest)

// Even with a registered coordinator, the readiness env var
// short-circuits orchestration. This is the load-bearing safety
// property: production builds with the frost_roast_retry tag
// still cannot enter the orchestration path without an explicit
// operator decision.
RegisterRoastRetryCoordinator(RoastRetryDeps{
Coordinator: roast.NewInMemoryCoordinator(),
Signer: roast.NoOpSigner(),
Verifier: roast.NoOpSignatureVerifier(),
SelfMember: 1,
})

_, _, err := BeginOrchestrationForSession("session-no-optin", newOrchestrationTestContext(t))
if !errors.Is(err, ErrRoastRetryReadinessOptOut) {
t.Fatalf("expected ErrRoastRetryReadinessOptOut, got %v", err)
}
}

func TestBeginOrchestrationForSession_ErrorsWhenCoordinatorNil(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, "true")
ResetRoastRetryRegistrationForTest()
ResetSessionHandleRegistryForTest()
t.Cleanup(ResetRoastRetryRegistrationForTest)
Expand All @@ -108,6 +140,7 @@ func TestBeginOrchestrationForSession_ErrorsWhenCoordinatorNil(t *testing.T) {
}

func TestBeginOrchestrationForSession_PropagatesBeginAttemptError(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, "true")
ResetRoastRetryRegistrationForTest()
ResetSessionHandleRegistryForTest()
t.Cleanup(ResetRoastRetryRegistrationForTest)
Expand Down Expand Up @@ -213,6 +246,7 @@ func TestStartSessionHandleSweeper_IsIdempotent(t *testing.T) {
}

func TestRegisterRoastRetryCoordinator_StartsSweeper(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, "true")
ResetRoastRetryRegistrationForTest()
ResetSessionHandleRegistryForTest()
t.Cleanup(ResetRoastRetryRegistrationForTest)
Expand Down
60 changes: 60 additions & 0 deletions pkg/frost/signing/roast_retry_readiness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package signing

import (
"errors"
"fmt"
"os"
"strings"
)

// RoastRetryReadinessOptInEnvVar is the environment variable name
// operators must set to "true" to opt in to RFC-21 ROAST retry
// activation. The variable is read per call -- not cached -- so an
// operator can flip it during a debugging session without
// restarting the node.
//
// Pattern matches the existing
// KEEP_CORE_FROST_TBTC_SIGNER_ACCEPT_SCAFFOLD_KEY_GROUP env var
// from PR #3960: a build tag enables the code path, an env var
// enables the wiring, both must agree for the feature to be live.
const RoastRetryReadinessOptInEnvVar = "KEEP_CORE_FROST_ROAST_RETRY_ENABLED"

// ErrRoastRetryReadinessOptOut is the error
// EnsureRoastRetryReadinessOptIn returns when the env var is unset
// or set to anything other than "true". Use errors.Is to detect.
var ErrRoastRetryReadinessOptOut = errors.New(
"roast retry readiness: operator opt-in env var is not set to true",
)

// EnsureRoastRetryReadinessOptIn reads the
// RoastRetryReadinessOptInEnvVar environment variable and returns
// nil if it is set to the string "true" (case-insensitive,
// whitespace-trimmed). Returns ErrRoastRetryReadinessOptOut
// otherwise.
//
// Callers in the orchestration layer invoke this before
// RegisterRoastRetryCoordinator so production builds with the
// frost_roast_retry build tag still refuse to wire orchestration
// without an explicit operator decision.
//
// The function is per-call (not cached) so operators can flip the
// env var dynamically during debugging.
func EnsureRoastRetryReadinessOptIn() error {
if !RoastRetryReadinessOptInEnabled() {
return fmt.Errorf(
"%w: set %s=true to enable",
ErrRoastRetryReadinessOptOut,
RoastRetryReadinessOptInEnvVar,
)
}
return nil
}

// RoastRetryReadinessOptInEnabled reports whether the readiness
// env var is currently set to "true". Cheap to call; use this when
// you need a boolean (e.g., to gate a log message) and
// EnsureRoastRetryReadinessOptIn when you need an error.
func RoastRetryReadinessOptInEnabled() bool {
value := strings.TrimSpace(os.Getenv(RoastRetryReadinessOptInEnvVar))
return strings.EqualFold(value, "true")
}
82 changes: 82 additions & 0 deletions pkg/frost/signing/roast_retry_readiness_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package signing

import (
"errors"
"strings"
"testing"
)

func TestEnsureRoastRetryReadinessOptIn_AcceptsTrue(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, "true")
if err := EnsureRoastRetryReadinessOptIn(); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
}

func TestEnsureRoastRetryReadinessOptIn_AcceptsTrueCaseInsensitive(t *testing.T) {
cases := []string{"true", "True", "TRUE", "tRuE"}
for _, value := range cases {
t.Run(value, func(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, value)
if err := EnsureRoastRetryReadinessOptIn(); err != nil {
t.Fatalf("expected nil error for %q, got %v", value, err)
}
})
}
}

func TestEnsureRoastRetryReadinessOptIn_AcceptsTrimmedWhitespace(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, " true ")
if err := EnsureRoastRetryReadinessOptIn(); err != nil {
t.Fatalf("expected nil error for whitespace-padded 'true', got %v", err)
}
}

func TestEnsureRoastRetryReadinessOptIn_RejectsUnset(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, "")
err := EnsureRoastRetryReadinessOptIn()
if !errors.Is(err, ErrRoastRetryReadinessOptOut) {
t.Fatalf("expected ErrRoastRetryReadinessOptOut, got %v", err)
}
if !strings.Contains(err.Error(), RoastRetryReadinessOptInEnvVar) {
t.Fatalf(
"error must mention the env var name to guide operators; got %v",
err,
)
}
}

func TestEnsureRoastRetryReadinessOptIn_RejectsOtherValues(t *testing.T) {
cases := []string{"false", "1", "yes", "TRUE_", "tru", "anything"}
for _, value := range cases {
t.Run(value, func(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, value)
err := EnsureRoastRetryReadinessOptIn()
if !errors.Is(err, ErrRoastRetryReadinessOptOut) {
t.Fatalf("expected error for %q, got nil", value)
}
})
}
}

func TestRoastRetryReadinessOptInEnabled_MirrorsEnsureResult(t *testing.T) {
t.Setenv(RoastRetryReadinessOptInEnvVar, "true")
if !RoastRetryReadinessOptInEnabled() {
t.Fatal("expected true when env var set to true")
}
t.Setenv(RoastRetryReadinessOptInEnvVar, "false")
if RoastRetryReadinessOptInEnabled() {
t.Fatal("expected false when env var set to false")
}
}

func TestRoastRetryReadinessOptInEnvVar_MatchesRFC(t *testing.T) {
const expected = "KEEP_CORE_FROST_ROAST_RETRY_ENABLED"
if RoastRetryReadinessOptInEnvVar != expected {
t.Fatalf(
"env var name drifted: got %q want %q (must match RFC-21 Phase 5)",
RoastRetryReadinessOptInEnvVar,
expected,
)
}
}
Loading