From 31a9ded8df54944f2ca98973cc6a8098f53265e5 Mon Sep 17 00:00:00 2001 From: blindchaser Date: Tue, 8 Sep 2026 15:50:29 -0400 Subject: [PATCH 1/2] Refuse to start a mock_chain_validation build outside its reserve role A mock_chain_validation binary swallows ErrAppHash and the validator-set sentinels, which is what lets a reserve node keep following a chain it disagrees with. That same property makes two configurations dangerous rather than merely wrong: as a validator it would prevote and precommit blocks whose app hash contradicts its own state instead of prevoting nil, and left unpinned it resolves to auto and joins the migration on the first block after governance raises the batch size, spending the reserve with nothing to signal that it happened. Both are now refused in startInProcess, which every deployed node reaches and no test does. The unpinned case is the default state of a fresh node rather than an unlikely slip: a generated app.toml renders sc-write-mode = "memiavl_only" but not sc-write-mode-enable-auto, and while that unrendered key stays true it discards the rendered one. Production builds compile the no-op variant and are unaffected. Co-authored-by: Cursor (cherry picked from commit a16167767f307fec42f0b2cdba728f0b41f1c641) --- sei-cosmos/server/reserve_policy_default.go | 10 +++ .../server/reserve_policy_default_test.go | 41 ++++++++++++ .../reserve_policy_mock_chain_validation.go | 46 ++++++++++++++ ...serve_policy_mock_chain_validation_test.go | 62 +++++++++++++++++++ sei-cosmos/server/start.go | 3 + 5 files changed, 162 insertions(+) create mode 100644 sei-cosmos/server/reserve_policy_default.go create mode 100644 sei-cosmos/server/reserve_policy_default_test.go create mode 100644 sei-cosmos/server/reserve_policy_mock_chain_validation.go create mode 100644 sei-cosmos/server/reserve_policy_mock_chain_validation_test.go diff --git a/sei-cosmos/server/reserve_policy_default.go b/sei-cosmos/server/reserve_policy_default.go new file mode 100644 index 0000000000..1bda347baa --- /dev/null +++ b/sei-cosmos/server/reserve_policy_default.go @@ -0,0 +1,10 @@ +//go:build !mock_chain_validation + +package server + +import sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" + +// assertReserveNodeAllowed reports whether a node may start with the given +// Tendermint mode and effective state-commit write mode. Production builds +// accept every combination the configuration itself accepts. +func assertReserveNodeAllowed(string, sctypes.WriteMode) error { return nil } diff --git a/sei-cosmos/server/reserve_policy_default_test.go b/sei-cosmos/server/reserve_policy_default_test.go new file mode 100644 index 0000000000..0403b1e39d --- /dev/null +++ b/sei-cosmos/server/reserve_policy_default_test.go @@ -0,0 +1,41 @@ +//go:build !mock_chain_validation + +package server + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" + tmcfg "github.com/sei-protocol/sei-chain/sei-tendermint/config" +) + +// A production build must not refuse any mode/write-mode combination; the +// reserve guard exists only in the mock_chain_validation build. +func TestAssertReserveNodeAllowed_Default_AcceptsEveryCombination(t *testing.T) { + for _, nodeMode := range allNodeModes() { + for _, writeMode := range allSCWriteModes() { + require.NoError(t, assertReserveNodeAllowed(nodeMode, writeMode), + "mode %q with write mode %q must be accepted by a production build", nodeMode, writeMode) + } + } +} + +func allNodeModes() []string { + return []string{tmcfg.ModeFull, tmcfg.ModeValidator, tmcfg.ModeSeed} +} + +func allSCWriteModes() []sctypes.WriteMode { + return []sctypes.WriteMode{ + sctypes.MemiavlOnly, + sctypes.MigrateEVM, + sctypes.EVMMigrated, + sctypes.MigrateAllButBank, + sctypes.AllMigratedButBank, + sctypes.MigrateBank, + sctypes.FlatKVOnly, + sctypes.TestOnlyDualWrite, + sctypes.Auto, + } +} diff --git a/sei-cosmos/server/reserve_policy_mock_chain_validation.go b/sei-cosmos/server/reserve_policy_mock_chain_validation.go new file mode 100644 index 0000000000..69149ddd13 --- /dev/null +++ b/sei-cosmos/server/reserve_policy_mock_chain_validation.go @@ -0,0 +1,46 @@ +//go:build mock_chain_validation + +package server + +import ( + "fmt" + + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" + tmcfg "github.com/sei-protocol/sei-chain/sei-tendermint/config" +) + +// assertReserveNodeAllowed reports whether a node may start with the given +// Tendermint mode and effective state-commit write mode. This build starts +// only as a non-validator pinned to memiavl_only. +// +// Both conditions are what make the build a reserve rather than a liability. +// The consensus policy compiled in here swallows ErrAppHash and the +// validator-set sentinels, so a validator running it would prevote and +// precommit blocks whose app hash contradicts its own state instead of +// prevoting nil. And a node that is not pinned joins the migration on the +// first block after governance raises the batch size, which spends the reserve +// with nothing to signal that it happened. +// +// Refusing here rather than trusting app.toml costs a restart to discover and +// saves finding out at the first divergent block. +func assertReserveNodeAllowed(nodeMode string, writeMode sctypes.WriteMode) error { + if nodeMode == tmcfg.ModeValidator { + return fmt.Errorf( + "mock_chain_validation builds must not run as a validator: this build "+ + "swallows app-hash and validator-set validation failures, so it cannot "+ + "safely vote; set mode = %q in config.toml", + tmcfg.ModeFull, + ) + } + if writeMode != sctypes.MemiavlOnly { + return fmt.Errorf( + "mock_chain_validation builds must run %[1]q, got %[2]q: set "+ + "state-commit.sc-write-mode = %[1]q and "+ + "state-commit.sc-write-mode-enable-auto = false in app.toml (the latter "+ + "is absent from a generated app.toml and defaults to true, which "+ + "discards the former)", + sctypes.MemiavlOnly, writeMode, + ) + } + return nil +} diff --git a/sei-cosmos/server/reserve_policy_mock_chain_validation_test.go b/sei-cosmos/server/reserve_policy_mock_chain_validation_test.go new file mode 100644 index 0000000000..238291b608 --- /dev/null +++ b/sei-cosmos/server/reserve_policy_mock_chain_validation_test.go @@ -0,0 +1,62 @@ +//go:build mock_chain_validation + +package server + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" + tmcfg "github.com/sei-protocol/sei-chain/sei-tendermint/config" +) + +// Exactly one combination may start: a non-validator pinned to memiavl_only. +func TestAssertReserveNodeAllowed_MockChainValidation_Matrix(t *testing.T) { + for _, nodeMode := range allNodeModes() { + for _, writeMode := range allSCWriteModes() { + allowed := nodeMode != tmcfg.ModeValidator && writeMode == sctypes.MemiavlOnly + err := assertReserveNodeAllowed(nodeMode, writeMode) + if allowed { + require.NoError(t, err, "mode %q with write mode %q must be accepted", nodeMode, writeMode) + continue + } + require.Error(t, err, "mode %q with write mode %q must be refused", nodeMode, writeMode) + } + } +} + +// Validator mode is refused whatever the write mode, so a correctly pinned +// validator does not slip through. +func TestAssertReserveNodeAllowed_MockChainValidation_RefusesPinnedValidator(t *testing.T) { + err := assertReserveNodeAllowed(tmcfg.ModeValidator, sctypes.MemiavlOnly) + require.Error(t, err) + require.Contains(t, err.Error(), tmcfg.ModeFull) +} + +// Auto is what a forgotten sc-write-mode-enable-auto resolves to, and it is the +// failure the write-mode half of this guard exists for, so the message must +// name that key. +func TestAssertReserveNodeAllowed_MockChainValidation_ErrorNamesTheAutoKey(t *testing.T) { + err := assertReserveNodeAllowed(tmcfg.ModeFull, sctypes.Auto) + require.Error(t, err) + require.Contains(t, err.Error(), "sc-write-mode-enable-auto") +} + +func allNodeModes() []string { + return []string{tmcfg.ModeFull, tmcfg.ModeValidator, tmcfg.ModeSeed} +} + +func allSCWriteModes() []sctypes.WriteMode { + return []sctypes.WriteMode{ + sctypes.MemiavlOnly, + sctypes.MigrateEVM, + sctypes.EVMMigrated, + sctypes.MigrateAllButBank, + sctypes.AllMigratedButBank, + sctypes.MigrateBank, + sctypes.FlatKVOnly, + sctypes.TestOnlyDualWrite, + sctypes.Auto, + } +} diff --git a/sei-cosmos/server/start.go b/sei-cosmos/server/start.go index ff9e416b75..5f9d437b07 100644 --- a/sei-cosmos/server/start.go +++ b/sei-cosmos/server/start.go @@ -312,6 +312,9 @@ func startInProcess( if err := config.ValidateFreeze(); err != nil { return err } + if err := assertReserveNodeAllowed(cfg.Mode, config.StateCommit.WriteMode); err != nil { + return err + } gRPCOnly := ctx.Viper.GetBool(flagGRPCOnly) if gRPCOnly && config.FreezeHeight > 0 { return errors.New("freeze-height cannot be used with grpc-only mode") From feb9aa72fff996f273122434e2c2ed1c772b51a4 Mon Sep 17 00:00:00 2001 From: blindchaser Date: Sat, 12 Sep 2026 21:56:18 -0400 Subject: [PATCH 2/2] Default the mock_chain_validation build to its reserve configuration A reserve node follows the chain on memiavl_only so it holds pre-H_start state that the fleet can recover from once the FlatKV migration is under way. Getting a node into that state depended on the operator pinning the write mode by hand at creation time, and a value that misses `seid init` cannot reach the node afterwards, so a missed key cost a rebuild. Under the mock_chain_validation tag the build now defaults to the reserve configuration instead of depending on that step. sc-write-mode-enable-auto defaults to false, so the shipped memiavl_only write mode is honoured rather than being forced to auto, and the key is rendered into app.toml so the file states what the node actually does. Production builds are unaffected: the default stays true and the key stays unrendered. The existing start-up guard already refuses a validator role or a write mode other than memiavl_only, so a misconfigured reserve node fails loudly rather than diverging quietly. This change removes the configuration step that guard was most likely to catch. The configuration characterization suite pins this behaviour, so its records are now kept per build tag: the default-build golden is unchanged and a `.reserve.golden` records what the reserve build resolves to. Co-authored-by: Cursor --- app/config_fuzz_test.go | 10 +- app/config_register_agreement_test.go | 21 +-- app/seidb_test.go | 8 +- app/testdata/state-commit.reserve.golden | 71 +++++++++ app/write_mode_default_test.go | 30 ++++ app/write_mode_mock_chain_validation_test.go | 22 +++ sei-cosmos/server/config/config_fuzz_test.go | 8 +- sei-cosmos/server/config/config_test.go | 96 +---------- .../testdata/server_config.reserve.golden | 149 ++++++++++++++++++ .../server/config/write_mode_default_test.go | 112 +++++++++++++ .../write_mode_mock_chain_validation_test.go | 113 +++++++++++++ sei-db/config/reserve_defaults_default.go | 11 ++ .../config/reserve_defaults_default_test.go | 31 ++++ .../reserve_defaults_mock_chain_validation.go | 31 ++++ ...rve_defaults_mock_chain_validation_test.go | 30 ++++ sei-db/config/reserve_defaults_test.go | 37 +++++ sei-db/config/sc_config.go | 4 +- sei-db/config/sc_config_test.go | 10 +- sei-db/config/toml.go | 13 +- 19 files changed, 681 insertions(+), 126 deletions(-) create mode 100644 app/testdata/state-commit.reserve.golden create mode 100644 app/write_mode_default_test.go create mode 100644 app/write_mode_mock_chain_validation_test.go create mode 100644 sei-cosmos/server/config/testdata/server_config.reserve.golden create mode 100644 sei-cosmos/server/config/write_mode_default_test.go create mode 100644 sei-cosmos/server/config/write_mode_mock_chain_validation_test.go create mode 100644 sei-db/config/reserve_defaults_default.go create mode 100644 sei-db/config/reserve_defaults_default_test.go create mode 100644 sei-db/config/reserve_defaults_mock_chain_validation.go create mode 100644 sei-db/config/reserve_defaults_mock_chain_validation_test.go create mode 100644 sei-db/config/reserve_defaults_test.go diff --git a/app/config_fuzz_test.go b/app/config_fuzz_test.go index 9d42328eee..a97d73a884 100644 --- a/app/config_fuzz_test.go +++ b/app/config_fuzz_test.go @@ -347,8 +347,8 @@ func FuzzSCWriteMode(f *testing.F) { cfg := parseSCConfigs(opts) - // enable-auto defaults to true and only an explicit key changes it. - effectiveAuto := true + // enable-auto takes its in-code default and only an explicit key changes it. + effectiveAuto := config.DefaultStateCommitConfig().WriteModeEnableAuto if setAuto { effectiveAuto = auto } @@ -507,14 +507,14 @@ func FuzzReadGenesisStreamImport(f *testing.F) { // TestParseSCConfigsAbsentBaseline records what an app.toml with no // [state-commit] section resolves to. It is not the in-code default: the two // unguarded reads clobber Enable to false and Directory to "", and the write mode -// resolves to auto because enable-auto defaults to true. The Enable clobber is the +// follows whatever enable-auto defaults to on this build. The Enable clobber is the // reason a node whose app.toml predates the section refuses to boot — SetupSeiDB // panics on !Enable rather than falling back. func TestParseSCConfigsAbsentBaseline(t *testing.T) { want := config.DefaultStateCommitConfig() want.Enable = false // unguarded read of an absent key want.Directory = "" // unguarded read of an absent key - want.WriteMode = sctypes.Auto + want.WriteMode = wantAbsentAutoWriteMode want.HashLogger.Version = version.Version // stamped from the build, not from config got := parseSCConfigs(configtest.AppOpts{}) @@ -631,7 +631,7 @@ func TestKeyNamesMatchTheRecordedNames(t *testing.T) { // this package and sei-cosmos/server/config, and regenerating only one leaves the other red, so // regenerate both and read both diffs. func TestDefaultsMatchTheRecordedValues(t *testing.T) { - configtest.CheckDefaults(t, "state-commit", config.DefaultStateCommitConfig()) + configtest.CheckDefaults(t, stateCommitRecord, config.DefaultStateCommitConfig()) configtest.CheckDefaults(t, "state-store", config.DefaultStateStoreConfig()) configtest.CheckDefaults(t, "light_invariance", DefaultLightInvarianceConfig) configtest.CheckDefaults(t, "genesis", DefaultGenesisConfig) diff --git a/app/config_register_agreement_test.go b/app/config_register_agreement_test.go index ca84b68182..c9a7f60f39 100644 --- a/app/config_register_agreement_test.go +++ b/app/config_register_agreement_test.go @@ -22,7 +22,7 @@ var whatANodeRunsToday = map[string]string{ FlagSSPruneInterval: "0", FlagSSImportNumWorkers: "0", FlagSCEnable: "false", - FlagSCWriteMode: "auto", + FlagSCWriteMode: scWriteModeANodeRuns, } // whyItMatters says what a node gets today, for the keys where that is worth stating. @@ -42,15 +42,18 @@ var whyItMatters = map[string]string{ // Per mode because the section answers per mode for two of these settings and the reader does not answer // per mode at all. An archive node declares the retention the reader also produces, so that key agrees for // archive and disagrees everywhere else; the store toggle is the reverse. +// +// Every entry ends in scWriteModeDivergence, which holds sc-write-mode on builds where the two sides +// disagree and is empty on the one build where they do not. var theDivergences = map[registry.Mode][]string{ - registry.ModeValidator: {FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent, - FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable, FlagSCWriteMode}, - registry.ModeSeed: {FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent, - FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable, FlagSCWriteMode}, - registry.ModeFull: {FlagSSEnable, FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent, - FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable, FlagSCWriteMode}, - registry.ModeArchive: {FlagSSEnable, FlagSSBackend, FlagSSAsyncWriterBuffer, - FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable, FlagSCWriteMode}, + registry.ModeValidator: append([]string{FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent, + FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable}, scWriteModeDivergence...), + registry.ModeSeed: append([]string{FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent, + FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable}, scWriteModeDivergence...), + registry.ModeFull: append([]string{FlagSSEnable, FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent, + FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable}, scWriteModeDivergence...), + registry.ModeArchive: append([]string{FlagSSEnable, FlagSSBackend, FlagSSAsyncWriterBuffer, + FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable}, scWriteModeDivergence...), } // readerValues is what each section's reader produces for a file carrying no keys at all. diff --git a/app/seidb_test.go b/app/seidb_test.go index f089826f2b..6ac968d4ea 100644 --- a/app/seidb_test.go +++ b/app/seidb_test.go @@ -76,10 +76,10 @@ func TestNewDefaultConfig(t *testing.T) { ssConfig := parseSSConfigs(appOpts) receiptConfig, err := config.ReadReceiptConfig(appOpts) assert.NoError(t, err) - // WriteModeEnableAuto defaults to true, so parseSCConfigs resolves the effective - // WriteMode to auto, overriding the fixed-fallback default (memiavl_only). + // parseSCConfigs resolves the effective WriteMode through WriteModeEnableAuto, + // so it is the one field that need not equal the fixed-fallback default. expectedSC := config.DefaultStateCommitConfig() - expectedSC.WriteMode = sctypes.Auto + expectedSC.WriteMode = wantAbsentAutoWriteMode // parseSCConfigs is a raw parse and does not align FlatKV with memIAVL (that // happens in composite.NewCompositeCommitStore), so the parsed config matches // the in-code defaults verbatim apart from the resolved write mode. @@ -169,7 +169,7 @@ func TestParseSCConfigs_LegacyCosmosOnlyWriteMode(t *testing.T) { FlagSCEnable: true, FlagSCWriteMode: "cosmos_only", }) - assert.Equal(t, sctypes.Auto, scConfig.WriteMode) + assert.Equal(t, wantAbsentAutoWriteMode, scConfig.WriteMode) scConfig = parseSCConfigs(mapAppOpts{ FlagSCEnable: true, diff --git a/app/testdata/state-commit.reserve.golden b/app/testdata/state-commit.reserve.golden new file mode 100644 index 0000000000..1627385aca --- /dev/null +++ b/app/testdata/state-commit.reserve.golden @@ -0,0 +1,71 @@ +Enable = bool(true) +Directory = string("") +AsyncCommitBuffer = int(0) +WriteMode = types.WriteMode("memiavl_only") +WriteModeEnableAuto = bool(false) +MemIAVLConfig.AsyncCommitBuffer = int(100) +MemIAVLConfig.SnapshotKeepRecent = uint32(1) +MemIAVLConfig.SnapshotInterval = uint32(10000) +MemIAVLConfig.SnapshotMinTimeInterval = uint32(3600) +MemIAVLConfig.SnapshotWriterLimit = int(4) +MemIAVLConfig.SnapshotPrefetchThreshold = float64(0.8) +MemIAVLConfig.SnapshotWriteRateMBps = int(100) +FlatKVConfig.DataDir = string("") +FlatKVConfig.Fsync = bool(false) +FlatKVConfig.AsyncWriteBuffer = int(0) +FlatKVConfig.SnapshotInterval = uint32(10000) +FlatKVConfig.SnapshotKeepRecent = uint32(1) +FlatKVConfig.ExternalPruning = bool(false) +FlatKVConfig.EnablePebbleMetrics = bool(true) +FlatKVConfig.EnableReadWriteMetrics = bool(false) +FlatKVConfig.AccountDBConfig.DataDir = string("") +FlatKVConfig.AccountDBConfig.EnableMetrics = bool(true) +FlatKVConfig.AccountDBConfig.EnableReadWriteMetrics = bool(false) +FlatKVConfig.AccountDBConfig.MetricsScrapeInterval = time.Duration(10s) +FlatKVConfig.AccountCacheConfig.ShardCount = uint64(8) +FlatKVConfig.AccountCacheConfig.MaxSize = uint64(1073741824) +FlatKVConfig.AccountCacheConfig.EstimatedOverheadPerEntry = uint64(250) +FlatKVConfig.AccountCacheConfig.MetricsName = string("") +FlatKVConfig.AccountCacheConfig.MetricsScrapeInterval = time.Duration(0s) +FlatKVConfig.CodeDBConfig.DataDir = string("") +FlatKVConfig.CodeDBConfig.EnableMetrics = bool(true) +FlatKVConfig.CodeDBConfig.EnableReadWriteMetrics = bool(false) +FlatKVConfig.CodeDBConfig.MetricsScrapeInterval = time.Duration(10s) +FlatKVConfig.CodeCacheConfig.ShardCount = uint64(8) +FlatKVConfig.CodeCacheConfig.MaxSize = uint64(536870912) +FlatKVConfig.CodeCacheConfig.EstimatedOverheadPerEntry = uint64(250) +FlatKVConfig.CodeCacheConfig.MetricsName = string("") +FlatKVConfig.CodeCacheConfig.MetricsScrapeInterval = time.Duration(0s) +FlatKVConfig.StorageDBConfig.DataDir = string("") +FlatKVConfig.StorageDBConfig.EnableMetrics = bool(true) +FlatKVConfig.StorageDBConfig.EnableReadWriteMetrics = bool(false) +FlatKVConfig.StorageDBConfig.MetricsScrapeInterval = time.Duration(10s) +FlatKVConfig.StorageCacheConfig.ShardCount = uint64(8) +FlatKVConfig.StorageCacheConfig.MaxSize = uint64(4294967296) +FlatKVConfig.StorageCacheConfig.EstimatedOverheadPerEntry = uint64(250) +FlatKVConfig.StorageCacheConfig.MetricsName = string("") +FlatKVConfig.StorageCacheConfig.MetricsScrapeInterval = time.Duration(0s) +FlatKVConfig.MiscDBConfig.DataDir = string("") +FlatKVConfig.MiscDBConfig.EnableMetrics = bool(true) +FlatKVConfig.MiscDBConfig.EnableReadWriteMetrics = bool(false) +FlatKVConfig.MiscDBConfig.MetricsScrapeInterval = time.Duration(10s) +FlatKVConfig.MiscCacheConfig.ShardCount = uint64(8) +FlatKVConfig.MiscCacheConfig.MaxSize = uint64(536870912) +FlatKVConfig.MiscCacheConfig.EstimatedOverheadPerEntry = uint64(250) +FlatKVConfig.MiscCacheConfig.MetricsName = string("") +FlatKVConfig.MiscCacheConfig.MetricsScrapeInterval = time.Duration(0s) +FlatKVConfig.ReaderThreadsPerCore = float64(2) +FlatKVConfig.ReaderConstantThreadCount = int(0) +FlatKVConfig.ReaderPoolQueueSize = int(1024) +FlatKVConfig.MiscPoolThreadsPerCore = float64(4) +FlatKVConfig.MiscConstantThreadCount = int(0) +FlatKVConfig.LtHashThreadsPerCore = float64(1) +HistoricalProofMaxInFlight = int(1) +HistoricalProofRateLimit = float64(1) +HistoricalProofBurst = int(1) +HashLogger.Enable = bool(true) +HashLogger.Directory = string("") +HashLogger.BlocksToRetain = uint(0) +HashLogger.TargetFileSize = uint(16777216) +HashLogger.MaxDiskSize = uint(17179869184) +HashLogger.Version = string("") diff --git a/app/write_mode_default_test.go b/app/write_mode_default_test.go new file mode 100644 index 0000000000..759ee3a4a1 --- /dev/null +++ b/app/write_mode_default_test.go @@ -0,0 +1,30 @@ +//go:build !mock_chain_validation + +package app + +import ( + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" +) + +// The four values below are everything in this package's configuration records +// that moves with sc-write-mode-enable-auto's in-code default. The reserve build +// declares that key false where this one declares it true, so it carries a +// write_mode_mock_chain_validation_test.go stating each of these the other way. + +// stateCommitRecord names this build's [state-commit] defaults record. The two +// builds keep separate records because a shared one would be rewritten by +// whichever build regenerated it last, losing the value the reviewer needed. +const stateCommitRecord = "state-commit" + +// wantAbsentAutoWriteMode is the mode parseSCConfigs resolves memiavl_only to +// when app.toml carries no sc-write-mode-enable-auto key. +const wantAbsentAutoWriteMode = sctypes.Auto + +// scWriteModeDivergence is the tail of every theDivergences entry. sc-write-mode +// belongs on those lists here, because the section declares memiavl_only and a +// node missing the key runs auto instead. +var scWriteModeDivergence = []string{FlagSCWriteMode} + +// scWriteModeANodeRuns is what sc-write-mode resolves to for a file carrying no +// keys at all. +const scWriteModeANodeRuns = "auto" diff --git a/app/write_mode_mock_chain_validation_test.go b/app/write_mode_mock_chain_validation_test.go new file mode 100644 index 0000000000..2501d8c739 --- /dev/null +++ b/app/write_mode_mock_chain_validation_test.go @@ -0,0 +1,22 @@ +//go:build mock_chain_validation + +package app + +import ( + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" +) + +// See write_mode_default_test.go for what these four values are and why each +// build states them separately. This build declares sc-write-mode-enable-auto +// false, so an explicit sc-write-mode is honored rather than replaced by auto. + +const stateCommitRecord = "state-commit.reserve" + +const wantAbsentAutoWriteMode = sctypes.MemiavlOnly + +// scWriteModeDivergence is empty here. The section declares memiavl_only and a +// node missing the key runs memiavl_only, so this is the one build on which +// sc-write-mode is not a setting whose declared and running values disagree. +var scWriteModeDivergence []string + +const scWriteModeANodeRuns = "memiavl_only" diff --git a/sei-cosmos/server/config/config_fuzz_test.go b/sei-cosmos/server/config/config_fuzz_test.go index 04ef74c8a4..591bafcba2 100644 --- a/sei-cosmos/server/config/config_fuzz_test.go +++ b/sei-cosmos/server/config/config_fuzz_test.go @@ -224,8 +224,8 @@ func FuzzGetConfigGRPCDurationClamps(f *testing.F) { // FuzzGetConfigWriteMode pins GetConfig's own copy of the write-mode resolution. // // The rules match app/seidb.go — always parse, then let sc-write-mode-enable-auto -// (default true, flipped only by an explicit key) decide whether the parsed mode is -// honored — but the mechanism differs: GetConfig returns an error where seidb.go +// (the in-code default, flipped only by an explicit key) decide whether the parsed +// mode is honored — but the mechanism differs: GetConfig returns an error where seidb.go // panics. Both parsers must agree on the resolved mode for a node's store choice // and its reported config to describe the same thing, so the agreement is asserted // against the shared helpers rather than restated. @@ -261,7 +261,7 @@ func FuzzGetConfigWriteMode(f *testing.F) { t.Fatalf("sc-write-mode = %q must parse, got %v", mode, err) } - effectiveAuto := true + effectiveAuto := config.DefaultStateCommitConfig().WriteModeEnableAuto if setAuto { effectiveAuto = auto } @@ -743,7 +743,7 @@ func TestDefaultsMatchTheRecordedValues(t *testing.T) { // the other leaves that other one red. configtest.CheckDefaults(t, "state-sync", DefaultConfig().StateSync) - configtest.CheckDefaults(t, "server_config", DefaultConfig(), + configtest.CheckDefaults(t, serverConfigRecord, DefaultConfig(), configtest.DerivedDefault{ Path: "ConcurrencyWorkers", Want: max(10, min(runtime.NumCPU()*2, 128)), Why: "max(10, min(runtime.NumCPU()*2, 128))", diff --git a/sei-cosmos/server/config/config_test.go b/sei-cosmos/server/config/config_test.go index 38a1abc0b9..554d3665fc 100644 --- a/sei-cosmos/server/config/config_test.go +++ b/sei-cosmos/server/config/config_test.go @@ -640,88 +640,9 @@ func TestGetConfigRejectsInvalidWriteMode(t *testing.T) { require.Contains(t, err.Error(), "bogus_mode") } -// TestGetConfigLegacyMemiavlOnlyResolvesToAuto guards the existing-fleet -// upgrade path: a config written by an older binary carries an explicit -// sc-write-mode = "memiavl_only" but no sc-write-mode-enable-auto key. The absent -// key must default to true so the node resolves to auto and can follow a -// governance-driven migration without any app.toml edit. -func TestGetConfigLegacyMemiavlOnlyResolvesToAuto(t *testing.T) { - v := viper.New() - - v.Set("minimum-gas-prices", DefaultMinGasPrices) - v.Set("telemetry.global-labels", []interface{}{}) - v.Set("state-commit.sc-write-mode", "memiavl_only") - - cfg, err := GetConfig(v) - require.NoError(t, err) - require.True(t, cfg.StateCommit.WriteModeEnableAuto) - require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, - "absent sc-write-mode-enable-auto must default to true and override an explicit memiavl_only") -} - -func TestGetConfigLegacyCosmosOnlyResolvesToAuto(t *testing.T) { - v := viper.New() - - v.Set("minimum-gas-prices", DefaultMinGasPrices) - v.Set("telemetry.global-labels", []interface{}{}) - v.Set("state-commit.sc-write-mode", "cosmos_only") - - cfg, err := GetConfig(v) - require.NoError(t, err) - require.True(t, cfg.StateCommit.WriteModeEnableAuto) - require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, - "v6.4/v6.5 app.toml files with cosmos_only must parse before auto mode is applied") -} - -// TestGetConfigPinnedModeRequiresAutoDisabled verifies that an explicit -// sc-write-mode is only honored when sc-write-mode-enable-auto = false. With auto -// enabled (the default), the explicit mode is ignored and the node runs in auto. -func TestGetConfigPinnedModeRequiresAutoDisabled(t *testing.T) { - for _, mode := range []sctypes.WriteMode{ - sctypes.FlatKVOnly, - sctypes.EVMMigrated, - sctypes.TestOnlyDualWrite, - } { - t.Run(string(mode)+"/auto-disabled-pins", func(t *testing.T) { - v := viper.New() - v.Set("minimum-gas-prices", DefaultMinGasPrices) - v.Set("telemetry.global-labels", []interface{}{}) - v.Set("state-commit.sc-write-mode-enable-auto", false) - v.Set("state-commit.sc-write-mode", string(mode)) - - cfg, err := GetConfig(v) - require.NoError(t, err) - require.False(t, cfg.StateCommit.WriteModeEnableAuto) - require.Equal(t, mode, cfg.StateCommit.WriteMode, - "with auto disabled the explicit mode must be honored as a pin") - }) - - t.Run(string(mode)+"/auto-enabled-overrides", func(t *testing.T) { - v := viper.New() - v.Set("minimum-gas-prices", DefaultMinGasPrices) - v.Set("telemetry.global-labels", []interface{}{}) - v.Set("state-commit.sc-write-mode", string(mode)) - - cfg, err := GetConfig(v) - require.NoError(t, err) - require.True(t, cfg.StateCommit.WriteModeEnableAuto) - require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, - "with auto enabled (default) the explicit mode must be ignored in favor of auto") - }) - } -} - -func TestGetConfigEmptyWriteModeUsesDefault(t *testing.T) { - v := viper.New() - - v.Set("minimum-gas-prices", DefaultMinGasPrices) - v.Set("telemetry.global-labels", []interface{}{}) - - cfg, err := GetConfig(v) - require.NoError(t, err) - require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, - "unset sc-write-mode must fall back to the in-code default") -} +// The write-mode resolution records live in write_mode_default_test.go and its +// mock_chain_validation counterpart: the reserve build defaults +// sc-write-mode-enable-auto the other way, so every resolution differs by build. func TestGetConfigStateStore(t *testing.T) { v := viper.New() @@ -758,17 +679,6 @@ func TestGetConfigStateStore(t *testing.T) { require.True(t, cfg.StateStore.SeparateEVMSubDBs) } -func TestDefaultStateCommitConfig(t *testing.T) { - cfg := DefaultConfig() - - require.True(t, cfg.StateCommit.Enable) - require.Empty(t, cfg.StateCommit.Directory) - // WriteMode is the fixed fallback (memiavl_only); WriteModeEnableAuto - // defaults true, so the effective default after resolution is auto. - require.Equal(t, sctypes.MemiavlOnly, cfg.StateCommit.WriteMode) - require.True(t, cfg.StateCommit.WriteModeEnableAuto) -} - func TestDefaultStateStoreConfig(t *testing.T) { cfg := DefaultConfig() diff --git a/sei-cosmos/server/config/testdata/server_config.reserve.golden b/sei-cosmos/server/config/testdata/server_config.reserve.golden new file mode 100644 index 0000000000..a63756118c --- /dev/null +++ b/sei-cosmos/server/config/testdata/server_config.reserve.golden @@ -0,0 +1,149 @@ +MinGasPrices = string("0.01usei") +Pruning = string("nothing") +PruningKeepRecent = string("0") +PruningKeepEvery = string("0") +PruningInterval = string("0") +HaltHeight = uint64(0) +FreezeHeight = uint64(0) +HaltTime = uint64(0) +MinRetainBlocks = uint64(0) +InterBlockCache = bool(true) +IndexEvents = +CompactionInterval = uint64(0) +ConcurrencyWorkers = +OccEnabled = bool(true) +Telemetry.ServiceName = string("") +Telemetry.Enabled = bool(true) +Telemetry.EnableHostname = bool(false) +Telemetry.EnableHostnameLabel = bool(false) +Telemetry.EnableServiceLabel = bool(false) +Telemetry.PrometheusRetentionTime = int64(7200) +Telemetry.GlobalLabels = +API.Enable = bool(false) +API.Swagger = bool(true) +API.EnableUnsafeCORS = bool(false) +API.Address = string("tcp://0.0.0.0:1317") +API.MaxOpenConnections = uint(1000) +API.RPCReadTimeout = uint(10) +API.RPCWriteTimeout = uint(0) +API.RPCMaxBodyBytes = uint(1000000) +GRPC.Enable = bool(true) +GRPC.Address = string("0.0.0.0:9090") +GRPC.MaxRecvMsgSize = int(4194304) +GRPC.MaxOpenConnections = uint(1000) +GRPC.MaxConnectionIdle = time.Duration(5m0s) +GRPC.MaxConnectionAge = time.Duration(0s) +GRPC.MaxConnectionAgeGrace = time.Duration(0s) +GRPC.KeepaliveTime = time.Duration(2h0m0s) +GRPC.KeepaliveTimeout = time.Duration(20s) +GRPC.KeepaliveMinTime = time.Duration(5m0s) +GRPC.KeepalivePermitWithoutStream = bool(false) +Rosetta.Address = string(":8080") +Rosetta.Blockchain = string("app") +Rosetta.Network = string("network") +Rosetta.Retries = int(3) +Rosetta.Enable = bool(false) +Rosetta.Offline = bool(false) +GRPCWeb.Enable = bool(true) +GRPCWeb.Address = string("0.0.0.0:9091") +GRPCWeb.EnableUnsafeCORS = bool(false) +GRPCWeb.MaxOpenConnections = uint(1000) +Query.DisableLimits = bool(false) +Query.TrustedCIDRs = +Query.MaxLimit = uint64(1000) +Query.MaxOffset = uint64(10000) +Query.MaxIterations = uint64(11000) +StateSync.SnapshotInterval = uint64(0) +StateSync.SnapshotKeepRecent = uint32(2) +StateSync.SnapshotDirectory = string("") +StateCommit.Enable = bool(true) +StateCommit.Directory = string("") +StateCommit.AsyncCommitBuffer = int(0) +StateCommit.WriteMode = types.WriteMode("memiavl_only") +StateCommit.WriteModeEnableAuto = bool(false) +StateCommit.MemIAVLConfig.AsyncCommitBuffer = int(100) +StateCommit.MemIAVLConfig.SnapshotKeepRecent = uint32(1) +StateCommit.MemIAVLConfig.SnapshotInterval = uint32(10000) +StateCommit.MemIAVLConfig.SnapshotMinTimeInterval = uint32(3600) +StateCommit.MemIAVLConfig.SnapshotWriterLimit = int(4) +StateCommit.MemIAVLConfig.SnapshotPrefetchThreshold = float64(0.8) +StateCommit.MemIAVLConfig.SnapshotWriteRateMBps = int(100) +StateCommit.FlatKVConfig.DataDir = string("") +StateCommit.FlatKVConfig.Fsync = bool(false) +StateCommit.FlatKVConfig.AsyncWriteBuffer = int(0) +StateCommit.FlatKVConfig.SnapshotInterval = uint32(10000) +StateCommit.FlatKVConfig.SnapshotKeepRecent = uint32(1) +StateCommit.FlatKVConfig.ExternalPruning = bool(false) +StateCommit.FlatKVConfig.EnablePebbleMetrics = bool(true) +StateCommit.FlatKVConfig.EnableReadWriteMetrics = bool(false) +StateCommit.FlatKVConfig.AccountDBConfig.DataDir = string("") +StateCommit.FlatKVConfig.AccountDBConfig.EnableMetrics = bool(true) +StateCommit.FlatKVConfig.AccountDBConfig.EnableReadWriteMetrics = bool(false) +StateCommit.FlatKVConfig.AccountDBConfig.MetricsScrapeInterval = time.Duration(10s) +StateCommit.FlatKVConfig.AccountCacheConfig.ShardCount = uint64(8) +StateCommit.FlatKVConfig.AccountCacheConfig.MaxSize = uint64(1073741824) +StateCommit.FlatKVConfig.AccountCacheConfig.EstimatedOverheadPerEntry = uint64(250) +StateCommit.FlatKVConfig.AccountCacheConfig.MetricsName = string("") +StateCommit.FlatKVConfig.AccountCacheConfig.MetricsScrapeInterval = time.Duration(0s) +StateCommit.FlatKVConfig.CodeDBConfig.DataDir = string("") +StateCommit.FlatKVConfig.CodeDBConfig.EnableMetrics = bool(true) +StateCommit.FlatKVConfig.CodeDBConfig.EnableReadWriteMetrics = bool(false) +StateCommit.FlatKVConfig.CodeDBConfig.MetricsScrapeInterval = time.Duration(10s) +StateCommit.FlatKVConfig.CodeCacheConfig.ShardCount = uint64(8) +StateCommit.FlatKVConfig.CodeCacheConfig.MaxSize = uint64(536870912) +StateCommit.FlatKVConfig.CodeCacheConfig.EstimatedOverheadPerEntry = uint64(250) +StateCommit.FlatKVConfig.CodeCacheConfig.MetricsName = string("") +StateCommit.FlatKVConfig.CodeCacheConfig.MetricsScrapeInterval = time.Duration(0s) +StateCommit.FlatKVConfig.StorageDBConfig.DataDir = string("") +StateCommit.FlatKVConfig.StorageDBConfig.EnableMetrics = bool(true) +StateCommit.FlatKVConfig.StorageDBConfig.EnableReadWriteMetrics = bool(false) +StateCommit.FlatKVConfig.StorageDBConfig.MetricsScrapeInterval = time.Duration(10s) +StateCommit.FlatKVConfig.StorageCacheConfig.ShardCount = uint64(8) +StateCommit.FlatKVConfig.StorageCacheConfig.MaxSize = uint64(4294967296) +StateCommit.FlatKVConfig.StorageCacheConfig.EstimatedOverheadPerEntry = uint64(250) +StateCommit.FlatKVConfig.StorageCacheConfig.MetricsName = string("") +StateCommit.FlatKVConfig.StorageCacheConfig.MetricsScrapeInterval = time.Duration(0s) +StateCommit.FlatKVConfig.MiscDBConfig.DataDir = string("") +StateCommit.FlatKVConfig.MiscDBConfig.EnableMetrics = bool(true) +StateCommit.FlatKVConfig.MiscDBConfig.EnableReadWriteMetrics = bool(false) +StateCommit.FlatKVConfig.MiscDBConfig.MetricsScrapeInterval = time.Duration(10s) +StateCommit.FlatKVConfig.MiscCacheConfig.ShardCount = uint64(8) +StateCommit.FlatKVConfig.MiscCacheConfig.MaxSize = uint64(536870912) +StateCommit.FlatKVConfig.MiscCacheConfig.EstimatedOverheadPerEntry = uint64(250) +StateCommit.FlatKVConfig.MiscCacheConfig.MetricsName = string("") +StateCommit.FlatKVConfig.MiscCacheConfig.MetricsScrapeInterval = time.Duration(0s) +StateCommit.FlatKVConfig.ReaderThreadsPerCore = float64(2) +StateCommit.FlatKVConfig.ReaderConstantThreadCount = int(0) +StateCommit.FlatKVConfig.ReaderPoolQueueSize = int(1024) +StateCommit.FlatKVConfig.MiscPoolThreadsPerCore = float64(4) +StateCommit.FlatKVConfig.MiscConstantThreadCount = int(0) +StateCommit.FlatKVConfig.LtHashThreadsPerCore = float64(1) +StateCommit.HistoricalProofMaxInFlight = int(1) +StateCommit.HistoricalProofRateLimit = float64(1) +StateCommit.HistoricalProofBurst = int(1) +StateCommit.HashLogger.Enable = bool(true) +StateCommit.HashLogger.Directory = string("") +StateCommit.HashLogger.BlocksToRetain = uint(0) +StateCommit.HashLogger.TargetFileSize = uint(16777216) +StateCommit.HashLogger.MaxDiskSize = uint(17179869184) +StateCommit.HashLogger.Version = string("") +StateStore.Enable = bool(true) +StateStore.DBDirectory = string("") +StateStore.Backend = string("pebbledb") +StateStore.AsyncWriteBuffer = int(100) +StateStore.KeepRecent = int(100000) +StateStore.PruneIntervalSeconds = int(600) +StateStore.ImportNumWorkers = int(1) +StateStore.EnableReadWriteMetrics = bool(false) +StateStore.KeepLastVersion = bool(true) +StateStore.UseDefaultComparer = bool(false) +StateStore.SnapshotEnable = bool(false) +StateStore.SnapshotInterval = int64(0) +StateStore.SnapshotKeepRecent = int(0) +StateStore.SnapshotMinTimeInterval = time.Duration(0s) +StateStore.ExternalPruning = bool(false) +StateStore.EVMSplit = bool(false) +StateStore.EVMDBDirectory = string("") +StateStore.SeparateEVMSubDBs = bool(false) +Genesis.StreamImport = bool(false) +Genesis.GenesisStreamFile = string("") diff --git a/sei-cosmos/server/config/write_mode_default_test.go b/sei-cosmos/server/config/write_mode_default_test.go new file mode 100644 index 0000000000..2ed243260a --- /dev/null +++ b/sei-cosmos/server/config/write_mode_default_test.go @@ -0,0 +1,112 @@ +//go:build !mock_chain_validation + +package config + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" +) + +// serverConfigRecord names this build's defaults record. The two builds declare +// different defaults, so they keep separate records: a shared one would be +// rewritten by whichever build regenerated it last, and the value it lost is the +// one the reviewer needed to see. +const serverConfigRecord = "server_config" + +// TestGetConfigLegacyMemiavlOnlyResolvesToAuto guards the existing-fleet +// upgrade path: a config written by an older binary carries an explicit +// sc-write-mode = "memiavl_only" but no sc-write-mode-enable-auto key. The absent +// key must default to true so the node resolves to auto and can follow a +// governance-driven migration without any app.toml edit. +func TestGetConfigLegacyMemiavlOnlyResolvesToAuto(t *testing.T) { + v := viper.New() + + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode", "memiavl_only") + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.True(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, + "absent sc-write-mode-enable-auto must default to true and override an explicit memiavl_only") +} + +func TestGetConfigLegacyCosmosOnlyResolvesToAuto(t *testing.T) { + v := viper.New() + + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode", "cosmos_only") + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.True(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, + "v6.4/v6.5 app.toml files with cosmos_only must parse before auto mode is applied") +} + +// TestGetConfigPinnedModeRequiresAutoDisabled verifies that an explicit +// sc-write-mode is only honored when sc-write-mode-enable-auto = false. With auto +// enabled (the default), the explicit mode is ignored and the node runs in auto. +func TestGetConfigPinnedModeRequiresAutoDisabled(t *testing.T) { + for _, mode := range []sctypes.WriteMode{ + sctypes.FlatKVOnly, + sctypes.EVMMigrated, + sctypes.TestOnlyDualWrite, + } { + t.Run(string(mode)+"/auto-disabled-pins", func(t *testing.T) { + v := viper.New() + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode-enable-auto", false) + v.Set("state-commit.sc-write-mode", string(mode)) + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.False(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, mode, cfg.StateCommit.WriteMode, + "with auto disabled the explicit mode must be honored as a pin") + }) + + t.Run(string(mode)+"/auto-enabled-overrides", func(t *testing.T) { + v := viper.New() + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode", string(mode)) + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.True(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, + "with auto enabled (default) the explicit mode must be ignored in favor of auto") + }) + } +} + +func TestGetConfigEmptyWriteModeUsesDefault(t *testing.T) { + v := viper.New() + + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, + "unset sc-write-mode must fall back to the in-code default") +} + +func TestDefaultStateCommitConfig(t *testing.T) { + cfg := DefaultConfig() + + require.True(t, cfg.StateCommit.Enable) + require.Empty(t, cfg.StateCommit.Directory) + // WriteMode is the fixed fallback (memiavl_only); WriteModeEnableAuto + // defaults true, so the effective default after resolution is auto. + require.Equal(t, sctypes.MemiavlOnly, cfg.StateCommit.WriteMode) + require.True(t, cfg.StateCommit.WriteModeEnableAuto) +} diff --git a/sei-cosmos/server/config/write_mode_mock_chain_validation_test.go b/sei-cosmos/server/config/write_mode_mock_chain_validation_test.go new file mode 100644 index 0000000000..1314297d6a --- /dev/null +++ b/sei-cosmos/server/config/write_mode_mock_chain_validation_test.go @@ -0,0 +1,113 @@ +//go:build mock_chain_validation + +package config + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" +) + +// These records mirror write_mode_default_test.go for the reserve build, where +// sc-write-mode-enable-auto defaults to false rather than true. The rule is +// unchanged — an absent key takes the in-code default, and the default decides +// whether an explicit sc-write-mode is honored — so every resolution below is +// the opposite of the stock one for the same input. + +// serverConfigRecord names this build's defaults record, separate from the stock +// build's so both declared values stay readable side by side. +const serverConfigRecord = "server_config.reserve" + +func TestGetConfigLegacyMemiavlOnlyIsPinnedOnAReserveBuild(t *testing.T) { + v := viper.New() + + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode", "memiavl_only") + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.False(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, sctypes.MemiavlOnly, cfg.StateCommit.WriteMode, + "absent sc-write-mode-enable-auto must default to false and honor the explicit memiavl_only") +} + +func TestGetConfigLegacyCosmosOnlyIsPinnedOnAReserveBuild(t *testing.T) { + v := viper.New() + + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode", "cosmos_only") + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.False(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, sctypes.MemiavlOnly, cfg.StateCommit.WriteMode, + "v6.4/v6.5 app.toml files with cosmos_only must parse to memiavl_only and then be honored") +} + +// TestGetConfigPinnedModeNeedsNoAutoKeyOnAReserveBuild is the inverse of the +// stock TestGetConfigPinnedModeRequiresAutoDisabled: pinning needs no key here, +// and setting the key to true is what un-pins the node. assertReserveNodeAllowed +// refuses to start in that state. +func TestGetConfigPinnedModeNeedsNoAutoKeyOnAReserveBuild(t *testing.T) { + for _, mode := range []sctypes.WriteMode{ + sctypes.FlatKVOnly, + sctypes.EVMMigrated, + sctypes.TestOnlyDualWrite, + } { + t.Run(string(mode)+"/auto-key-absent-pins", func(t *testing.T) { + v := viper.New() + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode", string(mode)) + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.False(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, mode, cfg.StateCommit.WriteMode, + "with auto defaulted off the explicit mode must be honored as a pin") + }) + + t.Run(string(mode)+"/auto-enabled-overrides", func(t *testing.T) { + v := viper.New() + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode-enable-auto", true) + v.Set("state-commit.sc-write-mode", string(mode)) + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.True(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, + "an explicit sc-write-mode-enable-auto = true must still win over the default") + }) + } +} + +func TestGetConfigEmptyWriteModeUsesDefaultOnAReserveBuild(t *testing.T) { + v := viper.New() + + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.Equal(t, sctypes.MemiavlOnly, cfg.StateCommit.WriteMode, + "an app.toml carrying neither key must resolve to a pinned memiavl_only, "+ + "which is what lets a reserve start with no configuration at all") +} + +func TestDefaultStateCommitConfigOnAReserveBuild(t *testing.T) { + cfg := DefaultConfig() + + require.True(t, cfg.StateCommit.Enable) + require.Empty(t, cfg.StateCommit.Directory) + // WriteMode is the same fixed fallback as the stock build; only + // WriteModeEnableAuto moves, and it is what makes that fallback effective. + require.Equal(t, sctypes.MemiavlOnly, cfg.StateCommit.WriteMode) + require.False(t, cfg.StateCommit.WriteModeEnableAuto) +} diff --git a/sei-db/config/reserve_defaults_default.go b/sei-db/config/reserve_defaults_default.go new file mode 100644 index 0000000000..3d4755a233 --- /dev/null +++ b/sei-db/config/reserve_defaults_default.go @@ -0,0 +1,11 @@ +//go:build !mock_chain_validation + +package config + +// applyReserveDefaults returns the state-commit defaults for this build's node +// role. Production builds have no reserve role and use the defaults unchanged. +func applyReserveDefaults(cfg StateCommitConfig) StateCommitConfig { return cfg } + +// reserveStateCommitConfigTemplate is the app.toml fragment this build adds to +// the [state-commit] section. Production builds add nothing. +const reserveStateCommitConfigTemplate = "" diff --git a/sei-db/config/reserve_defaults_default_test.go b/sei-db/config/reserve_defaults_default_test.go new file mode 100644 index 0000000000..4872de1e3e --- /dev/null +++ b/sei-db/config/reserve_defaults_default_test.go @@ -0,0 +1,31 @@ +//go:build !mock_chain_validation + +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" +) + +func TestDefaultStateCommitConfigWriteMode(t *testing.T) { + cfg := DefaultStateCommitConfig() + // The raw default is the fixed fallback; auto comes from WriteModeEnableAuto + // via ApplyWriteModeAuto at the config-parse boundary. + require.Equal(t, types.MemiavlOnly, cfg.WriteMode) + require.True(t, cfg.WriteModeEnableAuto) +} + +// TestStateCommitConfigTemplateOmitsWriteModeEnableAuto records that a stock +// app.toml carries no sc-write-mode-enable-auto key. An operator who wants to pin +// a mode has to add the key by hand; the key's absence is what makes an unedited +// config resolve to auto. The template does name the key in a comment, so the +// assertion is on assignments in the rendered file rather than on the text. +func TestStateCommitConfigTemplateOmitsWriteModeEnableAuto(t *testing.T) { + require.Empty(t, reserveStateCommitConfigTemplate) + require.NotContains(t, renderedAssignments(t), "sc-write-mode-enable-auto") + require.Contains(t, renderedAssignments(t), "sc-write-mode", + "sc-write-mode itself is rendered, which is why its companion key reads as a defaulted one") +} diff --git a/sei-db/config/reserve_defaults_mock_chain_validation.go b/sei-db/config/reserve_defaults_mock_chain_validation.go new file mode 100644 index 0000000000..3ac0d4ce6d --- /dev/null +++ b/sei-db/config/reserve_defaults_mock_chain_validation.go @@ -0,0 +1,31 @@ +//go:build mock_chain_validation + +package config + +// applyReserveDefaults returns the state-commit defaults for this build's node +// role. This build starts only as a reserve, so it honors the explicit write +// mode rather than resolving to auto. +func applyReserveDefaults(cfg StateCommitConfig) StateCommitConfig { + // sc-write-mode-enable-auto is never rendered into app.toml, so this in-code + // default is what a node resolves against unless an operator adds the key by + // hand. At true the explicit sc-write-mode is discarded and the node joins a + // governance-driven migration on the first block after the batch size rises, + // which spends the reserve with nothing to signal that it happened. Flipping + // it here makes a reserve correct with no operator configuration at all; an + // explicit key in app.toml still wins, and assertReserveNodeAllowed refuses + // the node when it wins in the wrong direction. + cfg.WriteModeEnableAuto = false + return cfg +} + +// reserveStateCommitConfigTemplate is the app.toml fragment this build adds to +// the [state-commit] section. It renders sc-write-mode-enable-auto, which the +// stock template omits. +const reserveStateCommitConfigTemplate = ` +# sc-write-mode-enable-auto is rendered by this build and omitted by the stock +# one, because the two default it differently: false here, true there. At false +# the explicit sc-write-mode above is honored, which is what makes this node a +# reserve. Set it to true and this build refuses to start, since an unpinned +# node joins the migration and stops being a recovery source. +sc-write-mode-enable-auto = {{ .StateCommit.WriteModeEnableAuto }} +` diff --git a/sei-db/config/reserve_defaults_mock_chain_validation_test.go b/sei-db/config/reserve_defaults_mock_chain_validation_test.go new file mode 100644 index 0000000000..e2d4675706 --- /dev/null +++ b/sei-db/config/reserve_defaults_mock_chain_validation_test.go @@ -0,0 +1,30 @@ +//go:build mock_chain_validation + +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" +) + +func TestDefaultStateCommitConfigWriteModeOnAReserveBuild(t *testing.T) { + cfg := DefaultStateCommitConfig() + // The raw default is the same fixed fallback as the stock build. Only + // WriteModeEnableAuto moves, and at false ApplyWriteModeAuto leaves the + // fallback standing instead of replacing it with auto. + require.Equal(t, types.MemiavlOnly, cfg.WriteMode) + require.False(t, cfg.WriteModeEnableAuto) +} + +// TestStateCommitConfigTemplateRendersWriteModeEnableAuto records that this build +// writes the key the stock template omits. Rendering it is what keeps a generated +// app.toml truthful: the two builds default the key differently, so an absent key +// would describe the wrong node on one of them. +func TestStateCommitConfigTemplateRendersWriteModeEnableAuto(t *testing.T) { + require.NotEmpty(t, reserveStateCommitConfigTemplate) + require.Contains(t, renderedAssignments(t), "sc-write-mode-enable-auto") + require.Contains(t, renderedAssignments(t), "sc-write-mode") +} diff --git a/sei-db/config/reserve_defaults_test.go b/sei-db/config/reserve_defaults_test.go new file mode 100644 index 0000000000..4b5856581a --- /dev/null +++ b/sei-db/config/reserve_defaults_test.go @@ -0,0 +1,37 @@ +package config + +import ( + "bytes" + "strings" + "testing" + "text/template" + + "github.com/stretchr/testify/require" +) + +// renderedAssignments returns the keys StateCommitConfigTemplate assigns when it +// is rendered with the in-code defaults, one name per entry. Comments are dropped, +// so a key the template only discusses in prose is not mistaken for one it writes. +func renderedAssignments(t *testing.T) []string { + t.Helper() + + tmpl, err := template.New("sc").Parse(StateCommitConfigTemplate) + require.NoError(t, err) + + var rendered bytes.Buffer + require.NoError(t, tmpl.Execute(&rendered, struct{ StateCommit StateCommitConfig }{ + StateCommit: DefaultStateCommitConfig(), + })) + + var keys []string + for _, line := range strings.Split(rendered.String(), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { + continue + } + key, _, ok := strings.Cut(line, "=") + require.True(t, ok, "unexpected non-assignment line in the rendered template: %q", line) + keys = append(keys, strings.TrimSpace(key)) + } + return keys +} diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index 6b3bda0a54..9682a21205 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -109,7 +109,7 @@ type StateCommitConfig struct { // DefaultStateCommitConfig returns the default StateCommitConfig func DefaultStateCommitConfig() StateCommitConfig { - return StateCommitConfig{ + return applyReserveDefaults(StateCommitConfig{ Enable: true, WriteMode: types.MemiavlOnly, WriteModeEnableAuto: true, @@ -119,7 +119,7 @@ func DefaultStateCommitConfig() StateCommitConfig { HistoricalProofRateLimit: DefaultSCHistoricalProofRateLimit, HistoricalProofBurst: DefaultSCHistoricalProofBurst, HashLogger: DefaultHashLoggerConfig(), - } + }) } // ApplyWriteModeAuto resolves the effective write mode from the diff --git a/sei-db/config/sc_config_test.go b/sei-db/config/sc_config_test.go index 3e3a13fc1f..75d79f3c37 100644 --- a/sei-db/config/sc_config_test.go +++ b/sei-db/config/sc_config_test.go @@ -32,13 +32,9 @@ func TestApplyWriteModeAuto(t *testing.T) { } } -func TestDefaultStateCommitConfigWriteMode(t *testing.T) { - cfg := DefaultStateCommitConfig() - // The raw default is the fixed fallback; auto comes from WriteModeEnableAuto - // via ApplyWriteModeAuto at the config-parse boundary. - require.Equal(t, types.MemiavlOnly, cfg.WriteMode) - require.True(t, cfg.WriteModeEnableAuto) -} +// The WriteMode default records live in reserve_defaults_default_test.go and its +// mock_chain_validation counterpart, which default WriteModeEnableAuto the two +// opposite ways. func TestParseSCWriteMode(t *testing.T) { parsed, err := ParseSCWriteMode("cosmos_only") diff --git a/sei-db/config/toml.go b/sei-db/config/toml.go index 34e0d8b42b..158ed5bc47 100644 --- a/sei-db/config/toml.go +++ b/sei-db/config/toml.go @@ -1,7 +1,14 @@ package config -// StateCommitConfigTemplate defines the configuration template for state-commit -const StateCommitConfigTemplate = ` +// StateCommitConfigTemplate defines the configuration template for state-commit. +// The reserve fragment sits between the two halves so that the keys it adds land +// inside [state-commit] rather than in the [state-commit.flatkv] subsection the +// section ends with. +const StateCommitConfigTemplate = stateCommitConfigTemplateHead + + reserveStateCommitConfigTemplate + + stateCommitConfigTemplateTail + +const stateCommitConfigTemplateHead = ` ############################################################################### ### State Commit Configuration ### ############################################################################### @@ -68,7 +75,9 @@ sc-snapshot-write-rate-mbps = {{ .StateCommit.MemIAVLConfig.SnapshotWriteRateMBp # Valid values: memiavl_only, migrate_evm, evm_migrated, migrate_all_but_bank, # all_migrated_but_bank, migrate_bank, flatkv_only, test_only_dual_write, auto. sc-write-mode = "{{ .StateCommit.WriteMode }}" +` +const stateCommitConfigTemplateTail = ` # HashLogger records a per-block CSV of named hashes (memIAVL module/root hashes, flatKV DB/root # hashes, the app hash, the block hash, and the changeset hash) so block-hash computation can be # studied and compared across nodes. It is a debugging/forensics tool; enabled by default.