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
12 changes: 3 additions & 9 deletions supernode/cmd/evmigration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,7 @@ func (f *fakeMigrationClient) BroadcastMigrationTx(_ context.Context, msg sdk.Ms
// newMigrationCfg creates a config with tmpDir for tests that need config persistence.
func newMigrationCfg(t *testing.T, keyName, evmKeyName string) *snConfig.Config {
t.Helper()
cfg := &snConfig.Config{}
cfg.SupernodeConfig.KeyName = keyName
cfg := snConfig.CreateDefaultConfig(keyName, "", "testing", "test", "keys", "", "", "")
cfg.SupernodeConfig.EVMKeyName = evmKeyName
cfg.BaseDir = t.TempDir()
return cfg
Expand Down Expand Up @@ -793,13 +792,8 @@ func TestKeyDeleteAfterMigration(t *testing.T) {
func TestConfigUpdateAfterMigration(t *testing.T) {
tmpDir := t.TempDir()

cfg := &snConfig.Config{
SupernodeConfig: snConfig.SupernodeConfig{
KeyName: "mykey",
Identity: "lumera1oldaddr",
EVMKeyName: "evm-key",
},
}
cfg := snConfig.CreateDefaultConfig("mykey", "lumera1oldaddr", "testing", "test", "keys", "", "", "")
cfg.SupernodeConfig.EVMKeyName = "evm-key"
cfg.BaseDir = tmpDir

newAddr := "lumera1newaddr"
Expand Down
2 changes: 2 additions & 0 deletions supernode/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ type LogConfig struct {
}

type StorageChallengeConfig struct {
enabledSet bool `yaml:"-"`

Enabled bool `yaml:"enabled"`
PollIntervalMs uint64 `yaml:"poll_interval_ms,omitempty"`
SubmitEvidence bool `yaml:"submit_evidence,omitempty"`
Expand Down
35 changes: 19 additions & 16 deletions supernode/config/config_lep6_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@ import (
"time"
)

func TestLoadConfig_LEP6SafeDefaults(t *testing.T) {
func TestLoadConfig_LEP6DefaultEnabled(t *testing.T) {
t.Parallel()

// LEP-6 review C1 (Matee, 2026-05-06): with the missing-block default
// flipped to FALSE, an operator who upgrades without adding the LEP-6
// toggles MUST stay opted out. This test pins that contract: even
// though storage_challenge.enabled=true, the missing lep6 / recheck /
// self_healing blocks default to disabled. Operators must opt in
// explicitly. Runtime knobs (timeouts, concurrency) still receive
// their defaults so that flipping a toggle on later requires no
// further config edits.
// Testnet rollout default: an operator who upgrades without adding the
// LEP-6 blocks should run storage challenge + LEP-6 while the chain
// StorageTruthEnforcementMode remains the protocol gate. Explicit
// enabled:false remains the emergency-disable path. Runtime knobs
// still receive defaults so no extra config edits are required.
cfg := loadConfigFromBody(t, `
supernode:
key_name: test-key
Expand All @@ -40,17 +37,20 @@ storage_challenge:
enabled: true
`)

if cfg.StorageChallengeConfig.LEP6.Enabled {
t.Fatalf("storage_challenge.lep6.enabled default = true, want false (C1: opt-in not opt-out)")
if !cfg.StorageChallengeConfig.Enabled {
t.Fatalf("storage_challenge.enabled default = false, want true")
}
if !cfg.StorageChallengeConfig.LEP6.Enabled {
t.Fatalf("storage_challenge.lep6.enabled default = false, want true")
}
if cfg.StorageChallengeConfig.LEP6.MaxConcurrentTargets != DefaultLEP6MaxConcurrentTargets {
t.Fatalf("max_concurrent_targets = %d, want %d", cfg.StorageChallengeConfig.LEP6.MaxConcurrentTargets, DefaultLEP6MaxConcurrentTargets)
}
if cfg.StorageChallengeConfig.LEP6.RecipientReadTimeout != DefaultLEP6RecipientReadTimeout {
t.Fatalf("recipient_read_timeout = %s, want %s", cfg.StorageChallengeConfig.LEP6.RecipientReadTimeout, DefaultLEP6RecipientReadTimeout)
}
if cfg.StorageChallengeConfig.LEP6.Recheck.Enabled {
t.Fatalf("storage_challenge.lep6.recheck.enabled default = true, want false (C1)")
if !cfg.StorageChallengeConfig.LEP6.Recheck.Enabled {
t.Fatalf("storage_challenge.lep6.recheck.enabled default = false, want true")
}
if cfg.StorageChallengeConfig.LEP6.Recheck.LookbackEpochs != DefaultLEP6RecheckLookbackEpochs {
t.Fatalf("recheck.lookback_epochs = %d, want %d", cfg.StorageChallengeConfig.LEP6.Recheck.LookbackEpochs, DefaultLEP6RecheckLookbackEpochs)
Expand All @@ -68,8 +68,8 @@ storage_challenge:
t.Fatalf("recheck.failure_backoff_ttl_ms = %d, want %d", cfg.StorageChallengeConfig.LEP6.Recheck.FailureBackoffTTLms, int(DefaultLEP6RecheckFailureBackoffTTL/time.Millisecond))
}

if cfg.SelfHealingConfig.Enabled {
t.Fatalf("self_healing.enabled default = true, want false (C1)")
if !cfg.SelfHealingConfig.Enabled {
t.Fatalf("self_healing.enabled default = false, want true")
}
if cfg.SelfHealingConfig.PollIntervalMs != int(DefaultSelfHealingPollInterval/time.Millisecond) {
t.Fatalf("self_healing.poll_interval_ms = %d, want %d", cfg.SelfHealingConfig.PollIntervalMs, int(DefaultSelfHealingPollInterval/time.Millisecond))
Expand Down Expand Up @@ -118,7 +118,7 @@ lumera:
raptorq:
files_dir: raptorq_files
storage_challenge:
enabled: true
enabled: false
lep6:
enabled: false
recheck:
Expand All @@ -127,6 +127,9 @@ self_healing:
enabled: false
`)

if cfg.StorageChallengeConfig.Enabled {
t.Fatalf("storage_challenge.enabled = true, want explicit false emergency disable preserved")
}
if cfg.StorageChallengeConfig.LEP6.Enabled {
t.Fatalf("storage_challenge.lep6.enabled = true, want explicit false emergency disable preserved")
}
Expand Down
49 changes: 26 additions & 23 deletions supernode/config/lep6.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ func (c *Config) UnmarshalYAML(value *yaml.Node) error {
return nil
}

func (c *StorageChallengeConfig) UnmarshalYAML(value *yaml.Node) error {
type raw StorageChallengeConfig
var out raw
if err := value.Decode(&out); err != nil {
return err
}
*c = StorageChallengeConfig(out)
c.enabledSet = hasYAMLKey(value, "enabled")
return nil
}

func (c *StorageChallengeLEP6Config) UnmarshalYAML(value *yaml.Node) error {
type raw StorageChallengeLEP6Config
var out raw
Expand Down Expand Up @@ -77,30 +88,22 @@ func hasYAMLKey(value *yaml.Node, key string) bool {
return false
}

// applyLEP6DefaultsAndValidate applies safe defaults to LEP-6 toggles and
// runtime knobs, then runs validation.
// applyLEP6DefaultsAndValidate applies testnet-ready defaults to LEP-6
// toggles and runtime knobs, then runs validation.
//
// LEP-6 review C1 (Matee, 2026-05-06): the missing-block default for the
// three LEP-6 toggles (storage_challenge.lep6.enabled,
// storage_challenge.lep6.recheck.enabled, self_healing.enabled) is FALSE.
// Pre-Wave-4 the missing-block default was TRUE, which silently auto-opted
// every operator into LEP-6 on upgrade. Now an operator must explicitly
// opt in via either an explicit `enabled: true` in their YAML or by relying
// on `CreateDefaultConfig`, which writes the explicit toggles into the
// generated supernode.yml. Operators who want their existing config to
// pick up LEP-6 must add the toggles explicitly.
//
// Chain enforcement remains the protocol source of truth: even when these
// toggles are TRUE, every LEP-6 service no-ops while
// StorageTruthEnforcementMode is UNSPECIFIED (see e.g.
// LEP6Dispatcher.DispatchEpoch and self_healing.Service.Run). The toggles
// are only an operator-side opt-in switch.
// Storage truth remains chain-gated: even when local toggles default TRUE,
// every LEP-6 service no-ops while StorageTruthEnforcementMode is
// UNSPECIFIED. Operators can still emergency-disable any local runtime by
// setting the relevant `enabled: false` explicitly in YAML.
func (c *Config) applyLEP6DefaultsAndValidate() error {
// LEP-6 toggles: missing-block defaults to FALSE (C1).
// enabledSet=true means the YAML had an explicit `enabled:` key — keep
// the operator's value verbatim.
// Local storage-truth runtimes default ON for testnet operators who update
// without adding new config blocks. enabledSet=true means the YAML had an
// explicit `enabled:` key — keep the operator's value verbatim.
if !c.StorageChallengeConfig.enabledSet {
c.StorageChallengeConfig.Enabled = true
}
if !c.StorageChallengeConfig.LEP6.enabledSet {
c.StorageChallengeConfig.LEP6.Enabled = false
c.StorageChallengeConfig.LEP6.Enabled = true
}
if c.StorageChallengeConfig.LEP6.MaxConcurrentTargets == 0 {
c.StorageChallengeConfig.LEP6.MaxConcurrentTargets = DefaultLEP6MaxConcurrentTargets
Expand All @@ -111,7 +114,7 @@ func (c *Config) applyLEP6DefaultsAndValidate() error {

recheck := &c.StorageChallengeConfig.LEP6.Recheck
if !recheck.enabledSet {
recheck.Enabled = false
recheck.Enabled = true
}
if recheck.LookbackEpochs == 0 {
recheck.LookbackEpochs = DefaultLEP6RecheckLookbackEpochs
Expand All @@ -130,7 +133,7 @@ func (c *Config) applyLEP6DefaultsAndValidate() error {
}

if !c.SelfHealingConfig.enabledSet {
c.SelfHealingConfig.Enabled = false
c.SelfHealingConfig.Enabled = true
}
if c.SelfHealingConfig.PollIntervalMs == 0 {
c.SelfHealingConfig.PollIntervalMs = int(DefaultSelfHealingPollInterval / time.Millisecond)
Expand Down
69 changes: 34 additions & 35 deletions supernode/config/lep6_config_regression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,35 @@ import (
"testing"
)

// LEP-6 review regression: LEP-6 PR286 review fix regression tests.
// LEP-6 config regression tests.
//
// Coverage:
// - C1: missing-block default for LEP-6 toggles is FALSE (no silent
// upgrade-time opt-in). Already covered structurally by
// TestLoadConfig_LEP6SafeDefaults; this file adds focused negative
// cases (wrong-direction default would cause auto-opt-in) and the
// advisory helper.
// - Missing-block defaults are ON for storage_challenge, LEP-6 dispatch,
// recheck, and self-healing so testnet operators get storage-truth
// runtime after update unless they explicitly emergency-disable it.
// - L6: structural validator rejects recheck=true with disabled parents.
// Before this fix, fixtures could carry recheck.enabled=true while
// storage_challenge.enabled=false, silently no-op'd at runtime.

func TestLoadConfig_C1_MissingBlocksDefaultDisabled(t *testing.T) {
func TestLoadConfig_MissingBlocksDefaultEnabled(t *testing.T) {
t.Parallel()

// No LEP-6 / recheck / self_healing block at all — defaults must be FALSE.
// No storage_challenge / LEP-6 / recheck / self_healing block at all — defaults must be TRUE.
cfg := loadConfigFromBody(t, baseConfigYAML())

if cfg.StorageChallengeConfig.LEP6.Enabled {
t.Fatalf("C1: storage_challenge.lep6.enabled = true on missing-block; want false (no silent opt-in)")
if !cfg.StorageChallengeConfig.Enabled {
t.Fatalf("storage_challenge.enabled = false on missing-block; want true")
}
if cfg.StorageChallengeConfig.LEP6.Recheck.Enabled {
t.Fatalf("C1: storage_challenge.lep6.recheck.enabled = true on missing-block; want false")
if !cfg.StorageChallengeConfig.LEP6.Enabled {
t.Fatalf("storage_challenge.lep6.enabled = false on missing-block; want true")
}
if !cfg.StorageChallengeConfig.LEP6.Recheck.Enabled {
t.Fatalf("storage_challenge.lep6.recheck.enabled = false on missing-block; want true")
}
if cfg.SelfHealingConfig.Enabled {
t.Fatalf("C1: self_healing.enabled = true on missing-block; want false")
if !cfg.SelfHealingConfig.Enabled {
t.Fatalf("self_healing.enabled = false on missing-block; want true")
}
}

func TestLoadConfig_C1_ExplicitTrueRespected(t *testing.T) {
func TestLoadConfig_ExplicitTrueRespected(t *testing.T) {
t.Parallel()

cfg := loadConfigFromBody(t, baseConfigYAML()+`
Expand All @@ -51,24 +50,33 @@ self_healing:
`)

if !cfg.StorageChallengeConfig.LEP6.Enabled {
t.Fatalf("C1: explicit storage_challenge.lep6.enabled=true must be respected")
t.Fatalf("explicit storage_challenge.lep6.enabled=true must be respected")
}
if !cfg.StorageChallengeConfig.LEP6.Recheck.Enabled {
t.Fatalf("C1: explicit recheck.enabled=true must be respected")
t.Fatalf("explicit recheck.enabled=true must be respected")
}
if !cfg.SelfHealingConfig.Enabled {
t.Fatalf("C1: explicit self_healing.enabled=true must be respected")
t.Fatalf("explicit self_healing.enabled=true must be respected")
}
}

func TestLoadConfig_C1_OptInAdvisory(t *testing.T) {
func TestLoadConfig_LEP6OperatorOptInAdvisory(t *testing.T) {
t.Parallel()

// All three opted out — advisory must mention each disabled service.
allOff := loadConfigFromBody(t, baseConfigYAML())
// Explicitly opted out — advisory must mention each disabled service.
allOff := loadConfigFromBody(t, baseConfigYAML()+`
storage_challenge:
enabled: true
lep6:
enabled: false
recheck:
enabled: false
self_healing:
enabled: false
`)
advisory := allOff.LEP6OperatorOptInAdvisory()
if advisory == "" {
t.Fatalf("C1: advisory must be non-empty when toggles are off")
t.Fatalf("advisory must be non-empty when toggles are off")
}
for _, want := range []string{
"storage_challenge.lep6.enabled=false",
Expand All @@ -80,17 +88,8 @@ func TestLoadConfig_C1_OptInAdvisory(t *testing.T) {
}
}

// All three opted in — advisory must be empty.
allOn := loadConfigFromBody(t, baseConfigYAML()+`
storage_challenge:
enabled: true
lep6:
enabled: true
recheck:
enabled: true
self_healing:
enabled: true
`)
// Missing blocks now default on — advisory must be empty.
allOn := loadConfigFromBody(t, baseConfigYAML())
if got := allOn.LEP6OperatorOptInAdvisory(); got != "" {
t.Fatalf("C1 advisory should be empty when all opted in; got %q", got)
}
Expand Down
Loading
Loading