diff --git a/AGENTS.md b/AGENTS.md index 05325c09a0..4bc1538058 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,16 +17,25 @@ progressively the deeper you go. Existing package guides include: ## Code style -All Go files must be `gofmt`-compliant. After modifying any `.go` file, run: +All Go files must be both `gofmt`- and `goimports`-compliant (`.golangci.yml` +enables the `gofmt` and `goimports` formatters). After modifying **any** `.go` +file, run **both** tools on **every** file you touched — not just the ones you +think changed formatting: ```bash -gofmt -s -w +gofmt -s -w ... +goimports -w ... # groups/orders imports; catches the goimports linter ``` -Or verify the whole tree (prints nothing when everything is clean): +`goimports` is required in addition to `gofmt`: `gofmt` alone does not separate +the stdlib import group from third-party imports, so a `gofmt`-clean file can +still fail the `goimports` linter. + +Verify the whole tree (each prints nothing when everything is clean): ```bash gofmt -s -l . +goimports -l . ``` ## Lint, build & test diff --git a/app/abci.go b/app/abci.go index 9683d03f2e..cc9a111cbd 100644 --- a/app/abci.go +++ b/app/abci.go @@ -13,6 +13,7 @@ import ( otelmetrics "go.opentelemetry.io/otel/metric" "github.com/sei-protocol/sei-chain/app/legacyabci" + "github.com/sei-protocol/sei-chain/app/migration" "github.com/sei-protocol/sei-chain/sei-cosmos/tasks" "github.com/sei-protocol/sei-chain/sei-cosmos/telemetry" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" @@ -53,12 +54,55 @@ func (app *App) BeginBlock( if app.HardForkManager.TargetHeightReached(ctx) { app.HardForkManager.ExecuteForTargetHeight(ctx) } + app.applyMigrationBatchSize(ctx) legacyabci.BeginBlock(ctx, height, votes, byzantineValidators, app.BeginBlockKeepers) return abci.ResponseBeginBlock{ Events: sdk.MarkEventsToIndex(ctx.EventManager().ABCIEvents(), app.IndexEvents), } } +// applyMigrationBatchSize paces the SC store's background data migration at the network-agreed rate. +// The NumKeysToMigratePerBlock gov param is read from chain state so every node +// applies the same value each block; a per-node rate would diverge the +// AppHash. 0 (the default until a gov proposal raises it) leaves the migration +// paused; it is the sole source of the rate (there is no node-local fallback). +func (app *App) applyMigrationBatchSize(ctx sdk.Context) { + if app.rootStore == nil { + return + } + numKeys := migration.DefaultNumKeysToMigratePerBlock + if subspace, ok := app.ParamsKeeper.GetSubspace(migration.SubspaceName); ok { + // The migration subspace has no owning module to seed it in InitGenesis, + // so lazily persist the default the first time we see it unset. This is + // deterministic across nodes (every node runs BeginBlock identically) and + // makes the param visible to gov: ParameterChangeProposal submission only + // accepts a change when subspace.Has reports the key is already stored. + if !subspace.Has(ctx, migration.KeyNumKeysToMigratePerBlock) { + subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, migration.DefaultNumKeysToMigratePerBlock) + } + subspace.GetIfExists(ctx, migration.KeyNumKeysToMigratePerBlock, &numKeys) + } + // Defense-in-depth: gov validation already rejects values above + // MaxNumKeysToMigratePerBlock, but clamp here too so an out-of-range value + // reaching state via any path can never overflow the int cast or trigger an + // oversized preallocation in the migration iterator. The clamp is + // deterministic across nodes. + if numKeys > migration.MaxNumKeysToMigratePerBlock { + numKeys = migration.MaxNumKeysToMigratePerBlock + } + if err := app.rootStore.SetMigrationBatchSize(int(numKeys)); err != nil { + // Never panic on the migration-rate update: log and continue. AppHash + // verification is the safety net. If the rate/mode update fails on only + // some nodes, those nodes' AppHash diverges and the normal AppHash + // comparison halts them at the next block — no proactive panic needed. + // If it fails on every node, all stay in the same (old) mode with an + // identical AppHash, so the chain keeps moving and the level-triggered + // trigger re-fires on a later block. Panicking here would needlessly + // halt the whole chain in that all-fail case. + logger.Error("failed to set SC migration batch size; continuing", "err", err) + } +} + func (app *App) MidBlock(ctx sdk.Context, height int64) []abci.Event { _, span := app.GetBaseApp().TracingInfo.StartWithContext("MidBlock", ctx.TraceSpanContext()) defer span.End() diff --git a/app/abci_test.go b/app/abci_test.go new file mode 100644 index 0000000000..db6ea5298d --- /dev/null +++ b/app/abci_test.go @@ -0,0 +1,125 @@ +package app + +import ( + "context" + "math" + "testing" + "time" + + "github.com/sei-protocol/sei-chain/app/migration" + abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + "github.com/stretchr/testify/require" +) + +// TestMigrationSubspaceRegistered verifies the generic "migration" params +// subspace is wired with its key table so governance can edit +// NumKeysToMigratePerBlock via a ParameterChangeProposal. +func TestMigrationSubspaceRegistered(t *testing.T) { + a := Setup(t, false, false, false) + subspace, ok := a.ParamsKeeper.GetSubspace(migration.SubspaceName) + require.True(t, ok, "migration subspace must be registered") + require.True(t, subspace.HasKeyTable(), "migration subspace must have a key table") + + ctx := a.NewContext(false, tmproto.Header{Height: 1, ChainID: "sei-test", Time: time.Now()}) + subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(123)) + var got uint64 + subspace.GetIfExists(ctx, migration.KeyNumKeysToMigratePerBlock, &got) + require.Equal(t, uint64(123), got) +} + +// TestApplyMigrationBatchSize covers the BeginBlock push: the gov param is +// read from chain state and forwarded into the SC commit store. +func TestApplyMigrationBatchSize(t *testing.T) { + a := Setup(t, false, false, false) + ctx := a.NewContext(false, tmproto.Header{Height: 1, ChainID: "sei-test", Time: time.Now()}) + + subspace, ok := a.ParamsKeeper.GetSubspace(migration.SubspaceName) + require.True(t, ok) + + // Unset param: the store receives the default (0 = paused). + a.applyMigrationBatchSize(ctx) + got, ok := a.rootStore.GetMigrationBatchSize() + require.True(t, ok, "SC store should track a migration batch size") + require.Equal(t, 0, got) + + // Governance raises the rate: BeginBlock forwards the new value. + subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(500)) + a.applyMigrationBatchSize(ctx) + got, _ = a.rootStore.GetMigrationBatchSize() + require.Equal(t, 500, got) + + // Defense-in-depth: an out-of-range value reaching state (gov validation + // already rejects these) is clamped to the sane maximum, never overflowing + // the int cast or the migration iterator preallocation. + subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(math.MaxUint64)) + a.applyMigrationBatchSize(ctx) + got, _ = a.rootStore.GetMigrationBatchSize() + require.Equal(t, int(migration.MaxNumKeysToMigratePerBlock), got) +} + +// TestBeginBlockAppliesMigrationBatchSize exercises the full BeginBlock path +// (not the helper in isolation): it mimics a governance ParameterChangeProposal +// having set NumKeysToMigratePerBlock, then runs app.BeginBlock and asserts the +// new rate landed in the SC commit store. +func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { + a := Setup(t, false, false, false) + ctx := a.NewContext(false, tmproto.Header{Height: 2, ChainID: "sei-test", Time: time.Now()}) + + // Sanity: nothing set yet, so the store is paused at 0. + before, ok := a.rootStore.GetMigrationBatchSize() + require.True(t, ok) + require.Equal(t, 0, before) + + // Simulate the gov proposal landing in chain state. + subspace, ok := a.ParamsKeeper.GetSubspace(migration.SubspaceName) + require.True(t, ok) + subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(321)) + + // Run the real BeginBlock (checkHeight=false to skip height validation). + require.NotPanics(t, func() { + a.BeginBlock(ctx, 2, nil, nil, false) + }) + + after, _ := a.rootStore.GetMigrationBatchSize() + require.Equal(t, 321, after, "BeginBlock should push the gov param into the SC store") +} + +// TestMigrationBatchSizeTakesEffectNextBlock is the full end-to-end timing +// check: a governance proposal committed in block N (written into the block's +// deliver state, then Commit) only changes the SC store's migration rate when +// block N+1's BeginBlock runs and reads it from committed state. +func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { + a := Setup(t, false, false, false) + bg := context.Background() + + // Block 1: BeginBlock runs first (param still unset), then the gov + // proposal lands by writing into this block's deliver state, then Commit + // persists it to the committed multistore. + _, err := a.FinalizeBlock(bg, &abci.RequestFinalizeBlock{ + Header: &tmproto.Header{ChainID: "sei-test", Height: 1, Time: time.Now()}, + }) + require.NoError(t, err) + + subspace, ok := a.ParamsKeeper.GetSubspace(migration.SubspaceName) + require.True(t, ok) + subspace.Set(a.GetContextForDeliverTx([]byte{}), migration.KeyNumKeysToMigratePerBlock, uint64(640)) + + _, err = a.Commit(bg) + require.NoError(t, err) + + // The param was committed in block 1, but BeginBlock(1) ran before it + // existed, so the rate is still paused at this point. + got, ok := a.rootStore.GetMigrationBatchSize() + require.True(t, ok) + require.Equal(t, 0, got, "param committed in block 1 must not take effect within block 1") + + // Block 2: BeginBlock reads the now-committed param and applies it. + _, err = a.FinalizeBlock(bg, &abci.RequestFinalizeBlock{ + Header: &tmproto.Header{ChainID: "sei-test", Height: 2, Time: time.Now().Add(time.Second)}, + }) + require.NoError(t, err) + + got, _ = a.rootStore.GetMigrationBatchSize() + require.Equal(t, 640, got, "migration rate must take effect on the block after the param is committed") +} diff --git a/app/app.go b/app/app.go index b1693faa53..24241cf468 100644 --- a/app/app.go +++ b/app/app.go @@ -42,7 +42,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/server/config" servertypes "github.com/sei-protocol/sei-chain/sei-cosmos/server/types" storetypes "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" - storev2_rootmulti "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" genesistypes "github.com/sei-protocol/sei-chain/sei-cosmos/types/genesis" @@ -110,6 +109,7 @@ import ( "github.com/sei-protocol/sei-chain/app/antedecorators" "github.com/sei-protocol/sei-chain/app/benchmark" "github.com/sei-protocol/sei-chain/app/legacyabci" + "github.com/sei-protocol/sei-chain/app/migration" appparams "github.com/sei-protocol/sei-chain/app/params" "github.com/sei-protocol/sei-chain/app/upgrades" v0upgrade "github.com/sei-protocol/sei-chain/app/upgrades/v0" @@ -140,6 +140,7 @@ import ( tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" wasmkeeper "github.com/sei-protocol/sei-chain/sei-wasmd/x/wasm/keeper" + "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" "github.com/sei-protocol/sei-chain/utils" utilmetrics "github.com/sei-protocol/sei-chain/utils/metrics" "github.com/sei-protocol/sei-chain/wasmbinding" @@ -469,6 +470,7 @@ type App struct { genesisImportConfig genesistypes.GenesisImportConfig stateStore seidb.StateStore + rootStore *rootmulti.Store receiptStore receipt.ReceiptStore forkInitializer func(sdk.Context) @@ -549,6 +551,16 @@ func New( option(app) } + // The storev2 rootmulti store is the only supported commit multistore; its + // composite SC backend drives the in-flight memiavl->flatkv migration that + // BeginBlock paces via the migration gov param. Fail fast if the legacy + // root multistore is somehow in use. + rootStore, ok := app.CommitMultiStore().(*rootmulti.Store) + if !ok { + panic(fmt.Sprintf("unsupported commit multistore %T: expected *rootmulti.Store", app.CommitMultiStore())) + } + app.rootStore = rootStore + app.ParamsKeeper = initParamsKeeper(appCodec, cdc, keys[paramstypes.StoreKey], tkeys[paramstypes.TStoreKey]) // set the BaseApp's parameter store @@ -746,7 +758,7 @@ func New( app.EvmKeeper.SetTraceDB(traceDB) if app.evmRPCConfig.TraceBakeUseSnapshot { - if rs, ok := app.CommitMultiStore().(*storev2_rootmulti.Store); ok { + if rs, ok := app.CommitMultiStore().(*rootmulti.Store); ok { app.EvmKeeper.SetTraceSnapshotStore(evmkeeper.NewTraceSnapshotStore(app.evmRPCConfig.TraceBakeSnapshotWindow)) app.EvmKeeper.SetTraceSnapshotCapture(rs.SnapshotSCStore) } else { @@ -2670,7 +2682,7 @@ func (app *App) SnapshotAwareRPCContextProvider() evmrpc.TraceContextProvider { return app.RPCContextProvider(i), func() {} }) } - rs, ok := app.CommitMultiStore().(*storev2_rootmulti.Store) + rs, ok := app.CommitMultiStore().(*rootmulti.Store) if !ok { return evmrpc.TraceContextProvider(func(i int64) (sdk.Context, func()) { return app.RPCContextProvider(i), func() {} @@ -2933,6 +2945,7 @@ func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino paramsKeeper.Subspace(evmtypes.ModuleName) paramsKeeper.Subspace(epochmoduletypes.ModuleName) paramsKeeper.Subspace(tokenfactorytypes.ModuleName) + paramsKeeper.Subspace(migration.SubspaceName).WithKeyTable(migration.ParamKeyTable()) // this line is used by starport scaffolding # stargate/app/paramSubspace return paramsKeeper diff --git a/app/migration/params.go b/app/migration/params.go new file mode 100644 index 0000000000..e47dd2c227 --- /dev/null +++ b/app/migration/params.go @@ -0,0 +1,72 @@ +// Package migration defines the module-agnostic governance parameters +// that control the state-commitment store's background data migration +// (currently memiavl->flatkv). +// +// These live outside any business module on purpose: the migration rate +// applies to whichever stores the SC router is migrating, so it is an +// app/storage-level concern rather than EVM-specific. The value is held in a +// dedicated x/params subspace and is editable via the standard +// ParameterChangeProposal gov flow. The app reads it once per block in +// BeginBlock and pushes it into the SC commit store. +// +// Caveat: this subspace has no owning AppModule, so the x/params module's +// ExportGenesis (which only emits Fees/CosmosGas params) does not serialize +// NumKeysToMigratePerBlock. A `seid export` taken mid-migration therefore +// omits the rate, and a chain bootstrapped from that genesis re-seeds the +// default (0, paused) on the first BeginBlock. This is not consensus-fatal — +// re-issue the ParameterChangeProposal on the new chain to resume the drain — +// but operators forking/recovering via export must be aware of it. +package migration + +import ( + "fmt" + + paramtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/params/types" +) + +// SubspaceName is the x/params subspace that holds storage-migration controls. +const SubspaceName = "migration" + +// KeyNumKeysToMigratePerBlock is the param key for the number of keys the +// in-flight SC migration advances per block. +var KeyNumKeysToMigratePerBlock = []byte("NumKeysToMigratePerBlock") + +// DefaultNumKeysToMigratePerBlock leaves the migration paused. While it is 0 +// (the default until a gov proposal raises it) the SC store does no migration +// work; this param is the sole source of the per-block rate. +const DefaultNumKeysToMigratePerBlock uint64 = 0 + +// MaxNumKeysToMigratePerBlock bounds the governance-controlled rate. The value +// flows into MemiavlMigrationIterator.NextBatch, which preallocates a slice of +// that capacity; an unbounded uint64 (e.g. a fat-fingered or malicious +// proposal) would deterministically panic (makeslice: cap out of range) or OOM +// every validator at the same height — an unrecoverable chain halt. 1,000,000 +// keys/block is far above any realistic drain rate yet caps the preallocation +// at a safe size, so we reject anything larger at proposal-validation time. +const MaxNumKeysToMigratePerBlock uint64 = 1_000_000 + +// ParamKeyTable returns the key table for the migration subspace. +func ParamKeyTable() paramtypes.KeyTable { + return paramtypes.NewKeyTable( + paramtypes.NewParamSetPair(KeyNumKeysToMigratePerBlock, new(uint64), validateNumKeysToMigratePerBlock), + ) +} + +// validateNumKeysToMigratePerBlock type-checks the value and bounds it to +// MaxNumKeysToMigratePerBlock. 0 means "paused"; any rate in [0, max] is a +// valid (consensus-deterministic) value. Rejecting oversized values here, at +// proposal-submission time, keeps them out of chain state where they would +// otherwise OOM/panic every validator (see MaxNumKeysToMigratePerBlock). +func validateNumKeysToMigratePerBlock(i interface{}) error { + v, ok := i.(uint64) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + if v > MaxNumKeysToMigratePerBlock { + return fmt.Errorf( + "NumKeysToMigratePerBlock must be <= %d, got %d", + MaxNumKeysToMigratePerBlock, v, + ) + } + return nil +} diff --git a/app/migration/params_test.go b/app/migration/params_test.go new file mode 100644 index 0000000000..82b1ebc09f --- /dev/null +++ b/app/migration/params_test.go @@ -0,0 +1,43 @@ +package migration + +import ( + "math" + "testing" + + paramtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/params/types" + "github.com/stretchr/testify/require" +) + +func TestDefaultLeavesMigrationPaused(t *testing.T) { + require.Equal(t, uint64(0), DefaultNumKeysToMigratePerBlock, + "default must be 0 so the migration stays paused until governance raises it") +} + +func TestParamKeyTableRegistersKey(t *testing.T) { + table := ParamKeyTable() + + // Re-registering the same key must panic with "duplicate parameter key", + // which proves NumKeysToMigratePerBlock is already in the returned table. + require.PanicsWithValue(t, "duplicate parameter key", func() { + table.RegisterType(paramtypes.NewParamSetPair( + KeyNumKeysToMigratePerBlock, new(uint64), validateNumKeysToMigratePerBlock)) + }) +} + +func TestValidateNumKeysToMigratePerBlock(t *testing.T) { + // Any uint64 in [0, max] is valid, including 0 (paused) and the boundary. + for _, v := range []uint64{0, 1, 1024, MaxNumKeysToMigratePerBlock} { + require.NoError(t, validateNumKeysToMigratePerBlock(v), "uint64 %d should be valid", v) + } + + // Values above the cap are rejected so they can never reach chain state + // and OOM/panic the migration iterator's preallocation. + for _, v := range []uint64{MaxNumKeysToMigratePerBlock + 1, 1 << 40, math.MaxUint64} { + require.Error(t, validateNumKeysToMigratePerBlock(v), "uint64 %d should be rejected as too large", v) + } + + // Wrong types are rejected. + for _, v := range []interface{}{int(1), int64(1), "1", float64(1), nil} { + require.Error(t, validateNumKeysToMigratePerBlock(v), "value %v (%T) should be rejected", v, v) + } +} diff --git a/app/seidb.go b/app/seidb.go index be6f550365..f419a98a60 100644 --- a/app/seidb.go +++ b/app/seidb.go @@ -30,15 +30,8 @@ const ( FlagSCHistoricalProofRateLimit = "state-commit.sc-historical-proof-rate-limit" FlagSCHistoricalProofBurst = "state-commit.sc-historical-proof-burst" FlagSCWriteMode = "state-commit.sc-write-mode" - // Per-block batch size used by the MigrationManager when sc-write-mode - // is one of the in-flight modes (migrate_evm, migrate_bank, - // migrate_all_but_bank). Optional: when unset in app.toml the field - // stays at DefaultStateCommitConfig().KeysToMigratePerBlock (= 1024), - // which is appropriate for production drains. Lowering it spreads the - // migration across more blocks, which is useful for tests that need to - // exercise the resume / hybrid-read path mid-flight. - FlagSCKeysToMigratePerBlock = "state-commit.sc-keys-to-migrate-per-block" - FlagSCFlatKVReadWriteMetrics = "state-commit.flatkv.enable-read-write-metrics" + FlagSCWriteModeEnableAuto = "state-commit.sc-write-mode-enable-auto" + FlagSCFlatKVReadWriteMetrics = "state-commit.flatkv.enable-read-write-metrics" // Hash logger configs (per-block hash logging; enabled by default) FlagSCHashLoggerEnable = "state-commit.sc-hash-logger-enable" @@ -122,6 +115,17 @@ func parseSCConfigs(appOpts servertypes.AppOptions) config.StateCommitConfig { scConfig.MemIAVLConfig.SnapshotWriteRateMBps = cast.ToInt(appOpts.Get(FlagSCSnapshotWriteRateMBps)) scConfig.FlatKVConfig.EnableReadWriteMetrics = cast.ToBool(appOpts.Get(FlagSCFlatKVReadWriteMetrics)) + // sc-write-mode-enable-auto (default true) decides whether the node may run + // in auto. An ABSENT key keeps the default (true): nodes provisioned by + // older binaries carry an explicit sc-write-mode = "memiavl_only" but no + // sc-write-mode-enable-auto key, and must still resolve to auto so a + // governance-driven migration can start without an app.toml edit. Only an + // explicit key flips it. + if v := appOpts.Get(FlagSCWriteModeEnableAuto); v != nil { + scConfig.WriteModeEnableAuto = cast.ToBool(v) + } + // Always parse sc-write-mode (even when auto is on) so a typo'd value fails + // fast here exactly as it does in server/config.GetConfig. if wm := cast.ToString(appOpts.Get(FlagSCWriteMode)); wm != "" { parsedWM, err := sctypes.ParseWriteMode(wm) if err != nil { @@ -129,6 +133,10 @@ func parseSCConfigs(appOpts servertypes.AppOptions) config.StateCommitConfig { } scConfig.WriteMode = parsedWM } + // When auto is enabled the explicit sc-write-mode is ignored and the node + // runs in auto; only when auto is disabled is the parsed mode honored (see + // config.ApplyWriteModeAuto). + scConfig.WriteMode = config.ApplyWriteModeAuto(scConfig.WriteModeEnableAuto, scConfig.WriteMode) if v := appOpts.Get(FlagSCHistoricalProofMaxInFlight); v != nil { scConfig.HistoricalProofMaxInFlight = cast.ToInt(v) @@ -139,16 +147,6 @@ func parseSCConfigs(appOpts servertypes.AppOptions) config.StateCommitConfig { if v := appOpts.Get(FlagSCHistoricalProofBurst); v != nil { scConfig.HistoricalProofBurst = cast.ToInt(v) } - // Guard with v != nil so that an absent app.toml entry preserves the - // default of 1024 instead of clobbering it to 0, which would fail - // StateCommitConfig.Validate ("keys-to-migrate-per-block must be > 0") - // and bring the node down at startup the first time write-mode is - // flipped to a migration mode. - if v := appOpts.Get(FlagSCKeysToMigratePerBlock); v != nil { - if n := cast.ToInt(v); n > 0 { - scConfig.KeysToMigratePerBlock = n - } - } // Hash logger. Guard each read with v != nil so an absent app.toml entry preserves the default // (notably Enable, which defaults to true) instead of clobbering it to the zero value. diff --git a/app/seidb_test.go b/app/seidb_test.go index 74f779d075..0dcd9da351 100644 --- a/app/seidb_test.go +++ b/app/seidb_test.go @@ -6,6 +6,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/server" "github.com/sei-protocol/sei-chain/sei-db/config" + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -75,7 +76,11 @@ func TestNewDefaultConfig(t *testing.T) { ssConfig := parseSSConfigs(appOpts) receiptConfig, err := config.ReadReceiptConfig(appOpts) assert.NoError(t, err) - assert.Equal(t, scConfig, config.DefaultStateCommitConfig()) + // WriteModeEnableAuto defaults to true, so parseSCConfigs resolves the effective + // WriteMode to auto, overriding the fixed-fallback default (memiavl_only). + expectedSC := config.DefaultStateCommitConfig() + expectedSC.WriteMode = sctypes.Auto + assert.Equal(t, expectedSC, scConfig) assert.Equal(t, ssConfig, config.DefaultStateStoreConfig()) assert.Equal(t, receiptConfig, config.DefaultReceiptStoreConfig()) } diff --git a/docker/localnode/config/app.toml b/docker/localnode/config/app.toml index 60f7237759..89ed5e9b22 100644 --- a/docker/localnode/config/app.toml +++ b/docker/localnode/config/app.toml @@ -236,13 +236,6 @@ sc-snapshot-writer-limit = 2 # CacheSize defines the size of the LRU cache for each store on top of the tree, default to 100000. sc-cache-size = 1000 -# KeysToMigratePerBlock controls how many EVM keys the in-flight migration -# (sc-write-mode = migrate_evm / migrate_bank / migrate_all_but_bank) drains -# from memiavl into flatkv per block. Default 1024 is appropriate for -# production drains; tests lower it to spread the migration across more -# blocks and exercise the resume / hybrid-read path. -sc-keys-to-migrate-per-block = 1024 - [state-store] # Enable defines if the state-store should be enabled for historical queries. diff --git a/docker/localnode/scripts/step4_config_override.sh b/docker/localnode/scripts/step4_config_override.sh index 8eab694e95..0dac4f7caa 100755 --- a/docker/localnode/scripts/step4_config_override.sh +++ b/docker/localnode/scripts/step4_config_override.sh @@ -40,6 +40,28 @@ sed -i.bak -e "s|^snapshot-directory *=.*|snapshot-directory = \"./build/generat # Enable slow mode sed -i.bak -e 's/slow = .*/slow = true/' ~/.sei/config/app.toml +# pin_sc_write_mode MODE: set sc-write-mode = "MODE" and pin +# sc-write-mode-enable-auto = false so the explicit mode is actually honored. +# sc-write-mode-enable-auto defaults to true, which forces the node into auto +# and ignores any explicit sc-write-mode; these test clusters need the exact +# pinned backend, so they must opt out of auto. +pin_sc_write_mode() { + mode="$1" + cfg="$HOME/.sei/config/app.toml" + if grep -q '^sc-write-mode[[:space:]]*=' "$cfg"; then + sed -i "s/^sc-write-mode[[:space:]]*=.*/sc-write-mode = \"$mode\"/" "$cfg" + else + sed -i "/^\[state-store\]/i sc-write-mode = \"$mode\"" "$cfg" + fi + if grep -q '^sc-write-mode-enable-auto[[:space:]]*=' "$cfg"; then + sed -i "s/^sc-write-mode-enable-auto[[:space:]]*=.*/sc-write-mode-enable-auto = false/" "$cfg" + elif grep -q '^sc-write-mode[[:space:]]*=' "$cfg"; then + sed -i "/^sc-write-mode[[:space:]]*=/a sc-write-mode-enable-auto = false" "$cfg" + else + sed -i "/^\[state-store\]/i sc-write-mode-enable-auto = false" "$cfg" + fi +} + # Boot the cluster in v0 (memiavl_only) for the FlatKV EVM migrate test. # Doing this here keeps the override surface narrow: the test runner # only has to set one env var to ship a v0-shaped config, and the @@ -47,11 +69,7 @@ sed -i.bak -e 's/slow = .*/slow = true/' ~/.sei/config/app.toml # coordinated stop. if [ "$GIGA_MIGRATE_FROM_MEMIAVL" = "true" ]; then echo "Booting node $NODE_ID in memiavl_only mode (FlatKV EVM migrate starting point)..." - if grep -q '^sc-write-mode[[:space:]]*=' ~/.sei/config/app.toml; then - sed -i 's/^sc-write-mode[[:space:]]*=.*/sc-write-mode = "memiavl_only"/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-write-mode = "memiavl_only"' ~/.sei/config/app.toml - fi + pin_sc_write_mode "memiavl_only" # The EVM SS split is irrelevant in this mode (flatkv is not allocated), # but explicitly disabling it keeps app.toml self-describing in case an # operator inspects it post-flip. @@ -62,11 +80,7 @@ fi # coverage. This mode must not also run the GIGA_STORAGE dual-write override. if [ "$GIGA_FLATKV_ONLY" = "true" ]; then echo "Booting node $NODE_ID in flatkv_only mode (post-migration steady state)..." - if grep -q '^sc-write-mode[[:space:]]*=' ~/.sei/config/app.toml; then - sed -i 's/^sc-write-mode[[:space:]]*=.*/sc-write-mode = "flatkv_only"/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-write-mode = "flatkv_only"' ~/.sei/config/app.toml - fi + pin_sc_write_mode "flatkv_only" sed -i 's/^evm-ss-split[[:space:]]*=.*/evm-ss-split = false/' ~/.sei/config/app.toml fi @@ -85,11 +99,7 @@ if [ "$GIGA_STORAGE" = "true" ] && [ "$GIGA_MIGRATE_FROM_MEMIAVL" != "true" ] && # from the memiavl tree via GetChildStoreByName. dual-write keeps memiavl # up-to-date for reads while also populating FlatKV. This mode is for test # clusters only — never deploy to testnet/mainnet. - if grep -q '^sc-write-mode[[:space:]]*=' ~/.sei/config/app.toml; then - sed -i 's/^sc-write-mode[[:space:]]*=.*/sc-write-mode = "test_only_dual_write"/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-write-mode = "test_only_dual_write"' ~/.sei/config/app.toml - fi + pin_sc_write_mode "test_only_dual_write" # --- SS layer: enable EVM split --- sed -i 's/^evm-ss-split[[:space:]]*=.*/evm-ss-split = true/' ~/.sei/config/app.toml diff --git a/docker/rpcnode/scripts/step1_configure_init.sh b/docker/rpcnode/scripts/step1_configure_init.sh index 34efdcee36..61613709a8 100755 --- a/docker/rpcnode/scripts/step1_configure_init.sh +++ b/docker/rpcnode/scripts/step1_configure_init.sh @@ -42,6 +42,28 @@ cp docker/rpcnode/config/app.toml ~/.sei/config/app.toml cp docker/rpcnode/config/config.toml ~/.sei/config/config.toml cp "$GENESIS_SRC" ~/.sei/config/genesis.json +# pin_sc_write_mode MODE: set sc-write-mode = "MODE" and pin +# sc-write-mode-enable-auto = false so the explicit mode is actually honored. +# sc-write-mode-enable-auto defaults to true, which forces the node into auto +# and ignores any explicit sc-write-mode; the RPC node must match the +# validators' pinned backend exactly, so it must opt out of auto. +pin_sc_write_mode() { + mode="$1" + cfg="$HOME/.sei/config/app.toml" + if grep -q '^sc-write-mode[[:space:]]*=' "$cfg"; then + sed -i "s/^sc-write-mode[[:space:]]*=.*/sc-write-mode = \"$mode\"/" "$cfg" + else + sed -i "/^\[state-store\]/i sc-write-mode = \"$mode\"" "$cfg" + fi + if grep -q '^sc-write-mode-enable-auto[[:space:]]*=' "$cfg"; then + sed -i "s/^sc-write-mode-enable-auto[[:space:]]*=.*/sc-write-mode-enable-auto = false/" "$cfg" + elif grep -q '^sc-write-mode[[:space:]]*=' "$cfg"; then + sed -i "/^sc-write-mode[[:space:]]*=/a sc-write-mode-enable-auto = false" "$cfg" + else + sed -i "/^\[state-store\]/i sc-write-mode-enable-auto = false" "$cfg" + fi +} + # Apply Giga Storage overrides so the RPC node's app hash matches the validators. GIGA_STORAGE=${GIGA_STORAGE:-false} GIGA_FLATKV_ONLY=${GIGA_FLATKV_ONLY:-false} @@ -52,11 +74,7 @@ if [ "$GIGA_STORAGE" = "true" ] && [ "$GIGA_FLATKV_ONLY" != "true" ]; then echo "Enabling Giga Storage for RPC node..." # SC layer: must match validators (test_only_dual_write) - if grep -q '^sc-write-mode[[:space:]]*=' ~/.sei/config/app.toml; then - sed -i 's/^sc-write-mode[[:space:]]*=.*/sc-write-mode = "test_only_dual_write"/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-write-mode = "test_only_dual_write"' ~/.sei/config/app.toml - fi + pin_sc_write_mode "test_only_dual_write" # SS layer: enable EVM split sed -i 's/^evm-ss-split[[:space:]]*=.*/evm-ss-split = true/' ~/.sei/config/app.toml @@ -64,11 +82,7 @@ fi if [ "$GIGA_FLATKV_ONLY" = "true" ]; then echo "Booting RPC node in flatkv_only mode..." - if grep -q '^sc-write-mode[[:space:]]*=' ~/.sei/config/app.toml; then - sed -i 's/^sc-write-mode[[:space:]]*=.*/sc-write-mode = "flatkv_only"/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-write-mode = "flatkv_only"' ~/.sei/config/app.toml - fi + pin_sc_write_mode "flatkv_only" sed -i 's/^evm-ss-split[[:space:]]*=.*/evm-ss-split = false/' ~/.sei/config/app.toml fi diff --git a/integration_test/contracts/verify_flatkv_evm_migrate.sh b/integration_test/contracts/verify_flatkv_evm_migrate.sh index 09155203f7..071a7070cb 100755 --- a/integration_test/contracts/verify_flatkv_evm_migrate.sh +++ b/integration_test/contracts/verify_flatkv_evm_migrate.sh @@ -39,8 +39,17 @@ GO_BIN=${GO_BIN:-/usr/local/go/bin/go} # default fixture (~4000 EVM keys), 400 keys/block gives roughly ten batches # and exercises the resume / hybrid-read path. Override to 1024+ for a # production-equivalent one-shot drain when sanity-checking the script. +# +# The rate is consensus-relevant (migration writes feed the AppHash), so it +# is NOT a node-local config: every validator reads the governance-controlled +# migration.NumKeysToMigratePerBlock param from chain state each BeginBlock. +# This script raises that param from its 0 (paused) default via a single +# param-change proposal (step 4b) so all nodes drain at the identical rate. KEYS_TO_MIGRATE_PER_BLOCK=${MIGRATE_KEYS_PER_BLOCK:-400} MIN_KEYS_MIGRATED=${MIGRATE_MIN_KEYS_MIGRATED:-3500} +# Upper bound on how long to wait for the migration param-change proposal to +# move from submission through the voting period to PROPOSAL_STATUS_PASSED. +GOV_PASS_TIMEOUT=${MIGRATE_GOV_PASS_TIMEOUT:-120} STOP_TIMEOUT=${MIGRATE_STOP_TIMEOUT:-30} HALT_TIMEOUT=${MIGRATE_HALT_TIMEOUT:-120} @@ -627,41 +636,31 @@ wait_for_safe_halt_height # --- step 3: flip sc-write-mode on every node ------------------------- # -# memiavl_only -> migrate_evm, and inject keys-to-migrate-per-block so -# the test runner controls how aggressive the per-block batch copier is. -# Both edits are idempotent: running this script twice in a row is safe -# (second flip is a no-op). +# memiavl_only -> migrate_evm. The per-block migration rate is no longer a +# node-local config: it is driven entirely by the consensus-relevant +# migration.NumKeysToMigratePerBlock gov param, raised via a proposal in +# step 4b below. Until that param is positive the migration stays paused, so +# flipping the write mode alone is a safe no-op copy boundary. The edit is +# idempotent: running this script twice in a row is safe (second flip is a +# no-op). for i in $(seq 0 $((NODE_COUNT - 1))); do node="sei-node-$i" - # The TOML key must match the FlagSC* constant in app/seidb.go - # (sc-keys-to-migrate-per-block, prefix matches sibling state-commit - # keys like sc-write-mode / sc-async-commit-buffer). The earlier - # un-prefixed name "keys-to-migrate-per-block" matched the mapstructure - # tag but not the FlagSC* viper key, so parseSCConfigs silently never - # read it -- the seid log showed KeysToMigratePerBlock:1024 (default) - # regardless of what we wrote here. docker exec "$node" bash -c " sed -i 's/^halt-height = .*/halt-height = 0/' '$APP_CONFIG' sed -i 's/^sc-write-mode = .*/sc-write-mode = \"migrate_evm\"/' '$APP_CONFIG' - if grep -q '^sc-keys-to-migrate-per-block' '$APP_CONFIG'; then - sed -i 's/^sc-keys-to-migrate-per-block = .*/sc-keys-to-migrate-per-block = $KEYS_TO_MIGRATE_PER_BLOCK/' '$APP_CONFIG' - else - sed -i '/^sc-write-mode/a sc-keys-to-migrate-per-block = $KEYS_TO_MIGRATE_PER_BLOCK' '$APP_CONFIG' - fi " done -echo "Flipped sc-write-mode to migrate_evm on all $NODE_COUNT nodes (batch=$KEYS_TO_MIGRATE_PER_BLOCK)" - -# Belt-and-suspenders: confirm the rewrite actually landed on node 0. -# If it didn't (e.g. unexpected app.toml format change), the migration -# would silently run at the 1024 default rather than the requested batch -# size, and the resume / hybrid-read coverage we want from this test -# would degrade to a one-shot drain. -written_batch=$(docker exec "sei-node-0" grep -E '^sc-keys-to-migrate-per-block' "$APP_CONFIG" \ +echo "Flipped sc-write-mode to migrate_evm on all $NODE_COUNT nodes (migration rate driven by gov param)" + +# Belt-and-suspenders: confirm the write-mode rewrite actually landed on +# node 0. If it didn't (e.g. unexpected app.toml format change), the nodes +# would restart in memiavl_only and the "migration" would silently do +# nothing, masking real bugs. +written_mode=$(docker exec "sei-node-0" grep -E '^sc-write-mode' "$APP_CONFIG" \ | tail -1 | awk -F'=' '{print $2}' | tr -d ' "' || true) -if [ -z "$written_batch" ] || [ "$written_batch" != "$KEYS_TO_MIGRATE_PER_BLOCK" ]; then - echo "ERROR: sei-node-0 app.toml has sc-keys-to-migrate-per-block='$written_batch' after rewrite; expected '$KEYS_TO_MIGRATE_PER_BLOCK'" >&2 +if [ "$written_mode" != "migrate_evm" ]; then + echo "ERROR: sei-node-0 app.toml has sc-write-mode='$written_mode' after rewrite; expected 'migrate_evm'" >&2 exit 1 fi @@ -693,6 +692,108 @@ if $SETTLE_FAIL; then fi echo "All $NODE_COUNT validators restarted in migrate_evm mode and survived a 5s settle" +# --- step 4b: drive the migration via a governance param change ------- +# +# The per-block migration rate is consensus-relevant (migration writes feed +# the AppHash), so a per-node config would diverge the chain. Instead every +# validator reads migration.NumKeysToMigratePerBlock from chain state in +# BeginBlock and applies the same value. We raise it from its 0 (paused) +# default to $KEYS_TO_MIGRATE_PER_BLOCK with a single param-change proposal +# voted through by a quorum; once it passes, all nodes begin draining at the +# identical rate on the same height. Uses the same gov helpers as +# integration_test/gov_module/gov_proposal_test.yaml. + +drive_migration_via_gov() { + local rate="$1" + local title="FlatKV migrate: set NumKeysToMigratePerBlock=${rate}" + + # Generate the param-change proposal JSON inside node 0. + docker exec "sei-node-0" bash -lc "cat > /tmp/migration_param_change_proposal.json <flatkv migration\", + \"changes\": [ + { + \"subspace\": \"migration\", + \"key\": \"NumKeysToMigratePerBlock\", + \"value\": \"${rate}\" + } + ], + \"deposit\": \"10000000usei\", + \"is_expedited\": false +} +EOF" + + # Submit from node 0's admin key; capture the new proposal id. + local proposal_id + proposal_id=$(docker exec "sei-node-0" bash -lc " + cd /sei-protocol/sei-chain + seidbin=build/seid chainid=sei + source integration_test/utils/_tx_helpers.sh + submit_gov_proposal admin '${title}' gov submit-proposal param-change /tmp/migration_param_change_proposal.json --fees 2000usei + ") + if [ -z "$proposal_id" ]; then + echo "ERROR: failed to submit migration param-change proposal" >&2 + for i in $(seq 0 $((NODE_COUNT - 1))); do dump_node_log "sei-node-$i"; done + exit 1 + fi + echo "Submitted migration param-change proposal id=$proposal_id (rate=$rate)" + + # Top up the deposit to clear the min-deposit threshold (mirrors the yaml). + docker exec "sei-node-0" bash -lc " + cd /sei-protocol/sei-chain + seidbin=build/seid chainid=sei + source integration_test/utils/_tx_helpers.sh + submit_tx_and_wait admin gov deposit $proposal_id 10000000usei --fees 2000usei + " >/dev/null + + # Vote yes from a quorum (2/4 with the devnet's 0.5 quorum). + for i in 0 1; do + docker exec "sei-node-$i" bash -lc " + cd /sei-protocol/sei-chain + seidbin=build/seid chainid=sei + source integration_test/utils/_tx_helpers.sh + submit_tx_and_wait node_admin gov vote $proposal_id yes --fees 2000usei + " >/dev/null + done + echo "Voted yes on proposal $proposal_id from a quorum of validators; waiting for it to pass..." + + # Poll for PASSED rather than a fixed sleep so a slow voting period does + # not flake the test (and a rejection fails fast). + local elapsed=0 + local status="" + while [ "$elapsed" -lt "$GOV_PASS_TIMEOUT" ]; do + status=$(docker exec "sei-node-0" build/seid q gov proposal "$proposal_id" --output json 2>/dev/null \ + | jq -r '.status // ""' 2>/dev/null || true) + if [ "$status" = "PROPOSAL_STATUS_PASSED" ]; then + break + fi + if [ "$status" = "PROPOSAL_STATUS_REJECTED" ] || [ "$status" = "PROPOSAL_STATUS_FAILED" ]; then + echo "ERROR: migration param-change proposal $proposal_id ended in status $status" >&2 + exit 1 + fi + echo "Waiting for proposal $proposal_id to pass (elapsed=${elapsed}s/${GOV_PASS_TIMEOUT}s, status=${status:-unknown})" + sleep 5 + elapsed=$((elapsed + 5)) + done + if [ "$status" != "PROPOSAL_STATUS_PASSED" ]; then + echo "ERROR: migration param-change proposal $proposal_id did not pass within ${GOV_PASS_TIMEOUT}s (last status=$status)" >&2 + exit 1 + fi + + # Confirm the param actually took the requested value on chain. + local on_chain + on_chain=$(docker exec "sei-node-0" build/seid q params subspace migration NumKeysToMigratePerBlock --output json 2>/dev/null \ + | jq -r '.value' 2>/dev/null | tr -d '"' || true) + if [ "$on_chain" != "$rate" ]; then + echo "ERROR: migration.NumKeysToMigratePerBlock='$on_chain' after proposal $proposal_id passed; expected '$rate'" >&2 + exit 1 + fi + echo "Governance raised migration.NumKeysToMigratePerBlock to $rate; migration now draining on all validators" +} + +drive_migration_via_gov "$KEYS_TO_MIGRATE_PER_BLOCK" + # --- step 5: wait for migration completion on every node -------------- # # Poll seidb migrate-evm-status against each node's flatkv dir. The tool diff --git a/integration_test/contracts/verify_flatkv_evm_store.sh b/integration_test/contracts/verify_flatkv_evm_store.sh index 6b19ceddd9..cb7b489d57 100755 --- a/integration_test/contracts/verify_flatkv_evm_store.sh +++ b/integration_test/contracts/verify_flatkv_evm_store.sh @@ -27,7 +27,7 @@ dump_flatkv_layout() { # to a different path than $flatkv_dir. Printing the layout removes that # ambiguity from the next CI run. echo "==================== app.toml FlatKV-related settings ====================" >&2 - grep -E '^(sc-write-mode|sc-keys-to-migrate-per-block|evm-ss-split)' \ + grep -E '^(sc-write-mode|evm-ss-split)' \ /root/.sei/config/app.toml >&2 2>/dev/null || true for candidate in "$flatkv_dir" /root/.sei/data/flatkv; do echo "==================== FlatKV directory state at $candidate ====================" >&2 diff --git a/integration_test/contracts/verify_flatkv_only_statesync.sh b/integration_test/contracts/verify_flatkv_only_statesync.sh index 2632623052..5f65e1c2a6 100755 --- a/integration_test/contracts/verify_flatkv_only_statesync.sh +++ b/integration_test/contracts/verify_flatkv_only_statesync.sh @@ -36,7 +36,7 @@ dump_node_log() { node_id=${node#sei-node-} logfile="/sei-protocol/sei-chain/build/generated/logs/seid-${node_id}.log" echo "==================== ${node} app.toml state-commit excerpt ====================" >&2 - docker exec "$node" bash -lc "grep -E '^(sc-write-mode|sc-keys-to-migrate-per-block|evm-ss-split)' '$APP_CONFIG' 2>/dev/null" >&2 || true + docker exec "$node" bash -lc "grep -E '^(sc-write-mode|evm-ss-split)' '$APP_CONFIG' 2>/dev/null" >&2 || true echo "==================== ${node} seid log ${logfile} (last 240 lines) ====================" >&2 docker exec "$node" tail -240 "$logfile" >&2 2>/dev/null \ || echo "(could not read ${logfile})" >&2 diff --git a/integration_test/gov_module/gov_proposal_test.yaml b/integration_test/gov_module/gov_proposal_test.yaml index 6acc2c7849..23d867bf8d 100644 --- a/integration_test/gov_module/gov_proposal_test.yaml +++ b/integration_test/gov_module/gov_proposal_test.yaml @@ -34,6 +34,51 @@ - type: eval expr: NEW_ABCI_PARAM == "true" +# Scope: this case verifies the governance plumbing for the migration rate — +# that a ParameterChangeProposal lands NumKeysToMigratePerBlock in the migration +# subspace and the new value is readable afterwards. The actual auto +# memiavl_only -> migrate_evm kick-off and the full cross-validator key-drain / +# digest agreement are covered separately by +# integration_test/contracts/verify_flatkv_evm_migrate.sh; asserting the +# one-time transition here is not idempotent across re-runs of a long-lived +# cluster, so this case intentionally stays scoped to the param plumbing. +- name: Test migration param change proposal should update NumKeysToMigratePerBlock + inputs: + # Get the current migration batch size param (defaults to 0 = paused) + - cmd: seid q params subspace migration NumKeysToMigratePerBlock --output json | jq -r .value | tr -d "\"" + env: OLD_PARAM + # Make a new proposal to raise the migration rate + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_gov_proposal admin "Update Migration Batch Size" gov submit-proposal param-change ./integration_test/gov_module/proposal/migration_param_change_proposal.json --fees 2000usei + env: PROPOSAL_ID + # Get proposal status + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + env: PROPOSAL_STATUS + # Make a deposit + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait admin gov deposit $PROPOSAL_ID 10000000usei --fees 2000usei + # sei-node-0 vote yes + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-0 + # sei-node-1 vote yes + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-1 + # since quorum is 0.5, we only need 2/4 votes and expect proposal to pass after 35 seconds + - cmd: sleep 35 + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + env: PROPOSAL_STATUS + # Get the migration batch size param again after proposal is passed + - cmd: seid q params subspace migration NumKeysToMigratePerBlock --output json | jq -r .value | tr -d "\"" + env: NEW_PARAM + verifiers: + # The proposal must have passed + - type: eval + expr: PROPOSAL_STATUS == "PROPOSAL_STATUS_PASSED" + # The migration batch size param must reflect the new value. The value is a + # string (piped through tr -d "\""), so quote the literal to match the + # sibling verifiers; the eval engine coerces both sides to big.Int anyway. + - type: eval + expr: NEW_PARAM == "12345" + - name: Test expedited proposal should respect expedited_voting_period inputs: # Get the current tally params diff --git a/integration_test/gov_module/proposal/migration_param_change_proposal.json b/integration_test/gov_module/proposal/migration_param_change_proposal.json new file mode 100644 index 0000000000..8ce8f36ed6 --- /dev/null +++ b/integration_test/gov_module/proposal/migration_param_change_proposal.json @@ -0,0 +1,13 @@ +{ + "title": "Update Migration Batch Size", + "description": "Set NumKeysToMigratePerBlock to 12345 to drive the SC memiavl->flatkv migration", + "changes": [ + { + "subspace": "migration", + "key": "NumKeysToMigratePerBlock", + "value": "12345" + } + ], + "deposit": "10000000usei", + "is_expedited": false +} diff --git a/sei-cosmos/server/config/config.go b/sei-cosmos/server/config/config.go index eb53f75cc2..c426653820 100644 --- a/sei-cosmos/server/config/config.go +++ b/sei-cosmos/server/config/config.go @@ -447,6 +447,16 @@ func GetConfig(v *viper.Viper) (Config, error) { } scWriteMode = parsed } + // sc-write-mode-enable-auto (default true) forces the node into auto and + // ignores the explicit sc-write-mode. An absent key keeps the default so + // older configs (explicit memiavl_only, no auto key) still resolve to auto, + // mirroring app/seidb.go. Set it to false to honor the explicit sc-write-mode + // as a deliberate pin (see config.ApplyWriteModeAuto). + scWriteModeEnableAuto := config.DefaultStateCommitConfig().WriteModeEnableAuto + if v.IsSet("state-commit.sc-write-mode-enable-auto") { + scWriteModeEnableAuto = v.GetBool("state-commit.sc-write-mode-enable-auto") + } + scWriteMode = config.ApplyWriteModeAuto(scWriteModeEnableAuto, scWriteMode) flatKVConfig := config.DefaultStateCommitConfig().FlatKVConfig if v.IsSet("state-commit.flatkv.fsync") { @@ -575,9 +585,10 @@ func GetConfig(v *viper.Viper) (Config, error) { SnapshotDirectory: v.GetString("state-sync.snapshot-directory"), }, StateCommit: config.StateCommitConfig{ - Enable: v.GetBool("state-commit.sc-enable"), - Directory: v.GetString("state-commit.sc-directory"), - WriteMode: scWriteMode, + Enable: v.GetBool("state-commit.sc-enable"), + Directory: v.GetString("state-commit.sc-directory"), + WriteMode: scWriteMode, + WriteModeEnableAuto: scWriteModeEnableAuto, MemIAVLConfig: memiavl.Config{ AsyncCommitBuffer: v.GetInt("state-commit.sc-async-commit-buffer"), SnapshotKeepRecent: v.GetUint32("state-commit.sc-keep-recent"), diff --git a/sei-cosmos/server/config/config_test.go b/sei-cosmos/server/config/config_test.go index f86f10b87b..f849ab98e9 100644 --- a/sei-cosmos/server/config/config_test.go +++ b/sei-cosmos/server/config/config_test.go @@ -439,6 +439,8 @@ func TestGetConfigStateCommit(t *testing.T) { v.Set("state-commit.sc-enable", true) v.Set("state-commit.sc-directory", "/custom/path") + // Opt out of auto so the explicit sc-write-mode is honored. + v.Set("state-commit.sc-write-mode-enable-auto", false) v.Set("state-commit.sc-write-mode", "test_only_dual_write") v.Set("state-commit.sc-async-commit-buffer", 200) v.Set("state-commit.sc-keep-recent", 5) @@ -452,6 +454,7 @@ func TestGetConfigStateCommit(t *testing.T) { require.True(t, cfg.StateCommit.Enable) require.Equal(t, "/custom/path", cfg.StateCommit.Directory) + require.False(t, cfg.StateCommit.WriteModeEnableAuto) require.Equal(t, sctypes.TestOnlyDualWrite, cfg.StateCommit.WriteMode) // Verify MemIAVLConfig fields @@ -477,6 +480,63 @@ 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") +} + +// 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() @@ -485,7 +545,7 @@ func TestGetConfigEmptyWriteModeUsesDefault(t *testing.T) { cfg, err := GetConfig(v) require.NoError(t, err) - require.Equal(t, sctypes.MemiavlOnly, cfg.StateCommit.WriteMode, + require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, "unset sc-write-mode must fall back to the in-code default") } @@ -529,7 +589,10 @@ func TestDefaultStateCommitConfig(t *testing.T) { 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) { diff --git a/sei-cosmos/storev2/rootmulti/flatkv_helpers_test.go b/sei-cosmos/storev2/rootmulti/flatkv_helpers_test.go index a9d079520f..d3dd70a761 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_helpers_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_helpers_test.go @@ -67,17 +67,14 @@ func memiavlOnlyConfig() seidbconfig.StateCommitConfig { } // migrateEVMConfig returns the MigrateEVM config used by phase 2 of the -// FlatKV EVM migrate tests. keysPerBlock caps the per-block migration batch -// so callers can deterministically spread the boundary across multiple -// commits without having to count source keys themselves; a small value -// (e.g. 4) reliably keeps the migration in flight long enough to cover -// the resume / determinism assertions, while a large value (e.g. 1024) -// is the production-equivalent that drains the boundary in one or two -// blocks. -func migrateEVMConfig(keysPerBlock int) seidbconfig.StateCommitConfig { +// FlatKV EVM migrate tests. The per-block migration batch is no longer part +// of the config; callers set it after constructing the store via +// store.SetMigrationBatchSize so a small value (e.g. 4) keeps the migration +// in flight across the resume / determinism assertions, while a large value +// drains the boundary in one or two blocks. +func migrateEVMConfig() seidbconfig.StateCommitConfig { cfg := seidbconfig.DefaultStateCommitConfig() cfg.WriteMode = sctypes.MigrateEVM - cfg.KeysToMigratePerBlock = keysPerBlock return withTestMemIAVL(cfg) } diff --git a/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go b/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go index f96ea39443..e9706a7a7d 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go @@ -103,7 +103,8 @@ func driveRootMultiMigration( } // --- Restart: MemiavlOnly -> MigrateEVM --- - store, storeKeys = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig(migrateBatchSize)) + store, storeKeys = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig()) + require.NoError(t, store.SetMigrationBatchSize(migrateBatchSize)) for i := phase1Blocks + 1; i <= phase1Blocks+phase2Blocks; i++ { records = append(records, simulateBlock(t, store, storeKeys, i, addrBase)) @@ -149,7 +150,8 @@ func TestRootMultiMigrateEVM_ReopenPreservesPreFlipAppHash(t *testing.T) { require.Equal(t, int64(3), preFlipID.Version) require.NotEmpty(t, preFlipID.Hash) - store, storeKeys = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig(2)) + store, storeKeys = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig()) + require.NoError(t, store.SetMigrationBatchSize(2)) defer func() { require.NoError(t, store.Close()) }() require.Equal(t, preFlipID, store.LastCommitID(), @@ -168,7 +170,8 @@ func TestRootMultiMigrateEVM_EmptyBlocksAdvanceMigration(t *testing.T) { storageKeys := storageMemIAVLKeys(0xB1, 4) simulateBlockManyStorage(t, store, storeKeys, 1, storageKeys, addrBase) - store, _ = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig(2)) + store, _ = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig()) + require.NoError(t, store.SetMigrationBatchSize(2)) for i := 0; i < 4; i++ { rec := finalizeBlock(t, store) @@ -177,7 +180,7 @@ func TestRootMultiMigrateEVM_EmptyBlocksAdvanceMigration(t *testing.T) { } require.NoError(t, store.Close()) - v, present := migrationVersionInFlatKV(t, dir, migrateEVMConfig(2)) + v, present := migrationVersionInFlatKV(t, dir, migrateEVMConfig()) require.True(t, present) require.Equal(t, uint64(migration.Version1_MigrateEVM), v) } @@ -193,7 +196,8 @@ func TestRootMultiMigrateEVM_EVMIteratorAvailableDuringMigration(t *testing.T) { rec := finalizeBlock(t, store) require.Equal(t, int64(1), rec.version) - store, storeKeys = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig(2)) + store, storeKeys = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig()) + require.NoError(t, store.SetMigrationBatchSize(2)) defer func() { require.NoError(t, store.Close()) }() cms = store.CacheMultiStore() @@ -263,7 +267,7 @@ func TestRootMultiMigrateEVM_HappyPath_Lifecycle(t *testing.T) { // LtHash equality; we don't repeat that here because the live // rootmulti store has by now rotated flatkv WAL snapshots in a way // that breaks LoadVersion(latest, readOnly=true) catchup. - v, present := migrationVersionInFlatKV(t, dir, migrateEVMConfig(batch)) + v, present := migrationVersionInFlatKV(t, dir, migrateEVMConfig()) require.True(t, present, "migration-version key must be present in flatkv after completion") require.Equal(t, uint64(migration.Version1_MigrateEVM), v, "flatkv migration version must be Version1_MigrateEVM") @@ -335,7 +339,7 @@ func TestRootMultiMigrateEVM_PostCompletionFlipToEVMMigrated(t *testing.T) { // Sanity: the close-and-reopen below depends on the migration // having actually finished before we flip the mode. require.NoError(t, store.Close()) - v, present := migrationVersionInFlatKV(t, dir, migrateEVMConfig(batch)) + v, present := migrationVersionInFlatKV(t, dir, migrateEVMConfig()) require.True(t, present && v == uint64(migration.Version1_MigrateEVM), "flip must happen after migration completes; tighten drainBlocks if this fails") @@ -404,7 +408,8 @@ func TestRootMultiMigrateEVM_DoubleFlushAppHashStable(t *testing.T) { // --- Restart into MigrateEVM with a small batch so multiple // per-block advances are required to drain. --- const batch = 4 - store, storeKeys = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig(batch)) + store, storeKeys = restartRootMultiWithConfig(t, store, dir, migrateEVMConfig()) + require.NoError(t, store.SetMigrationBatchSize(batch)) defer func() { require.NoError(t, store.Close()) }() // Alternate empty blocks (exercise the "empty changesets in diff --git a/sei-cosmos/storev2/rootmulti/set_write_mode_test.go b/sei-cosmos/storev2/rootmulti/set_write_mode_test.go index 42c26a65c1..2aa91b6c61 100644 --- a/sei-cosmos/storev2/rootmulti/set_write_mode_test.go +++ b/sei-cosmos/storev2/rootmulti/set_write_mode_test.go @@ -4,14 +4,14 @@ import ( "testing" seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/migration" sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" "github.com/stretchr/testify/require" ) -func autoModeConfig(batch int) seidbconfig.StateCommitConfig { +func autoModeConfig() seidbconfig.StateCommitConfig { cfg := seidbconfig.DefaultStateCommitConfig() cfg.WriteMode = sctypes.Auto - cfg.KeysToMigratePerBlock = batch return withTestMemIAVL(cfg) } @@ -24,10 +24,11 @@ func autoModeConfig(batch int) seidbconfig.StateCommitConfig { // nil for migrated keys. func TestRootMultiSetWriteMode_StaleViewsRouteCorrectly(t *testing.T) { dir := t.TempDir() - store, storeKeys := newTestRootMulti(t, dir, autoModeConfig(100)) + store, storeKeys := newTestRootMulti(t, dir, autoModeConfig()) defer func() { require.NoError(t, store.Close()) }() - // Deposit EVM state while effectively MemiavlOnly. + // Deposit EVM state while effectively MemiavlOnly (the batch size is + // still 0, so the auto store stays in its pre-migration steady state). addr := newEVMTestData(0xD1) for i := 1; i <= 2; i++ { simulateBlock(t, store, storeKeys, i, addr) @@ -35,13 +36,15 @@ func TestRootMultiSetWriteMode_StaleViewsRouteCorrectly(t *testing.T) { expected := makeSlot(2, 0xAA) // Capture views before the transition; rootmulti caches these in - // ckvStores and SetWriteMode's rebuild will NOT refresh our local + // ckvStores and the kick-off's view rebuild will NOT refresh our local // references. staleEVM := store.GetKVStore(storeKeys["evm"]) staleBank := store.GetKVStore(storeKeys["bank"]) require.Equal(t, expected, staleEVM.Get(addr.storKey)) - require.NoError(t, store.SetWriteMode(sctypes.MigrateEVM)) + // Raising the batch size above 0 is the migration trigger: it advances + // the auto store memiavl_only -> migrate_evm. + require.NoError(t, store.SetMigrationBatchSize(100)) // Drive cosmos-only blocks: migration modes forward every flush, so // the batch=100 migration drains the handful of evm keys and deletes @@ -61,43 +64,183 @@ func TestRootMultiSetWriteMode_StaleViewsRouteCorrectly(t *testing.T) { "pre-transition bank view must keep serving current data") } -// TestRootMultiSetWriteMode_RejectsPendingChanges pins the between-blocks -// contract enforcement: buffered writes that have not been flushed by -// Commit would be silently dropped by the view rebuild, so SetWriteMode -// must refuse to run. -func TestRootMultiSetWriteMode_RejectsPendingChanges(t *testing.T) { +// TestRootMultiSetWriteMode_RequiresAutoConfig confirms the error from the +// SC store propagates when the configured mode is fixed. +func TestRootMultiSetWriteMode_RequiresAutoConfig(t *testing.T) { dir := t.TempDir() - store, storeKeys := newTestRootMulti(t, dir, autoModeConfig(100)) + store, _ := newTestRootMulti(t, dir, memiavlOnlyConfig()) defer func() { require.NoError(t, store.Close()) }() - addr := newEVMTestData(0xD2) - simulateBlock(t, store, storeKeys, 1, addr) - - // Write directly into the cached view, bypassing the commit cycle: - // the change is buffered in the commitment.Store and not yet flushed. - store.GetKVStore(storeKeys["bank"]).Set([]byte("pending"), []byte{0x01}) - err := store.SetWriteMode(sctypes.MigrateEVM) require.Error(t, err) - require.Contains(t, err.Error(), "pending uncommitted changes") + require.Contains(t, err.Error(), "fixed") +} - // After a normal commit cycle the same transition succeeds. +// TestRootMultiAutoKickoff_LiveCacheStoreNotOrphaned reproduces the production +// ordering: the block's deliver cache-multi-store is created at block start, +// BEFORE BeginBlock runs the kick-off. cacheMultiStore snapshots rs.ckvStores, +// so if the kick-off's SetWriteMode REPLACES those objects mid-block, writes +// made through the pre-created cms land in orphaned stores and are dropped at +// flush. This test pins that a write through that cms survives the commit. +func TestRootMultiAutoKickoff_LiveCacheStoreNotOrphaned(t *testing.T) { + dir := t.TempDir() + store, storeKeys := newTestRootMulti(t, dir, autoModeConfig()) + defer func() { require.NoError(t, store.Close()) }() + + addr := newEVMTestData(0xE4) + storageKeys := storageMemIAVLKeys(0xE4, 6) + simulateBlockManyStorage(t, store, storeKeys, 1, storageKeys, addr) + + // Deliver cms created at block start, before the kick-off. cms := store.CacheMultiStore() + + // BeginBlock kick-off: batch>0 flips memiavl_only -> migrate_evm. + require.NoError(t, store.SetMigrationBatchSize(2)) + + // DeliverTx-equivalent write through the pre-created cms. + cms.GetKVStore(storeKeys["bank"]).Set([]byte("supply"), []byte{0x42}) cms.Write() - _, err = store.GetWorkingHash() + + _, err := store.GetWorkingHash() require.NoError(t, err) store.Commit(true) - require.NoError(t, store.SetWriteMode(sctypes.MigrateEVM)) + + got := store.GetKVStore(storeKeys["bank"]).Get([]byte("supply")) + require.Equal(t, []byte{0x42}, got, + "write through the deliver cms created before the kick-off must not be lost") } -// TestRootMultiSetWriteMode_RequiresAutoConfig confirms the error from the -// SC store propagates when the configured mode is fixed. -func TestRootMultiSetWriteMode_RequiresAutoConfig(t *testing.T) { +// TestRootMultiAutoKickoff_FixedMemiavlOnlySkips proves the kick-off does not +// fire for a node pinned to fixed memiavl_only. Such a store reports the same +// memiavl_only effective mode as an auto store mid-pre-migration, but it must +// NOT be transitioned at runtime: SetMigrationBatchSize must return no error +// (it would otherwise hit SetWriteMode's "fixed by configuration" rejection) +// and must leave the mode at memiavl_only. The node deliberately opts out of +// the migration and diverges from auto peers from the activation height on. +func TestRootMultiAutoKickoff_FixedMemiavlOnlySkips(t *testing.T) { dir := t.TempDir() - store, _ := newTestRootMulti(t, dir, memiavlOnlyConfig()) + store, storeKeys := newTestRootMulti(t, dir, memiavlOnlyConfig()) defer func() { require.NoError(t, store.Close()) }() - err := store.SetWriteMode(sctypes.MigrateEVM) - require.Error(t, err) - require.Contains(t, err.Error(), "fixed") + addr := newEVMTestData(0xE3) + simulateBlock(t, store, storeKeys, 1, addr) + + // A positive batch size must be a no-op for the fixed config (besides the + // skip log): no error surfaces and the mode stays memiavl_only. + require.NoError(t, store.SetMigrationBatchSize(100)) + mode, ok := store.GetWriteMode() + require.True(t, ok) + require.Equal(t, sctypes.MemiavlOnly, mode, + "fixed memiavl_only must not be advanced by the kick-off") + + // And the node keeps committing in memiavl_only on subsequent blocks. + simulateBlock(t, store, storeKeys, 2, addr) + mode, _ = store.GetWriteMode() + require.Equal(t, sctypes.MemiavlOnly, mode) +} + +// TestRootMultiAutoKickoff_RestartResumesMigrateEVM proves the crash/restart +// safety of the batch-size migration kick-off. Once a positive batch size has +// advanced an auto store memiavl_only -> migrate_evm and the first migrating +// block has committed (persisting an in-flight boundary), a restart under the +// same auto config must DERIVE migrate_evm from that persisted boundary — the +// in-memory mode is not carried across the restart, so resumption depends +// entirely on the metadata. The node must resume mid-migration and, once the +// boundary is fully drained, a later restart must derive the evm_migrated +// steady state. +func TestRootMultiAutoKickoff_RestartResumesMigrateEVM(t *testing.T) { + dir := t.TempDir() + store, storeKeys := newTestRootMulti(t, dir, autoModeConfig()) + + // Phase 1: deposit EVM storage keys while memiavl_only (batch size 0) so + // the migration has real work and a single small-batch block leaves it + // in-flight rather than completing immediately. + addrBase := newEVMTestData(0xE1) + storageKeys := storageMemIAVLKeys(0xE1, 12) + simulateBlockManyStorage(t, store, storeKeys, 1, storageKeys, addrBase) + + mode, ok := store.GetWriteMode() + require.True(t, ok) + require.Equal(t, sctypes.MemiavlOnly, mode, "still memiavl_only before any batch size is set") + + // Kick off: batch > 0 advances memiavl_only -> migrate_evm. + require.NoError(t, store.SetMigrationBatchSize(2)) + mode, _ = store.GetWriteMode() + require.Equal(t, sctypes.MigrateEVM, mode, "batch>0 must kick off migrate_evm") + + // One migrating block persists the in-flight boundary at commit; batch=2 + // against 12 keys keeps the migration in flight (version not yet bumped). + simulateBlock(t, store, storeKeys, 2, addrBase) + + // Restart under the SAME auto config (operator stop/start, or a crash + // after this commit). The mode must be re-derived from persisted metadata. + store, storeKeys = restartRootMultiWithConfig(t, store, dir, autoModeConfig()) + + mode, ok = store.GetWriteMode() + require.True(t, ok) + require.Equal(t, sctypes.MigrateEVM, mode, + "after restart the in-flight boundary must derive migrate_evm, not memiavl_only") + + // Resume draining to completion. The app re-applies the gov batch size + // every BeginBlock; re-applying it here is a no-op for the mode (already + // migrate_evm) and simply sets the rate. + require.NoError(t, store.SetMigrationBatchSize(100)) + for i := 3; i <= 6; i++ { + simulateBlock(t, store, storeKeys, i, addrBase) + } + require.NoError(t, store.Close()) + + v, present := migrationVersionInFlatKV(t, dir, autoModeConfig()) + require.True(t, present) + require.Equal(t, uint64(migration.Version1_MigrateEVM), v, + "the migration must have completed (version bumped to 1)") + + // A restart after completion derives the evm_migrated steady state. + store, _ = newTestRootMulti(t, dir, autoModeConfig()) + defer func() { require.NoError(t, store.Close()) }() + mode, _ = store.GetWriteMode() + require.Equal(t, sctypes.EVMMigrated, mode, + "after completion a restart must derive evm_migrated") +} + +// TestRootMultiAutoKickoff_RestartBeforeFirstCommitReKicks proves the +// level-triggered self-heal. If the node restarts after the kick-off flipped +// the mode in memory but before any migrating block committed (so no boundary +// is persisted), the restart correctly re-derives memiavl_only — and the next +// positive batch size (replayed every BeginBlock) re-fires the kick-off. This +// is the crash window SetWriteMode documents: the trigger is not consumed by a +// one-shot marker, so progress is neither lost nor duplicated. +func TestRootMultiAutoKickoff_RestartBeforeFirstCommitReKicks(t *testing.T) { + dir := t.TempDir() + store, storeKeys := newTestRootMulti(t, dir, autoModeConfig()) + + addrBase := newEVMTestData(0xE2) + storageKeys := storageMemIAVLKeys(0xE2, 8) + simulateBlockManyStorage(t, store, storeKeys, 1, storageKeys, addrBase) + + // Kick off but DO NOT run a migrating block: the boundary is never + // persisted (the flip lives only in memory; the freshly materialized + // flatkv carries no in-flight boundary yet). + require.NoError(t, store.SetMigrationBatchSize(2)) + mode, _ := store.GetWriteMode() + require.Equal(t, sctypes.MigrateEVM, mode) + + // Restart: with no persisted in-flight boundary the derivation falls back + // to memiavl_only (resolveCurrentWriteMode closes the idle flatkv). + store, storeKeys = restartRootMultiWithConfig(t, store, dir, autoModeConfig()) + defer func() { require.NoError(t, store.Close()) }() + mode, ok := store.GetWriteMode() + require.True(t, ok) + require.Equal(t, sctypes.MemiavlOnly, mode, + "a kick-off not yet persisted must re-derive memiavl_only after restart") + + // The gov batch size, replayed by the app every BeginBlock, re-fires the + // kick-off and the node converges to migrate_evm; the migration then runs + // to completion normally. + require.NoError(t, store.SetMigrationBatchSize(100)) + mode, _ = store.GetWriteMode() + require.Equal(t, sctypes.MigrateEVM, mode, "re-applying batch>0 re-fires the kick-off") + for i := 2; i <= 5; i++ { + simulateBlock(t, store, storeKeys, i, addrBase) + } } diff --git a/sei-cosmos/storev2/rootmulti/store.go b/sei-cosmos/storev2/rootmulti/store.go index 889e88cbf5..3d9e5b79e3 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -502,56 +502,38 @@ func (rs *Store) GetCommitKVStore(key types.StoreKey) types.CommitKVStore { return rs.ckvStores[key] } -// SetWriteMode transitions the SC store's effective write mode at runtime -// and refreshes the cached per-module store views so reads route per the -// new mode immediately (the views' write buffers are recreated empty, -// which is why the pending-changes assertion below is load-bearing). +// SetWriteMode transitions the SC store's effective write mode at runtime by +// swapping the composite store's router (see composite.SetWriteMode for the +// transition-legality rules). // -// Contract: must be called between blocks — after this store's Commit has -// completed (all flushes done, every cached view's changeset buffer -// popped) and before the next block's first write. The intended call site -// is baseapp.Commit, between cms.Commit() and the check-state reset, so -// that both deliver- and check-state are rebuilt from post-transition -// views. Calling it with buffered writes would silently drop them; that -// contract violation is rejected loudly here instead. +// It deliberately does NOT rebuild rootmulti's cached per-module views, and +// does not assert anything about their write buffers. Those views are dynamic +// router proxies — composite.GetChildStoreByName returns a RouterCommitKVStore +// that resolves cs.router at call time — so an existing view, including one +// already captured by a live deliver cache-multi-store, routes through the new +// mode automatically once the router is swapped. Buffered changesets are left +// in place and flush normally under the new mode. rs.Commit already reloads +// the views every block, so there is no extra invalidation to perform here. // -// Because migration writes feed the AppHash, the trigger driving this -// method must be deterministic across all nodes (same transition at the -// same height) and level-triggered: the underlying transition is not -// persisted until the next commit, so a node restarting inside that -// window reverts to the prior mode and the trigger must re-fire. See -// composite.SetWriteMode for the transition-legality rules. +// Replacing rs.ckvStores here would be actively unsafe: the kick-off that +// drives this method runs in BeginBlock, where the block's deliver +// cache-multi-store has already snapshotted rs.ckvStores (see +// cacheMultiStoreLocked). Swapping those objects out would orphan the live cms +// and silently drop every write made in the activation block — see the +// regression test TestRootMultiAutoKickoff_LiveCacheStoreNotOrphaned. +// +// Because migration writes feed the AppHash, the trigger driving this method +// must be deterministic across all nodes (same transition at the same height) +// and level-triggered: the underlying transition is not persisted until the +// next commit, so a node restarting inside that window reverts to the prior +// mode and the trigger must re-fire. func (rs *Store) SetWriteMode(mode sctypes.WriteMode) error { rs.mtx.Lock() defer rs.mtx.Unlock() - for key, store := range rs.ckvStores { - if commitStore, ok := store.(*commitment.Store); ok && commitStore.HasPendingChanges() { - return fmt.Errorf( - "SetWriteMode called with pending uncommitted changes in store %q; "+ - "write-mode transitions must run between blocks, after Commit", - key.Name()) - } - } - if err := rs.scStore.SetWriteMode(mode); err != nil { return fmt.Errorf("failed to set SC store write mode: %w", err) } - - // Refresh the cached views exactly like the post-Commit reload does: - // the SC store's router has been replaced, and although the views - // resolve the router dynamically, rebuilding keeps this path on the - // same invalidation contract as Commit. - for key := range rs.ckvStores { - store := rs.ckvStores[key] - if store.GetStoreType() == types.StoreTypeIAVL { - reloaded, err := rs.loadCommitStoreFromParams(key, rs.storesParams[key]) - if err != nil { - return fmt.Errorf("failed to reload store %q after write mode change: %w", key.Name(), err) - } - rs.ckvStores[key] = reloaded - } - } return nil } @@ -689,6 +671,108 @@ func (rs *Store) SetInitialVersion(version int64) error { return rs.scStore.SetInitialVersion(version) } +// SetMigrationBatchSize forwards the governance-controlled number of keys +// to migrate per block to the SC store, and — when migration has been +// requested (batch size > 0) while an auto store is still in the +// pre-migration steady state — kicks it off by transitioning memiavl_only +// -> migrate_evm. +// +// The kick-off only fires for an auto-configured store. A node pinned to a +// fixed mode never transitions at runtime: fixed modes other than +// memiavl_only do not report a memiavl_only effective mode, and a node +// pinned to fixed memiavl_only is deliberately excluded here — it is opting +// out of the migration and will intentionally diverge from the network at +// the activation height. We log that loudly and skip rather than calling +// SetWriteMode (which rejects fixed configs) and swallowing the error, +// which would hide the divergence. +// +// The kick-off is level-triggered, not edge-triggered: it fires whenever +// batchSize > 0 and the effective mode is still memiavl_only. This is the +// determinism contract SetWriteMode requires — every (auto) node reads the +// same gov param at the same height and transitions together; the +// transition is not persisted until the next commit, so a node crashing in +// that window re-derives memiavl_only on restart and re-fires, while a node +// past the persisted boundary derives migrate_evm directly and the guard +// makes the re-fire a no-op. +// +// The app calls this from BeginBlock. Forwarding the batch size to the SC +// store first means the migration router installed by the memiavl_only -> +// migrate_evm transition already observes the new rate. SetWriteMode only +// swaps the composite store's router — it does not rebuild rootmulti's +// cached views or require an empty write buffer — so it is safe to call +// mid-block without orphaning the block's live cache-multi-store. +func (rs *Store) SetMigrationBatchSize(batchSize int) error { + if err := rs.scStore.SetMigrationBatchSize(batchSize); err != nil { + return fmt.Errorf("failed to set SC store migration batch size: %w", err) + } + if batchSize <= 0 { + return nil + } + mode, ok := rs.GetWriteMode() + if !ok || mode != sctypes.MemiavlOnly { + return nil + } + // Effective mode is memiavl_only. Only an auto store may be advanced to + // migrate_evm at runtime; a node pinned to fixed memiavl_only must not. + configured, hasConfigured := rs.ConfiguredWriteMode() + if !hasConfigured { + // The underlying SC store does not expose a configured mode, so we + // cannot tell whether this is an auto store. Don't assume a deliberate + // opt-out — skip with a distinct message so it isn't mistaken for the + // pinned-memiavl_only case below during debugging. + logger.Info( + "migration requested (batch size > 0) but the SC store does not expose a "+ + "configured write mode; skipping migration kick-off", + "effectiveWriteMode", mode, "batchSize", batchSize) + return nil + } + if configured != sctypes.Auto { + logger.Info( + "migration requested (batch size > 0) but the SC write mode is pinned to fixed "+ + "memiavl_only by configuration; skipping migration kick-off. This node opts out of "+ + "the migration and will intentionally diverge from the network at the activation "+ + "height — set sc-write-mode-enable-auto = true (the default) to participate.", + "configuredWriteMode", configured, "batchSize", batchSize) + return nil + } + if err := rs.SetWriteMode(sctypes.MigrateEVM); err != nil { + return fmt.Errorf("failed to start EVM migration (memiavl_only -> migrate_evm): %w", err) + } + return nil +} + +// GetWriteMode returns the SC store's effective write mode. The bool is +// false when the underlying SC store does not expose one. Intended for the +// migration kick-off in SetMigrationBatchSize, observability, and tests. +func (rs *Store) GetWriteMode() (sctypes.WriteMode, bool) { + if g, ok := rs.scStore.(interface{ GetWriteMode() sctypes.WriteMode }); ok { + return g.GetWriteMode(), true + } + return "", false +} + +// ConfiguredWriteMode returns the SC store's configured (pre-derivation) +// write mode — types.Auto for an auto store, the pinned mode otherwise. The +// bool is false when the underlying SC store does not expose one. The +// migration kick-off uses it to act only on auto stores. +func (rs *Store) ConfiguredWriteMode() (sctypes.WriteMode, bool) { + if g, ok := rs.scStore.(interface{ ConfiguredWriteMode() sctypes.WriteMode }); ok { + return g.ConfiguredWriteMode(), true + } + return "", false +} + +// GetMigrationBatchSize returns the governance-controlled migration batch size +// last pushed into the SC store via SetMigrationBatchSize. The bool is false +// when the underlying SC store does not track one. Intended for observability +// and tests. +func (rs *Store) GetMigrationBatchSize() (int, bool) { + if g, ok := rs.scStore.(interface{ GetMigrationBatchSize() int }); ok { + return g.GetMigrationBatchSize(), true + } + return 0, false +} + // Implements interface CommitMultiStore func (rs *Store) SetLazyLoading(_ bool) { } diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index f128bb7c51..2094ef15a1 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -29,12 +29,32 @@ type StateCommitConfig struct { // defaults to 100 AsyncCommitBuffer int `mapstructure:"async-commit-buffer"` - // WriteMode defines the write routing mode for EVM data. - // 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. - // defaults to memiavl_only. + // WriteMode is the fixed write routing mode used only when WriteModeEnableAuto + // is false. 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. Defaults to memiavl_only — the safe + // pre-migration fallback for a node that has explicitly opted out of auto. WriteMode types.WriteMode `mapstructure:"write-mode"` + // WriteModeEnableAuto, when true (the default), forces the node to run in + // auto and ignores any explicit WriteMode: the effective mode is derived at + // startup from the persisted migration metadata (memiavl_only before + // migration has started, migrate_evm while EVM keys are still draining, + // evm_migrated once complete), and raising the NumKeysToMigratePerBlock gov + // param above 0 advances an auto store from memiavl_only to migrate_evm. + // + // Defaulting to true (and treating an absent config key as true) is what + // lets the existing fleet participate: nodes provisioned by older binaries + // carry an explicit sc-write-mode = "memiavl_only" but no + // sc-write-mode-enable-auto key, so they still resolve to auto and follow a + // governance-driven migration without any app.toml edit. + // + // Set this to false to honor the explicit WriteMode (memiavl_only, + // flatkv_only, test_only_dual_write, ...) as a deliberate pin. A node pinned + // this way does not participate in a governance-driven migration and will + // diverge once the chain migrates. + WriteModeEnableAuto bool `mapstructure:"write-mode-enable-auto"` + // MemIAVLConfig is the configuration for the MemIAVL (Cosmos) backend MemIAVLConfig memiavl.Config @@ -51,9 +71,6 @@ type StateCommitConfig struct { // Token bucket burst for historical proof queries. HistoricalProofBurst int `mapstructure:"historical-proof-burst"` - // The number of keys to migrate from memiavl to flatkv per block. Ignored if not in a migration mode. - KeysToMigratePerBlock int `mapstructure:"keys-to-migrate-per-block"` - // HashLogger configures the per-block hash logger (a debugging/forensics tool). Enabled by default. // Loaded via explicit sc-hash-logger-* flag reads in app.parseSCConfigs, not mapstructure. HashLogger HashLoggerConfig @@ -64,23 +81,35 @@ func DefaultStateCommitConfig() StateCommitConfig { return StateCommitConfig{ Enable: true, WriteMode: types.MemiavlOnly, + WriteModeEnableAuto: true, MemIAVLConfig: memiavl.DefaultConfig(), FlatKVConfig: *config.DefaultConfig(), HistoricalProofMaxInFlight: DefaultSCHistoricalProofMaxInFlight, HistoricalProofRateLimit: DefaultSCHistoricalProofRateLimit, HistoricalProofBurst: DefaultSCHistoricalProofBurst, - KeysToMigratePerBlock: 1024, HashLogger: DefaultHashLoggerConfig(), } } +// ApplyWriteModeAuto resolves the effective write mode from the +// sc-write-mode-enable-auto flag and the explicitly configured sc-write-mode. +// +// When auto is enabled the node always runs in auto, regardless of any explicit +// sc-write-mode — the explicit value is ignored. To pin an explicit mode +// (memiavl_only, flatkv_only, test_only_dual_write, ...) the operator must set +// sc-write-mode-enable-auto = false; only then is the configured sc-write-mode +// honored. +func ApplyWriteModeAuto(enableAuto bool, mode types.WriteMode) types.WriteMode { + if enableAuto { + return types.Auto + } + return mode +} + // Validate checks if the StateCommitConfig is valid func (c StateCommitConfig) Validate() error { if !c.WriteMode.IsValid() { return fmt.Errorf("invalid write-mode: %s", c.WriteMode) } - if c.KeysToMigratePerBlock <= 0 { - return fmt.Errorf("keys-to-migrate-per-block must be greater than 0") - } return nil } diff --git a/sei-db/config/sc_config_test.go b/sei-db/config/sc_config_test.go new file mode 100644 index 0000000000..c9a7ec4672 --- /dev/null +++ b/sei-db/config/sc_config_test.go @@ -0,0 +1,41 @@ +package config + +import ( + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" + "github.com/stretchr/testify/require" +) + +func TestApplyWriteModeAuto(t *testing.T) { + tests := []struct { + name string + enableAuto bool + mode types.WriteMode + want types.WriteMode + }{ + // When auto is enabled the explicit mode is always ignored in favor of + // auto, regardless of what was configured. + {"auto on + memiavl_only -> auto", true, types.MemiavlOnly, types.Auto}, + {"auto on + flatkv_only -> auto", true, types.FlatKVOnly, types.Auto}, + {"auto on + evm_migrated -> auto", true, types.EVMMigrated, types.Auto}, + {"auto on + test_only_dual_write -> auto", true, types.TestOnlyDualWrite, types.Auto}, + {"auto on + auto -> auto", true, types.Auto, types.Auto}, + // When auto is disabled the explicit mode is honored as a deliberate pin. + {"auto off + memiavl_only -> pinned", false, types.MemiavlOnly, types.MemiavlOnly}, + {"auto off + flatkv_only -> pinned", false, types.FlatKVOnly, types.FlatKVOnly}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.want, ApplyWriteModeAuto(tc.enableAuto, tc.mode)) + }) + } +} + +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) +} diff --git a/sei-db/config/toml.go b/sei-db/config/toml.go index dcaa9a2474..42d2e8480f 100644 --- a/sei-db/config/toml.go +++ b/sei-db/config/toml.go @@ -50,27 +50,25 @@ sc-snapshot-prefetch-threshold = {{ .StateCommit.MemIAVLConfig.SnapshotPrefetchT # Maximum snapshot write rate in MB/s (global across all trees). 0 = unlimited. Default 100. sc-snapshot-write-rate-mbps = {{ .StateCommit.MemIAVLConfig.SnapshotWriteRateMBps }} -# WriteMode defines the write routing mode for EVM data in the SC layer. -# 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 is the write routing mode. By default it is IGNORED: the +# advanced, unrendered state-commit.sc-write-mode-enable-auto defaults to true, +# which forces the node to run in auto regardless of this value. In auto the +# effective mode is derived from the on-disk migration state and advanced by the +# NumKeysToMigratePerBlock gov param, so the node follows a governance-driven EVM +# migration with no edits here. +# +# To pin an explicit mode (memiavl_only, flatkv_only, evm_migrated, +# test_only_dual_write, ...) you MUST also set +# state-commit.sc-write-mode-enable-auto = false; only then is this value +# honored. A pinned node does not participate in a governance-driven migration +# and diverges from the network once the chain migrates (e.g. auto left enabled +# on a flatkv_only-style node would either fail every commit with a +# version-mismatch error or silently serve reads from an empty memiavl). # -# auto derives the effective mode from the on-disk migration state and -# allows coordinated runtime transitions without restarts. It is only -# valid for nodes whose history began in memiavl (e.g. switching from -# memiavl_only). WARNING: never set auto on an existing flatkv_only node — -# depending on its on-disk metadata the node either fails every commit -# with a version-mismatch error or silently serves reads from an empty -# memiavl. Nodes that start in flatkv mode must keep flatkv_only forever. +# 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 }}" -# KeysToMigratePerBlock controls how many EVM keys the in-flight migration -# (sc-write-mode = migrate_evm / migrate_bank / migrate_all_but_bank) drains -# from memiavl into flatkv per block. Default 1024 is appropriate for -# production drains; lower it (e.g. 256) to spread the migration across more -# blocks for test runs that need to observe the resume / hybrid-read path. -# Must be > 0; ignored entirely when not in a migration mode. -sc-keys-to-migrate-per-block = {{ .StateCommit.KeysToMigratePerBlock }} - # 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. diff --git a/sei-db/state_db/sc/composite/random_test.go b/sei-db/state_db/sc/composite/random_test.go index 7459f8e398..24ba2edf97 100644 --- a/sei-db/state_db/sc/composite/random_test.go +++ b/sei-db/state_db/sc/composite/random_test.go @@ -259,9 +259,14 @@ func runMigrationScenario(t *testing.T, sc migrationScenario) { // batch size is randomized (small) to vary how quickly the boundary // advances. --- migCfg := randomTestConfig(t, rng, sc.migrationMode) - migCfg.KeysToMigratePerBlock = 3 + rng.Intn(3) // {3,4,5} + migBatch := 3 + rng.Intn(3) // {3,4,5} + // The per-block rate is no longer a persisted config; mirror production + // (BeginBlock re-applies the gov param after every restart) by having the + // framework re-apply it on every store open for the rest of the scenario. + testMigrationBatchSize = migBatch + defer func() { testMigrationBatchSize = 0 }() t.Logf("migration scenario %s->%s keysToMigratePerBlock=%d", - sc.migrationMode, sc.successorMode, migCfg.KeysToMigratePerBlock) + sc.migrationMode, sc.successorMode, migBatch) cs = restartComposite(t, cs, dir, migCfg) // Pre-migration reads must be transparent across the boundary. diff --git a/sei-db/state_db/sc/composite/random_test_framework_test.go b/sei-db/state_db/sc/composite/random_test_framework_test.go index c4dc9d8be0..302d3495b9 100644 --- a/sei-db/state_db/sc/composite/random_test_framework_test.go +++ b/sei-db/state_db/sc/composite/random_test_framework_test.go @@ -1483,6 +1483,24 @@ func randomTestConfig(t *testing.T, rng *testutil.TestRandom, mode types.WriteMo // openComposite constructs, initializes (with the full canonical store set), // and loads a composite store at the latest version, registering cleanup. +// testMigrationBatchSize is the per-block migration rate the framework +// re-applies on every store open. The rate is no longer a persisted config; +// production re-reads the governance-controlled migration.NumKeysToMigratePerBlock +// param in BeginBlock and re-applies it after every restart, so the framework +// mirrors that by re-applying it whenever a store is (re)opened (open / restart +// / state-sync clone). 0 leaves the migration paused, which is correct for the +// steady-state scenarios; migration scenarios set it for their duration. +var testMigrationBatchSize int + +// applyTestMigrationBatchSize re-applies testMigrationBatchSize to a freshly +// opened store, mimicking the app's BeginBlock push of the gov param. +func applyTestMigrationBatchSize(t *testing.T, cs *CompositeCommitStore) { + t.Helper() + if testMigrationBatchSize > 0 { + require.NoError(t, cs.SetMigrationBatchSize(testMigrationBatchSize)) + } +} + func openComposite(t *testing.T, dir string, cfg config.StateCommitConfig) *CompositeCommitStore { t.Helper() cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) @@ -1490,6 +1508,7 @@ func openComposite(t *testing.T, dir string, cfg config.StateCommitConfig) *Comp require.NoError(t, cs.Initialize(keys.MemIAVLStoreKeys)) _, err = cs.LoadVersion(0, false) require.NoError(t, err) + applyTestMigrationBatchSize(t, cs) t.Cleanup(func() { _ = cs.Close() }) return cs } @@ -1548,6 +1567,7 @@ func stateSyncClone( _, err = dst.LoadVersion(version, false) require.NoError(t, err) + applyTestMigrationBatchSize(t, dst) t.Cleanup(func() { _ = dst.Close() }) // State sync must reproduce identical committed state. When the source is diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 41f96a9dfe..3ec7a5f1ef 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -93,6 +93,19 @@ type CompositeCommitStore struct { // shouldIncludeMemiavlInfos for the gating rules. memiavlHashExcluded atomic.Bool + // migrationBatchSize is the governance-controlled number of keys to + // migrate per block, pushed in via SetMigrationBatchSize (the app reads + // the NumKeysToMigratePerBlock gov param each BeginBlock and forwards it + // here through the rootmulti store). 0 means the migration is paused; it + // is the sole source of the per-block batch size (there is no node-local + // config fallback). + // + // Atomic because SetMigrationBatchSize (consensus goroutine, between + // blocks) and the build paths that read it must not tear; it mirrors + // the between-blocks-write / unsynchronized-read contract used by the + // other sticky flags on this struct. + migrationBatchSize atomic.Int64 + // migrationAdvancedThisCommit gates per-block migration progress // against rootmulti.Store's double-flush pattern. rootmulti calls // flush() once inside GetWorkingHash (whose result is the AppHash @@ -444,7 +457,7 @@ func (cs *CompositeCommitStore) resolveCurrentWriteMode(closeIdleFlatKV bool) er func (cs *CompositeCommitStore) buildRouter() error { routerCtx, cancel := context.WithCancel(cs.ctx) router, err := migration.BuildRouter( - routerCtx, cs.currentWriteMode, cs.memIAVL, cs.flatKV, cs.config.KeysToMigratePerBlock) + routerCtx, cs.currentWriteMode, cs.memIAVL, cs.flatKV, int(cs.migrationBatchSize.Load())) if err != nil { cancel() return fmt.Errorf("failed to build router: %w", err) @@ -457,6 +470,57 @@ func (cs *CompositeCommitStore) buildRouter() error { return nil } +// SetMigrationBatchSize records the governance-controlled migration batch +// size and pushes it into the live router. Only a migration router acts on it +// (every other router treats it as a no-op), so this is safe to call in any +// write mode. A batch size of 0 pauses the migration. +// +// This is the single chokepoint feeding the router builders and the +// MigrationManager, so it normalizes the value here: a negative rate is +// meaningless and is clamped to 0 (paused). The lower layers therefore trust +// the batch size to be non-negative and do no validation of their own. +// +// Must be called between blocks (the app calls it from BeginBlock, before any +// ApplyChangeSets). The router's threadSafeRouter wrapper serializes the push +// against concurrent reads. +func (cs *CompositeCommitStore) SetMigrationBatchSize(batchSize int) error { + if batchSize < 0 { + batchSize = 0 + } + cs.migrationBatchSize.Store(int64(batchSize)) + if cs.router != nil { + cs.router.SetMigrationBatchSize(batchSize) + } + return nil +} + +// GetMigrationBatchSize returns the governance-controlled migration batch size +// most recently pushed via SetMigrationBatchSize (0 when never set / paused). +// It is intended for observability and tests. +func (cs *CompositeCommitStore) GetMigrationBatchSize() int { + return int(cs.migrationBatchSize.Load()) +} + +// GetWriteMode returns the effective write mode currently driving routing. +// Under types.Auto this is the mode derived from migration metadata (and +// advanced by SetWriteMode); under a fixed configuration it equals the +// configured mode. Callers that gate consensus-relevant transitions on it +// must observe it between blocks for the same reasons SetWriteMode documents. +func (cs *CompositeCommitStore) GetWriteMode() types.WriteMode { + return cs.currentWriteMode +} + +// ConfiguredWriteMode reports the write mode set by configuration, before any +// Auto derivation. It is types.Auto for an auto store (whose effective +// GetWriteMode is derived from migration metadata) and the pinned mode for any +// fixed configuration. The migration kick-off needs this to tell an auto store +// resting in memiavl_only (which it may advance to migrate_evm) apart from a +// node pinned to fixed memiavl_only — the two report the same effective mode, +// but only the former is allowed to transition at runtime. +func (cs *CompositeCommitStore) ConfiguredWriteMode() types.WriteMode { + return cs.config.WriteMode +} + // SetWriteMode transitions the effective write mode at runtime. Only legal // when the configured mode is types.Auto; with any fixed configuration the // write mode cannot change without a restart. diff --git a/sei-db/state_db/sc/composite/store_auto_test.go b/sei-db/state_db/sc/composite/store_auto_test.go index 670361ab11..086f37df43 100644 --- a/sei-db/state_db/sc/composite/store_auto_test.go +++ b/sei-db/state_db/sc/composite/store_auto_test.go @@ -19,10 +19,9 @@ import ( // transition-safety rules (only adjacent forward steps, no exits from an // in-flight migration). -func autoConfig(batch int) config.StateCommitConfig { +func autoConfig() config.StateCommitConfig { cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.Auto - cfg.KeysToMigratePerBlock = batch cfg.MemIAVLConfig.AsyncCommitBuffer = 0 return cfg } @@ -30,14 +29,34 @@ func autoConfig(batch int) config.StateCommitConfig { // openAutoStore opens (or reopens) a composite store at dir in Auto mode. func openAutoStore(t *testing.T, dir string, batch int) *CompositeCommitStore { t.Helper() - cs, err := NewCompositeCommitStore(t.Context(), dir, autoConfig(batch)) + cs, err := NewCompositeCommitStore(t.Context(), dir, autoConfig()) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(batch)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) return cs } +// TestComposite_SetMigrationBatchSize_ClampsNegative pins the top-layer +// fallback: SetMigrationBatchSize is the single chokepoint feeding the router +// builders and the MigrationManager, so a negative (meaningless) rate is +// normalized to 0 (paused) here and the lower layers do no validation. +func TestComposite_SetMigrationBatchSize_ClampsNegative(t *testing.T) { + dir := t.TempDir() + cs := openAutoStore(t, dir, 0) + defer func() { _ = cs.Close() }() + + require.NoError(t, cs.SetMigrationBatchSize(-5)) + require.Equal(t, 0, cs.GetMigrationBatchSize(), "negative batch size must clamp to 0") + + require.NoError(t, cs.SetMigrationBatchSize(100)) + require.Equal(t, 100, cs.GetMigrationBatchSize()) + + require.NoError(t, cs.SetMigrationBatchSize(-1)) + require.Equal(t, 0, cs.GetMigrationBatchSize(), "negative batch size must re-clamp to 0") +} + // runBlocks applies and commits n workload blocks. func runBlocks(t *testing.T, cs *CompositeCommitStore, workload *migrationWorkload, n int) { t.Helper() @@ -225,7 +244,7 @@ func TestComposite_SetWriteModeRequiresAutoConfig(t *testing.T) { } func TestComposite_SetWriteModeBeforeLoadVersion(t *testing.T) { - cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), autoConfig(10)) + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), autoConfig()) require.NoError(t, err) require.Error(t, cs.SetWriteMode(types.MigrateEVM)) } @@ -276,18 +295,19 @@ func TestComposite_Auto_RestartResume(t *testing.T) { // autoExportConfig is autoConfig with per-block memiavl snapshots so the // exporter can serve any committed version. -func autoExportConfig(batch int) config.StateCommitConfig { - cfg := autoConfig(batch) +func autoExportConfig() config.StateCommitConfig { + cfg := autoConfig() cfg.MemIAVLConfig.SnapshotInterval = 1 cfg.MemIAVLConfig.SnapshotMinTimeInterval = 0 return cfg } // openAutoStoreWithConfig mirrors openAutoStore for a caller-supplied config. -func openAutoStoreWithConfig(t *testing.T, dir string, cfg config.StateCommitConfig) *CompositeCommitStore { +func openAutoStoreWithConfig(t *testing.T, dir string, cfg config.StateCommitConfig, batch int) *CompositeCommitStore { t.Helper() cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(batch)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -317,7 +337,7 @@ func TestComposite_Auto_ExportExcludesFlatKVUntilMigrationStarts(t *testing.T) { dir := t.TempDir() workload := newMigrationWorkload(0xA076) - cs := openAutoStoreWithConfig(t, dir, autoExportConfig(100)) + cs := openAutoStoreWithConfig(t, dir, autoExportConfig(), 100) defer func() { _ = cs.Close() }() runBlocks(t, cs, workload, 3) h := cs.Version() @@ -353,9 +373,9 @@ func TestComposite_Auto_ExportExcludesFlatKVUntilMigrationStarts(t *testing.T) { // after which derivation and reads work from the imported state. func TestComposite_Auto_ExportImportRoundTrip(t *testing.T) { workload := newMigrationWorkload(0xA077) - cfg := autoExportConfig(100) + cfg := autoExportConfig() - src := openAutoStoreWithConfig(t, t.TempDir(), cfg) + src := openAutoStoreWithConfig(t, t.TempDir(), cfg, 100) runBlocks(t, src, workload, 3) require.NoError(t, src.SetWriteMode(types.MigrateEVM)) runUntilAtMigrationVersion(t, src, workload, migration.Version1_MigrateEVM, 200) @@ -372,7 +392,7 @@ func TestComposite_Auto_ExportImportRoundTrip(t *testing.T) { // Fresh Auto destination: no flatkv directory, no flatkv instance. dstDir := t.TempDir() - dst := openAutoStoreWithConfig(t, dstDir, cfg) + dst := openAutoStoreWithConfig(t, dstDir, cfg, 100) require.NoError(t, dst.Close()) require.Nil(t, dst.flatKV) @@ -556,7 +576,7 @@ func TestComposite_Auto_ReadOnlyHandle(t *testing.T) { // heights keep loading flatkv. func TestComposite_Auto_ReadOnlyPreFlatKVEraHeight(t *testing.T) { dir := t.TempDir() - cs := openAutoStoreWithConfig(t, dir, autoExportConfig(100)) + cs := openAutoStoreWithConfig(t, dir, autoExportConfig(), 100) defer func() { _ = cs.Close() }() // Heights 1..5 in the memiavl-only era, with a known per-height value. @@ -605,7 +625,7 @@ func TestComposite_Auto_ReadOnlyPreFlatKVEraHeight(t *testing.T) { } func TestComposite_Auto_InitializeRejectsNonCanonicalStores(t *testing.T) { - cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), autoConfig(10)) + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), autoConfig()) require.NoError(t, err) require.Error(t, cs.Initialize([]string{"not-a-canonical-store"}), "Auto must enforce canonical store names since the mode may become mixed") diff --git a/sei-db/state_db/sc/composite/store_migration_test.go b/sei-db/state_db/sc/composite/store_migration_test.go index 1e19b47ac5..2b5399c679 100644 --- a/sei-db/state_db/sc/composite/store_migration_test.go +++ b/sei-db/state_db/sc/composite/store_migration_test.go @@ -260,11 +260,11 @@ func driveMigrationWorkload( migCfg := config.DefaultStateCommitConfig() migCfg.WriteMode = types.MigrateEVM - migCfg.KeysToMigratePerBlock = keysToMigratePerBlock migCfg.MemIAVLConfig.AsyncCommitBuffer = 0 cs, err = NewCompositeCommitStore(t.Context(), dir, migCfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(keysToMigratePerBlock)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -288,11 +288,11 @@ func reopenInMigrateEVM(t *testing.T, dir string, batch int) *CompositeCommitSto t.Helper() cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = batch cfg.MemIAVLConfig.AsyncCommitBuffer = 0 cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(batch)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) diff --git a/sei-db/state_db/sc/composite/store_test.go b/sei-db/state_db/sc/composite/store_test.go index 35226125be..d74fc7d34b 100644 --- a/sei-db/state_db/sc/composite/store_test.go +++ b/sei-db/state_db/sc/composite/store_test.go @@ -448,10 +448,9 @@ func TestMemiavlOnlyToMigrateEVMPreservesLastCommitInfoBeforeFirstCommit(t *test // height. migrateCfg := config.DefaultStateCommitConfig() migrateCfg.WriteMode = types.MigrateEVM - migrateCfg.KeysToMigratePerBlock = 100 - cs2, err := NewCompositeCommitStore(t.Context(), dir, migrateCfg) require.NoError(t, err) + require.NoError(t, cs2.SetMigrationBatchSize(100)) require.NoError(t, cs2.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) @@ -493,10 +492,9 @@ func TestMemiavlOnlyToMigrateEVMPreservesLastCommitInfoBeforeFirstCommit(t *test func TestMigrateEVMGenesisPreFirstCommitOmitsLatticeHash(t *testing.T) { cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -530,10 +528,9 @@ func TestMigrateEVMGenesisPreFirstCommitOmitsLatticeHash(t *testing.T) { func TestMigrateEVMIncludesLatticeHashAfterFirstCommit(t *testing.T) { cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -579,10 +576,9 @@ func TestMigrateEVMLatticeRemainsAfterRestartPostMigrationCompletion(t *testing. // iterator's first batch reports MigrationBoundaryComplete and the // manager atomically deletes the boundary key and writes the version // key on the same commit. - cfg.KeysToMigratePerBlock = 1000 - cs1, err := NewCompositeCommitStore(t.Context(), dir, cfg) require.NoError(t, err) + require.NoError(t, cs1.SetMigrationBatchSize(1000)) require.NoError(t, cs1.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs1.LoadVersion(0, false) require.NoError(t, err) @@ -772,7 +768,6 @@ func TestGetLatestVersionBothBackendsAligned(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 // Force synchronous memiavl WAL writes so the on-disk tail // reflects every Commit before GetLatestVersion reads it (the // flatkv side is already synchronous). See the doc comment on @@ -781,6 +776,7 @@ func TestGetLatestVersionBothBackendsAligned(t *testing.T) { cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -817,10 +813,9 @@ func TestReadOnlyLoadVersionFailsLoudWhenFlatKVUnavailable(t *testing.T) { // Need flatkv to be allocated and exercised by LoadVersion; // MemiavlOnly would not touch the flatkv path at all. cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) @@ -929,10 +924,9 @@ func TestLoadVersionFlatKVOnlyReadOnly(t *testing.T) { func TestLoadVersionRebuildsRouterOnReload(t *testing.T) { cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) @@ -962,10 +956,9 @@ func TestLoadVersionRebuildsRouterOnReload(t *testing.T) { func TestLoadVersionDoesNotMountMigrationStoreInMigrationMode(t *testing.T) { cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err, "LoadVersion in migration mode must succeed without mounting a migration tree on memiavl") @@ -1185,10 +1178,9 @@ func TestExporterFailsLoudOnInHistoryFlatKVLoadFailure(t *testing.T) { cfg := config.DefaultStateCommitConfig() cfg.MemIAVLConfig.AsyncCommitBuffer = 0 cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -1224,10 +1216,9 @@ func TestExporterOmitsFlatKVForPreEraVersion(t *testing.T) { cfg.MemIAVLConfig.SnapshotMinTimeInterval = 0 cfg.MemIAVLConfig.AsyncCommitBuffer = 0 cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -1922,10 +1913,9 @@ func TestMigrationEntrySeedingMemiavlToMigrateEVM(t *testing.T) { // version 100 so the very next commit produces version 101 on both. migrateCfg := config.DefaultStateCommitConfig() migrateCfg.WriteMode = types.MigrateEVM - migrateCfg.KeysToMigratePerBlock = 100 - cs2, err := NewCompositeCommitStore(t.Context(), dir, migrateCfg) require.NoError(t, err) + require.NoError(t, cs2.SetMigrationBatchSize(100)) require.NoError(t, cs2.Initialize([]string{"bank", keys.EVMStoreKey})) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) @@ -1993,11 +1983,11 @@ func TestMigrateEVMReopenPreservesPreFlipLastCommitInfo(t *testing.T) { migrateCfg := config.DefaultStateCommitConfig() migrateCfg.WriteMode = types.MigrateEVM - migrateCfg.KeysToMigratePerBlock = 1 migrateCfg.MemIAVLConfig.AsyncCommitBuffer = 0 cs2, err := NewCompositeCommitStore(t.Context(), dir, migrateCfg) require.NoError(t, err) + require.NoError(t, cs2.SetMigrationBatchSize(1)) require.NoError(t, cs2.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) @@ -2059,10 +2049,9 @@ func TestMigrationEntrySeedingIsIdempotentAcrossRestarts(t *testing.T) { migrateCfg := config.DefaultStateCommitConfig() migrateCfg.WriteMode = types.MigrateEVM - migrateCfg.KeysToMigratePerBlock = 100 - cs2, err := NewCompositeCommitStore(t.Context(), dir, migrateCfg) require.NoError(t, err) + require.NoError(t, cs2.SetMigrationBatchSize(100)) require.NoError(t, cs2.Initialize([]string{"bank", keys.EVMStoreKey})) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) @@ -2130,10 +2119,9 @@ func TestSetInitialVersionMemiavlOnly(t *testing.T) { func TestSetInitialVersionDelegatesToBothBackends(t *testing.T) { cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{"bank", keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -2167,10 +2155,9 @@ func TestSetInitialVersionDelegatesToBothBackends(t *testing.T) { func TestSetInitialVersionRetryIsIdempotent(t *testing.T) { cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM - cfg.KeysToMigratePerBlock = 100 - cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) require.NoError(t, err) + require.NoError(t, cs.SetMigrationBatchSize(100)) require.NoError(t, cs.Initialize([]string{"bank", keys.EVMStoreKey})) _, err = cs.LoadVersion(0, false) require.NoError(t, err) @@ -2582,10 +2569,9 @@ func TestLoadVersionReadOnlyDuringMigrateEVMTransition(t *testing.T) { // flagged. migrateCfg := config.DefaultStateCommitConfig() migrateCfg.WriteMode = types.MigrateEVM - migrateCfg.KeysToMigratePerBlock = 100 - cs2, err := NewCompositeCommitStore(t.Context(), dir, migrateCfg) require.NoError(t, err) + require.NoError(t, cs2.SetMigrationBatchSize(100)) require.NoError(t, cs2.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) diff --git a/sei-db/state_db/sc/memiavl/store.go b/sei-db/state_db/sc/memiavl/store.go index 3c0ce33c25..290e6bdb7f 100644 --- a/sei-db/state_db/sc/memiavl/store.go +++ b/sei-db/state_db/sc/memiavl/store.go @@ -103,6 +103,12 @@ func (cs *CommitStore) SetWriteMode(types.WriteMode) error { return fmt.Errorf("memiavl commit store does not support runtime write-mode changes") } +// SetMigrationBatchSize implements types.Committer. The memiavl commit +// store runs no migration of its own, so this is a no-op. +func (cs *CommitStore) SetMigrationBatchSize(int) error { + return nil +} + // Copy returns an O(1) memiavl snapshot; COW nodes are shared with the live store. func (cs *CommitStore) Copy() types.Committer { if cs == nil || cs.db == nil { diff --git a/sei-db/state_db/sc/migration/dual_write_router.go b/sei-db/state_db/sc/migration/dual_write_router.go index 5d0b07e7f0..32dc7ec82d 100644 --- a/sei-db/state_db/sc/migration/dual_write_router.go +++ b/sei-db/state_db/sc/migration/dual_write_router.go @@ -73,6 +73,10 @@ func (t *TestOnlyDualWriteRouter) Read(store string, key []byte) ([]byte, bool, return value, found, nil } +// SetMigrationBatchSize is a no-op: the dual-write router duplicates +// traffic and performs no boundary-advancing data migration. +func (t *TestOnlyDualWriteRouter) SetMigrationBatchSize(int) {} + // BuildRoute returns a Route that dispatches the given module names to // this DualWriteRouter. Reads, writes and proof requests for those // modules will all flow through this dual-write router. diff --git a/sei-db/state_db/sc/migration/migration_manager.go b/sei-db/state_db/sc/migration/migration_manager.go index 74c23ca64c..daa51286f8 100644 --- a/sei-db/state_db/sc/migration/migration_manager.go +++ b/sei-db/state_db/sc/migration/migration_manager.go @@ -98,9 +98,12 @@ func NewMigrationManager( if iterator == nil { return nil, errors.New("iterator must not be nil") } - if migrationBatchSize <= 0 { - return nil, fmt.Errorf("migration batch size must be positive, got %d", migrationBatchSize) - } + // A batch size of 0 is a valid "paused" state: the migration manager + // is wired up and routes caller writes, but advances no keys per block + // until SetMigrationBatchSize raises it above 0 (the governance param + // acts as the migration trigger). The batch size is normalized to be + // non-negative by the caller (CompositeCommitStore.SetMigrationBatchSize), + // so no negativity check is needed here. if startVersion >= targetVersion { return nil, fmt.Errorf("startVersion (%d) must be strictly less than targetVersion (%d)", startVersion, targetVersion) @@ -285,8 +288,15 @@ func (m *MigrationManager) ApplyChangeSets(changesets []*proto.NamedChangeSet, f oldDBPairsByStore := make(map[string]map[string]*proto.KVPair) newDBPairsByStore := make(map[string]map[string]*proto.KVPair) + // advanceMigration gates the once-per-block boundary advance. It is + // suppressed when the batch size is 0 (migration paused): caller writes + // still route below, but no keys are pulled forward and no boundary + // metadata is rewritten, so the migration holds at its current cursor + // until the batch size is raised again. + advanceMigration := firstBatchInBlock && m.migrationBatchSize > 0 + batchStats := migrationBatchStats{} - if firstBatchInBlock { + if advanceMigration { // Get the next batch of keys to migrate. valuesToMigrate, newBoundary, err := m.iterator.NextBatch(m.migrationBatchSize) if err != nil { @@ -332,7 +342,7 @@ func (m *MigrationManager) ApplyChangeSets(changesets []*proto.NamedChangeSet, f migrationComplete := false metadataPairsWritten := int64(0) - if firstBatchInBlock { + if advanceMigration { migrationComplete = m.boundary.Equals(MigrationBoundaryComplete) metadataPairsWritten = 1 if migrationComplete { @@ -376,7 +386,7 @@ func (m *MigrationManager) ApplyChangeSets(changesets []*proto.NamedChangeSet, f // silently drop those stats. keysMigrated and the metadata pair count stay zero on // non-first calls because the migration-advance branch above is skipped. m.metrics.RecordBatch(batchStats) - if firstBatchInBlock && migrationComplete { + if advanceMigration && migrationComplete { m.logMigrationCompleteSummary() } @@ -472,6 +482,17 @@ func (m *MigrationManager) GetProof(store string, key []byte) (*ics23.Commitment return nil, fmt.Errorf("state proofs not supported for store %q", store) } +// SetMigrationBatchSize implements [Router]. It updates the number of keys +// the manager pulls forward per block. 0 pauses the migration; a positive +// value resumes it from the persisted boundary. +// +// Not safe for concurrent use; wrap with NewThreadSafeRouter (BuildRouter +// does this for migration modes, so the threadSafeRouter write lock +// serializes this against ApplyChangeSets / Read). +func (m *MigrationManager) SetMigrationBatchSize(batchSize int) { + m.migrationBatchSize = batchSize +} + // BuildRoute returns a Route that dispatches the given module names to // this MigrationManager. Reads, writes and proof requests for those // modules will all flow through this migration manager. @@ -480,5 +501,13 @@ func (m *MigrationManager) GetProof(store string, key []byte) (*ics23.Commitment // returned Route may be passed to NewModuleRouter alongside other // Routes to compose multi-database setups. func (m *MigrationManager) BuildRoute(moduleNames ...string) (*Route, error) { - return NewRoute(m.Read, m.ApplyChangeSets, m.GetProof, moduleNames...) + route, err := NewRoute(m.Read, m.ApplyChangeSets, m.GetProof, moduleNames...) + if err != nil { + return nil, err + } + // Record the manager as the route's owner so a ModuleRouter can + // propagate SetMigrationBatchSize back to it (the accessors above are + // closures and otherwise hide the manager). + route.owner = m + return route, nil } diff --git a/sei-db/state_db/sc/migration/migration_manager_test.go b/sei-db/state_db/sc/migration/migration_manager_test.go index 2fc730a44a..ad226ba5b9 100644 --- a/sei-db/state_db/sc/migration/migration_manager_test.go +++ b/sei-db/state_db/sc/migration/migration_manager_test.go @@ -842,23 +842,20 @@ func fuzzApplyToReference(ref map[string]map[string][]byte, changesets []*proto. // --- Constructor validation (Issue 2, Issue 11) --- -func TestNewMigrationManager_RejectsNonPositiveBatchSize(t *testing.T) { - cases := []int{0, -1, -100} - for _, size := range cases { - t.Run(fmt.Sprintf("size=%d", size), func(t *testing.T) { - oldDB := newMockDB() - newDB := newMockDB() - iter := NewMockMigrationIterator(nil, false) +// A batch size of 0 is valid: the migration manager comes up paused and +// advances no keys until SetMigrationBatchSize raises it above 0. +func TestNewMigrationManager_AcceptsZeroBatchSize(t *testing.T) { + oldDB := newMockDB() + newDB := newMockDB() + iter := NewMockMigrationIterator(nil, false) - _, err := newTestManager(t, - oldDB.reader(), oldDB.writer(), - newDB.reader(), newDB.writer(), - iter, size, - ) - require.Error(t, err) - require.Contains(t, err.Error(), "batch size must be positive") - }) - } + m, err := newTestManager(t, + oldDB.reader(), oldDB.writer(), + newDB.reader(), newDB.writer(), + iter, 0, + ) + require.NoError(t, err) + require.NotNil(t, m) } func TestNewMigrationManager_NilDependencies(t *testing.T) { diff --git a/sei-db/state_db/sc/migration/migration_test_framework_test.go b/sei-db/state_db/sc/migration/migration_test_framework_test.go index bbbb643a90..34a938ff56 100644 --- a/sei-db/state_db/sc/migration/migration_test_framework_test.go +++ b/sei-db/state_db/sc/migration/migration_test_framework_test.go @@ -53,6 +53,12 @@ func (m *TestMultiDB) GetProof(store string, key []byte) (*ics23.CommitmentProof panic("not implemented") } +func (m *TestMultiDB) SetMigrationBatchSize(batchSize int) { + for _, nestedDB := range m.nestedDBs { + nestedDB.SetMigrationBatchSize(batchSize) + } +} + func (m *TestMultiDB) Read(store string, key []byte) ([]byte, bool, error) { if len(m.nestedDBs) == 0 { return nil, false, fmt.Errorf("no nested databases configured") @@ -103,6 +109,8 @@ func (r *TestFlatKVRouter) GetProof(store string, key []byte) (*ics23.Commitment return nil, errors.New("TestFlatKVRouter does not support proofs") } +func (r *TestFlatKVRouter) SetMigrationBatchSize(int) {} + // TestMemIAVLRouter is a [Router] that sends all operations to a single underlying // memiavl.CommitStore. It does not support iteration or proofs. type TestMemIAVLRouter struct { @@ -132,6 +140,8 @@ func (r *TestMemIAVLRouter) GetProof(store string, key []byte) (*ics23.Commitmen return nil, errors.New("TestMemIAVLRouter does not support proofs") } +func (r *TestMemIAVLRouter) SetMigrationBatchSize(int) {} + // TestInMemoryRouter is a [Router] backed by an in-memory map. The outer map keys // are store (module) names and the inner map keys are store keys. It does not // support iteration or proofs. @@ -185,6 +195,8 @@ func (r *TestInMemoryRouter) GetProof(store string, key []byte) (*ics23.Commitme return nil, errors.New("TestInMemoryRouter does not support proofs") } +func (r *TestInMemoryRouter) SetMigrationBatchSize(int) {} + // VerifyKeyPlacement verifies that every key in the oracle is in the correct backend. // Keys whose store name appears in flatKVStores must be readable from flatKVRouter and // absent from memiavlRouter. All other keys must be in memiavlRouter and absent from diff --git a/sei-db/state_db/sc/migration/migration_types.go b/sei-db/state_db/sc/migration/migration_types.go index 696e28845a..244c67640f 100644 --- a/sei-db/state_db/sc/migration/migration_types.go +++ b/sei-db/state_db/sc/migration/migration_types.go @@ -73,4 +73,17 @@ type Router interface { // Get a proof of the value for a key in a store. Some stores may not support proofs, // and this method will return an error in that case. GetProof(store string, key []byte) (*ics23.CommitmentProof, error) + + // SetMigrationBatchSize updates the number of keys migrated per block. + // + // Only routers that perform data migration act on this; every other + // router treats it as a no-op. Composite/wrapper routers forward it to + // the underlying migration manager. A value of 0 pauses the migration + // (caller writes still route normally; only the background key transfer + // stops advancing). + // + // Must be called between blocks, on the same goroutine that drives + // ApplyChangeSets. threadSafeRouter additionally serializes it against + // concurrent reads. + SetMigrationBatchSize(batchSize int) } diff --git a/sei-db/state_db/sc/migration/module_router.go b/sei-db/state_db/sc/migration/module_router.go index 2ba592394b..fcaf1e6c33 100644 --- a/sei-db/state_db/sc/migration/module_router.go +++ b/sei-db/state_db/sc/migration/module_router.go @@ -22,6 +22,14 @@ type Route struct { writer DBWriter // For building a proof of the value for a key in a store. If nil, the route does not support proofs. proofBuilder DBProofBuilder + // owner is the Router whose accessors back this route, if any. It lets + // a ModuleRouter propagate control signals (today: + // SetMigrationBatchSize) to the underlying router — chiefly a + // MigrationManager whose Read/ApplyChangeSets/GetProof are otherwise + // only reachable through the closures above. Leaf routes that point + // straight at a single backend (routeToMemIAVL / routeToFlatKV) leave + // it nil, so those signals are skipped for them. + owner Router } // NewRoute creates a new Route. @@ -167,3 +175,21 @@ func (m *ModuleRouter) GetProof(store string, key []byte) (*ics23.CommitmentProo } return r.proofBuilder(store, key) } + +// SetMigrationBatchSize forwards the new batch size to every route that +// has an owning Router (e.g. a MigrationManager). Routes that point +// directly at a single backend have no owner and are skipped. A given +// owner is signalled at most once even if it backs multiple routes. +func (m *ModuleRouter) SetMigrationBatchSize(batchSize int) { + signalled := make(map[Router]struct{}, len(m.routes)) + for _, r := range m.routes { + if r.owner == nil { + continue + } + if _, done := signalled[r.owner]; done { + continue + } + signalled[r.owner] = struct{}{} + r.owner.SetMigrationBatchSize(batchSize) + } +} diff --git a/sei-db/state_db/sc/migration/passthrough_router.go b/sei-db/state_db/sc/migration/passthrough_router.go index 7ac332d27f..565d3cf348 100644 --- a/sei-db/state_db/sc/migration/passthrough_router.go +++ b/sei-db/state_db/sc/migration/passthrough_router.go @@ -68,3 +68,7 @@ func (p *PassthroughRouter) GetProof(store string, key []byte) (*ics23.Commitmen } return p.proofBuilder(store, key) } + +// SetMigrationBatchSize is a no-op: a passthrough router targets a single +// backend and performs no data migration. +func (p *PassthroughRouter) SetMigrationBatchSize(int) {} diff --git a/sei-db/state_db/sc/migration/router_builder.go b/sei-db/state_db/sc/migration/router_builder.go index f2f8ccbe3e..3a15ee6e7a 100644 --- a/sei-db/state_db/sc/migration/router_builder.go +++ b/sei-db/state_db/sc/migration/router_builder.go @@ -158,10 +158,6 @@ func buildMigrateEVMRouter( if flatKV == nil { return nil, fmt.Errorf("flatKV is nil") } - if migrationBatchSize <= 0 { - return nil, fmt.Errorf("migrationBatchSize must be greater than 0") - } - // Manages migration and routing for keys in the evm/ module. migrationManager, err := NewMigrationManager( migrationBatchSize, @@ -282,10 +278,6 @@ func buildMigrateAllButBankRouter( if flatKV == nil { return nil, fmt.Errorf("flatKV is nil") } - if migrationBatchSize <= 0 { - return nil, fmt.Errorf("migrationBatchSize must be greater than 0") - } - allModulesButEvmAndBank, err := keys.AllModulesExcept(keys.EVMStoreKey, keys.BankStoreKey) if err != nil { return nil, fmt.Errorf("AllModulesExcept: %w", err) @@ -411,10 +403,6 @@ func buildMigrateBankRouter( if flatKV == nil { return nil, fmt.Errorf("flatKV is nil") } - if migrationBatchSize <= 0 { - return nil, fmt.Errorf("migrationBatchSize must be greater than 0") - } - allButBankModules, err := keys.AllModulesExcept(keys.BankStoreKey) if err != nil { return nil, fmt.Errorf("AllModulesExcept: %w", err) diff --git a/sei-db/state_db/sc/migration/router_kvstore_test.go b/sei-db/state_db/sc/migration/router_kvstore_test.go index 8e6ad34dba..c0979b1d17 100644 --- a/sei-db/state_db/sc/migration/router_kvstore_test.go +++ b/sei-db/state_db/sc/migration/router_kvstore_test.go @@ -215,6 +215,8 @@ func (f *failingRouter) GetProof(string, []byte) (*ics23.CommitmentProof, error) return nil, errors.New("failingRouter.GetProof: not used by these tests") } +func (f *failingRouter) SetMigrationBatchSize(int) {} + func TestRouterCommitKVStore_GetPanicsOnRouterError(t *testing.T) { store := NewRouterCommitKVStore( staticRouter(&failingRouter{readErr: errors.New("boom")}), diff --git a/sei-db/state_db/sc/migration/thread_safe_router.go b/sei-db/state_db/sc/migration/thread_safe_router.go index a4acff8f56..ebf6cacd69 100644 --- a/sei-db/state_db/sc/migration/thread_safe_router.go +++ b/sei-db/state_db/sc/migration/thread_safe_router.go @@ -56,3 +56,12 @@ func (r *threadSafeRouter) GetProof(store string, key []byte) (*ics23.Commitment defer r.mu.RUnlock() return r.inner.GetProof(store, key) } + +// SetMigrationBatchSize forwards to the inner router under the write lock, +// so the update is serialized against in-flight Read / GetProof / +// ApplyChangeSets calls. +func (r *threadSafeRouter) SetMigrationBatchSize(batchSize int) { + r.mu.Lock() + defer r.mu.Unlock() + r.inner.SetMigrationBatchSize(batchSize) +} diff --git a/sei-db/state_db/sc/migration/thread_safe_router_test.go b/sei-db/state_db/sc/migration/thread_safe_router_test.go index 467babef84..d34e6ab0bb 100644 --- a/sei-db/state_db/sc/migration/thread_safe_router_test.go +++ b/sei-db/state_db/sc/migration/thread_safe_router_test.go @@ -48,6 +48,8 @@ func (b *blockingRouter) GetProof(_ string, _ []byte) (*ics23.CommitmentProof, e return nil, errors.New("proof: not implemented in blockingRouter") } +func (b *blockingRouter) SetMigrationBatchSize(int) {} + // expectEntered receives one message from ch and asserts it equals expected. // Fails the test if no message arrives within timeout. func expectEntered(t *testing.T, ch <-chan string, expected string, timeout time.Duration) { diff --git a/sei-db/state_db/sc/types/types.go b/sei-db/state_db/sc/types/types.go index 2e87f191bc..e789c8c360 100644 --- a/sei-db/state_db/sc/types/types.go +++ b/sei-db/state_db/sc/types/types.go @@ -153,6 +153,19 @@ type Committer interface { // trigger-determinism requirements. SetWriteMode(mode WriteMode) error + // SetMigrationBatchSize sets the number of keys the in-flight + // migration advances per block. Stores with no migration in progress + // (single-backend committers) treat it as a no-op. + // + // A value of 0 pauses the migration: caller writes still route + // normally, but no keys are pulled forward until the size is raised + // again. This governance-supplied value is the sole source of the + // per-block rate; there is no node-local config fallback. + // + // Must be called between blocks, before the next block's first write + // batch, on the consensus goroutine. + SetMigrationBatchSize(batchSize int) error + // Closer releases all backing resources (open files, background // goroutines, locks). After Close the Committer must not be used. io.Closer diff --git a/sei-ibc-go/modules/core/03-connection/types/msgs_test.go b/sei-ibc-go/modules/core/03-connection/types/msgs_test.go index 7b8fafc7b8..82f82d91e0 100644 --- a/sei-ibc-go/modules/core/03-connection/types/msgs_test.go +++ b/sei-ibc-go/modules/core/03-connection/types/msgs_test.go @@ -9,6 +9,7 @@ import ( storetypes "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" storev2rootmulti "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/stretchr/testify/suite" @@ -47,6 +48,9 @@ func (suite *MsgTestSuite) SetupTest() { app := simapp.Setup(false) scConfig := seidbconfig.DefaultStateCommitConfig() + // Mounts a non-canonical store name, so it cannot use the default auto + // mode; pin memiavl_only (this test store never migrates). + scConfig.WriteMode = sctypes.MemiavlOnly scConfig.MemIAVLConfig.AsyncCommitBuffer = 0 scConfig.MemIAVLConfig.SnapshotMinTimeInterval = 0 ssConfig := seidbconfig.StateStoreConfig{} diff --git a/sei-ibc-go/modules/core/04-channel/types/msgs_test.go b/sei-ibc-go/modules/core/04-channel/types/msgs_test.go index 238622586f..3529452b7c 100644 --- a/sei-ibc-go/modules/core/04-channel/types/msgs_test.go +++ b/sei-ibc-go/modules/core/04-channel/types/msgs_test.go @@ -9,6 +9,7 @@ import ( storev2rootmulti "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/stretchr/testify/suite" @@ -71,6 +72,9 @@ func (suite *TypesTestSuite) SetupTest() { app := simapp.Setup(false) scConfig := seidbconfig.DefaultStateCommitConfig() + // Mounts a non-canonical store name, so it cannot use the default auto + // mode; pin memiavl_only (this test store never migrates). + scConfig.WriteMode = sctypes.MemiavlOnly scConfig.MemIAVLConfig.AsyncCommitBuffer = 0 scConfig.MemIAVLConfig.SnapshotMinTimeInterval = 0 ssConfig := seidbconfig.StateStoreConfig{} diff --git a/sei-ibc-go/modules/core/23-commitment/types/commitment_test.go b/sei-ibc-go/modules/core/23-commitment/types/commitment_test.go index a26e5ee653..1fa20e16c8 100644 --- a/sei-ibc-go/modules/core/23-commitment/types/commitment_test.go +++ b/sei-ibc-go/modules/core/23-commitment/types/commitment_test.go @@ -6,6 +6,7 @@ import ( storetypes "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" storev2rootmulti "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" "github.com/stretchr/testify/suite" ) @@ -19,6 +20,9 @@ type MerkleTestSuite struct { func (suite *MerkleTestSuite) SetupTest() { scConfig := seidbconfig.DefaultStateCommitConfig() + // Mounts a non-canonical store name, so it cannot use the default auto + // mode; pin memiavl_only (this test store never migrates). + scConfig.WriteMode = sctypes.MemiavlOnly scConfig.MemIAVLConfig.AsyncCommitBuffer = 0 scConfig.MemIAVLConfig.SnapshotMinTimeInterval = 0 ssConfig := seidbconfig.StateStoreConfig{} diff --git a/sei-ibc-go/testing/simapp/test_helpers.go b/sei-ibc-go/testing/simapp/test_helpers.go index 05d3932e31..b0ffebf156 100644 --- a/sei-ibc-go/testing/simapp/test_helpers.go +++ b/sei-ibc-go/testing/simapp/test_helpers.go @@ -34,6 +34,7 @@ import ( stakingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/keeper" stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" @@ -73,6 +74,11 @@ func setup(withGenesis bool, invCheckPeriod uint) (*SimApp, GenesisState) { panic(err) } scConfig := seidbconfig.DefaultStateCommitConfig() + // This simapp mounts non-canonical store names (e.g. icacontroller, + // icahost) that are not in keys.MemIAVLStoreKeys, so it cannot use the + // default auto mode (which validates store names for migration + // routability). Pin memiavl_only: this app never migrates. + scConfig.WriteMode = sctypes.MemiavlOnly scConfig.MemIAVLConfig.AsyncCommitBuffer = 0 scConfig.MemIAVLConfig.SnapshotMinTimeInterval = 0 scConfig.HistoricalProofRateLimit = 0 diff --git a/sei-wasmd/app/test_helpers.go b/sei-wasmd/app/test_helpers.go index 013f60d37f..f65bd14fac 100644 --- a/sei-wasmd/app/test_helpers.go +++ b/sei-wasmd/app/test_helpers.go @@ -48,6 +48,7 @@ import ( seiapp "github.com/sei-protocol/sei-chain/app" storev2rootmulti "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" + sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" "github.com/sei-protocol/sei-chain/sei-wasmd/x/wasm" ) @@ -79,6 +80,11 @@ func setup(t testing.TB, withGenesis bool, invCheckPeriod uint, opts ...wasm.Opt require.NoError(t, err) scConfig := seidbconfig.DefaultStateCommitConfig() + // This test app mounts non-canonical store names (e.g. icacontroller, + // icahost) that are not in keys.MemIAVLStoreKeys, so it cannot use the + // default auto mode (which validates store names for migration + // routability). Pin memiavl_only: this app never migrates. + scConfig.WriteMode = sctypes.MemiavlOnly scConfig.MemIAVLConfig.SnapshotInterval = 1 scConfig.MemIAVLConfig.SnapshotMinTimeInterval = 0 scConfig.MemIAVLConfig.AsyncCommitBuffer = 0 diff --git a/x/evm/keeper/trace_snapshot_test.go b/x/evm/keeper/trace_snapshot_test.go index c0ddda691a..b0c0ce47cd 100644 --- a/x/evm/keeper/trace_snapshot_test.go +++ b/x/evm/keeper/trace_snapshot_test.go @@ -30,6 +30,7 @@ func (f *fakeCommitter) Commit() (int64, error) { panic("unuse func (f *fakeCommitter) GetLatestVersion() (int64, error) { panic("unused") } func (f *fakeCommitter) Get(string, []byte) ([]byte, bool, error) { panic("unused") } func (f *fakeCommitter) SetWriteMode(sctypes.WriteMode) error { panic("unused") } +func (f *fakeCommitter) SetMigrationBatchSize(int) error { panic("unused") } func (f *fakeCommitter) Has(string, []byte) (bool, error) { panic("unused") } func (f *fakeCommitter) Iterator(string, []byte, []byte, bool) (dbm.Iterator, error) { panic("unused")