From a8983aecbba5e85b210c30b025a88fa5e3794756 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Thu, 25 Jun 2026 19:49:56 -0700 Subject: [PATCH 01/19] Add gov proposal based migration trigger --- app/abci.go | 24 ++++ app/abci_test.go | 124 ++++++++++++++++++ app/app.go | 19 ++- app/migration/params.go | 45 +++++++ app/migration/params_test.go | 36 +++++ app/seidb.go | 1 - docker/localnode/config/app.toml | 14 +- .../gov_module/gov_proposal_test.yaml | 35 +++++ .../migration_param_change_proposal.json | 13 ++ sei-cosmos/storev2/rootmulti/store.go | 25 ++++ sei-db/config/sc_config.go | 13 +- sei-db/config/toml.go | 17 ++- sei-db/state_db/sc/composite/store.go | 51 ++++++- sei-db/state_db/sc/memiavl/store.go | 6 + .../sc/migration/dual_write_router.go | 4 + .../sc/migration/migration_manager.go | 42 +++++- .../sc/migration/migration_manager_test.go | 22 +++- .../migration_test_framework_test.go | 12 ++ .../state_db/sc/migration/migration_types.go | 13 ++ sei-db/state_db/sc/migration/module_router.go | 26 ++++ .../sc/migration/passthrough_router.go | 4 + .../state_db/sc/migration/router_builder.go | 12 +- .../sc/migration/router_kvstore_test.go | 2 + .../sc/migration/thread_safe_router.go | 9 ++ .../sc/migration/thread_safe_router_test.go | 2 + sei-db/state_db/sc/types/types.go | 13 ++ x/evm/keeper/trace_snapshot_test.go | 1 + 27 files changed, 549 insertions(+), 36 deletions(-) create mode 100644 app/abci_test.go create mode 100644 app/migration/params.go create mode 100644 app/migration/params_test.go create mode 100644 integration_test/gov_module/proposal/migration_param_change_proposal.json diff --git a/app/abci.go b/app/abci.go index 9683d03f2e..15a1ca0a72 100644 --- a/app/abci.go +++ b/app/abci.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "fmt" + "math" "math/big" "time" @@ -13,6 +14,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 +55,34 @@ 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, falling back to the node-local sc-keys-to-migrate-per-block config. +func (app *App) applyMigrationBatchSize(ctx sdk.Context) { + if app.scStore == nil { + return + } + numKeys := migration.DefaultNumKeysToMigratePerBlock + if subspace, ok := app.ParamsKeeper.GetSubspace(migration.SubspaceName); ok { + subspace.GetIfExists(ctx, migration.KeyNumKeysToMigratePerBlock, &numKeys) + } + if numKeys > uint64(math.MaxInt64) { + numKeys = uint64(math.MaxInt64) + } + if err := app.scStore.SetMigrationBatchSize(int(numKeys)); err != nil { + logger.Error("failed to set SC migration batch size", "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..1a5ab02f75 --- /dev/null +++ b/app/abci_test.go @@ -0,0 +1,124 @@ +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.scStore.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.scStore.GetMigrationBatchSize() + require.Equal(t, 500, got) + + // Out-of-int64-range values are clamped to MaxInt64 (defensive; gov + // validation only type-checks). + subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(math.MaxUint64)) + a.applyMigrationBatchSize(ctx) + got, _ = a.scStore.GetMigrationBatchSize() + require.Equal(t, math.MaxInt64, 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.scStore.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.scStore.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.scStore.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.scStore.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 d8818d3844..8b172236b8 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 + rs *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. + rs, ok := app.CommitMultiStore().(*rootmulti.Store) + if !ok { + panic(fmt.Sprintf("unsupported commit multistore %T: expected *storev2_rootmulti.Store", app.CommitMultiStore())) + } + app.rs = rs + 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..b32d965fc2 --- /dev/null +++ b/app/migration/params.go @@ -0,0 +1,45 @@ +// 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. +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 sets it) the SC store falls back to the +// node-local sc-keys-to-migrate-per-block config. +const DefaultNumKeysToMigratePerBlock uint64 = 0 + +// ParamKeyTable returns the key table for the migration subspace. +func ParamKeyTable() paramtypes.KeyTable { + return paramtypes.NewKeyTable( + paramtypes.NewParamSetPair(KeyNumKeysToMigratePerBlock, new(uint64), validateNumKeysToMigratePerBlock), + ) +} + +// validateNumKeysToMigratePerBlock only type-checks the value; any uint64 is a +// valid (consensus-deterministic) rate, with 0 meaning "paused". +func validateNumKeysToMigratePerBlock(i interface{}) error { + if _, ok := i.(uint64); !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + return nil +} diff --git a/app/migration/params_test.go b/app/migration/params_test.go new file mode 100644 index 0000000000..2b38732f2d --- /dev/null +++ b/app/migration/params_test.go @@ -0,0 +1,36 @@ +package migration + +import ( + "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 is valid, including 0 (paused) and large values. + for _, v := range []uint64{0, 1, 1024, 1 << 40} { + require.NoError(t, validateNumKeysToMigratePerBlock(v), "uint64 %d should be valid", 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 78eab0a06e..b7253b8fb3 100644 --- a/app/seidb.go +++ b/app/seidb.go @@ -2,7 +2,6 @@ package app import ( "fmt" - "github.com/spf13/cast" gigaconfig "github.com/sei-protocol/sei-chain/giga/executor/config" diff --git a/docker/localnode/config/app.toml b/docker/localnode/config/app.toml index 60f7237759..990715e713 100644 --- a/docker/localnode/config/app.toml +++ b/docker/localnode/config/app.toml @@ -236,12 +236,14 @@ 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 +# KeysToMigratePerBlock is the LOCAL FALLBACK for 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. It is only +# used when the governance-controlled NumKeysToMigratePerBlock param is unset +# (0); a positive param overrides it. Default 0 keeps the migration paused +# until governance raises the param. Migration rate is consensus-relevant, so +# do not set a non-zero value on a single node. +sc-keys-to-migrate-per-block = 0 [state-store] diff --git a/integration_test/gov_module/gov_proposal_test.yaml b/integration_test/gov_module/gov_proposal_test.yaml index 6acc2c7849..03c746a052 100644 --- a/integration_test/gov_module/gov_proposal_test.yaml +++ b/integration_test/gov_module/gov_proposal_test.yaml @@ -34,6 +34,41 @@ - type: eval expr: NEW_ABCI_PARAM == "true" +- 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 + - 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/storev2/rootmulti/store.go b/sei-cosmos/storev2/rootmulti/store.go index 25861a13a0..a2bc384b11 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -649,6 +649,31 @@ 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. Only a composite store mid- +// migration acts on it; every other SC store treats it as a no-op. +// +// Unlike SetWriteMode this does not swap the router or touch the cached +// views, so no view rebuild (and no pending-changes guard) is needed. The +// app calls it from BeginBlock, before the block's first write. +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) + } + return nil +} + +// 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 c1c78aa283..fa781cdd38 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -51,7 +51,12 @@ 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 is the local fallback for the number of keys to + // migrate from memiavl to flatkv per block. It is only used when the + // governance-controlled NumKeysToMigratePerBlock param is unset (0); + // otherwise the governance value wins. Defaults to 0, which (together + // with the param's 0 default) leaves the migration paused until + // governance raises the param. Ignored entirely outside migration modes. KeysToMigratePerBlock int `mapstructure:"keys-to-migrate-per-block"` } @@ -65,7 +70,7 @@ func DefaultStateCommitConfig() StateCommitConfig { HistoricalProofMaxInFlight: DefaultSCHistoricalProofMaxInFlight, HistoricalProofRateLimit: DefaultSCHistoricalProofRateLimit, HistoricalProofBurst: DefaultSCHistoricalProofBurst, - KeysToMigratePerBlock: 1024, + KeysToMigratePerBlock: 0, } } @@ -74,8 +79,8 @@ 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") + if c.KeysToMigratePerBlock < 0 { + return fmt.Errorf("keys-to-migrate-per-block must not be negative") } return nil } diff --git a/sei-db/config/toml.go b/sei-db/config/toml.go index 1c605432c9..3b3222c81a 100644 --- a/sei-db/config/toml.go +++ b/sei-db/config/toml.go @@ -63,12 +63,17 @@ sc-snapshot-write-rate-mbps = {{ .StateCommit.MemIAVLConfig.SnapshotWriteRateMBp # memiavl. Nodes that start in flatkv mode must keep flatkv_only forever. 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. +# KeysToMigratePerBlock is the LOCAL FALLBACK for 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. It is only +# consulted when the governance-controlled NumKeysToMigratePerBlock param is +# unset (0); whenever that param is positive it overrides this value. Default +# 0 keeps the migration paused until governance raises the param. Must be >= 0; +# ignored entirely when not in a migration mode. +# +# WARNING: this fallback is consensus-relevant — migration writes feed the +# AppHash. Do not set a non-zero value on a single node; either leave it at 0 +# and drive the rate via governance, or set the same value fleet-wide. sc-keys-to-migrate-per-block = {{ .StateCommit.KeysToMigratePerBlock }} ############################################################################### diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 41f96a9dfe..f5bb6e22fb 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 "unset": the effective rate + // then falls back to config.KeysToMigratePerBlock. See + // effectiveMigrationBatchSize. + // + // 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, cs.effectiveMigrationBatchSize()) if err != nil { cancel() return fmt.Errorf("failed to build router: %w", err) @@ -457,6 +470,42 @@ func (cs *CompositeCommitStore) buildRouter() error { return nil } +// effectiveMigrationBatchSize resolves the number of keys to migrate per +// block. The governance-controlled value (pushed via SetMigrationBatchSize) +// wins when set to a positive number; otherwise it falls back to the local +// config.KeysToMigratePerBlock. With both at their 0 default the migration +// is paused until governance raises the param. +func (cs *CompositeCommitStore) effectiveMigrationBatchSize() int { + if gov := cs.migrationBatchSize.Load(); gov > 0 { + return int(gov) + } + return cs.config.KeysToMigratePerBlock +} + +// SetMigrationBatchSize records the governance-controlled migration batch +// size and pushes the newly-resolved effective size 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. +// +// 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 { + cs.migrationBatchSize.Store(int64(batchSize)) + if cs.router != nil { + cs.router.SetMigrationBatchSize(cs.effectiveMigrationBatchSize()) + } + return nil +} + +// GetMigrationBatchSize returns the governance-controlled migration batch size +// most recently pushed via SetMigrationBatchSize (0 when never set / paused). +// This is the raw governance value before the config fallback applied by +// effectiveMigrationBatchSize, and is intended for observability and tests. +func (cs *CompositeCommitStore) GetMigrationBatchSize() int { + return int(cs.migrationBatchSize.Load()) +} + // 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/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..d803ff786f 100644 --- a/sei-db/state_db/sc/migration/migration_manager.go +++ b/sei-db/state_db/sc/migration/migration_manager.go @@ -98,8 +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). Only a negative size is rejected. + if migrationBatchSize < 0 { + return nil, fmt.Errorf("migration batch size must not be negative, got %d", migrationBatchSize) } if startVersion >= targetVersion { return nil, fmt.Errorf("startVersion (%d) must be strictly less than targetVersion (%d)", @@ -285,8 +289,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 +343,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 +387,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 +483,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 +502,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..14bba7b486 100644 --- a/sei-db/state_db/sc/migration/migration_manager_test.go +++ b/sei-db/state_db/sc/migration/migration_manager_test.go @@ -842,8 +842,8 @@ 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} +func TestNewMigrationManager_RejectsNegativeBatchSize(t *testing.T) { + cases := []int{-1, -100} for _, size := range cases { t.Run(fmt.Sprintf("size=%d", size), func(t *testing.T) { oldDB := newMockDB() @@ -856,11 +856,27 @@ func TestNewMigrationManager_RejectsNonPositiveBatchSize(t *testing.T) { iter, size, ) require.Error(t, err) - require.Contains(t, err.Error(), "batch size must be positive") + require.Contains(t, err.Error(), "batch size must not be negative") }) } } +// 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) + + 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) { iter := NewMockMigrationIterator(nil, false) oldDB := newMockDB() 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..5f69833ec3 100644 --- a/sei-db/state_db/sc/migration/router_builder.go +++ b/sei-db/state_db/sc/migration/router_builder.go @@ -158,8 +158,8 @@ 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") + if migrationBatchSize < 0 { + return nil, fmt.Errorf("migrationBatchSize must not be negative") } // Manages migration and routing for keys in the evm/ module. @@ -282,8 +282,8 @@ 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") + if migrationBatchSize < 0 { + return nil, fmt.Errorf("migrationBatchSize must not be negative") } allModulesButEvmAndBank, err := keys.AllModulesExcept(keys.EVMStoreKey, keys.BankStoreKey) @@ -411,8 +411,8 @@ 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") + if migrationBatchSize < 0 { + return nil, fmt.Errorf("migrationBatchSize must not be negative") } allButBankModules, err := keys.AllModulesExcept(keys.BankStoreKey) 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..0aefcc464e 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. The composite store resolves this governance-supplied value + // against its local fallback config before applying it. + // + // 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/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") From 1ab407ef38cbb58da7c97263b8cdf2badd512c1e Mon Sep 17 00:00:00 2001 From: YimingZang Date: Thu, 25 Jun 2026 19:54:26 -0700 Subject: [PATCH 02/19] Fix build issues --- app/abci.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/abci.go b/app/abci.go index 15a1ca0a72..13cc935b3a 100644 --- a/app/abci.go +++ b/app/abci.go @@ -68,7 +68,7 @@ func (app *App) BeginBlock( // AppHash. 0 (the default until a gov proposal raises it) leaves the migration // paused, falling back to the node-local sc-keys-to-migrate-per-block config. func (app *App) applyMigrationBatchSize(ctx sdk.Context) { - if app.scStore == nil { + if app.rs == nil { return } numKeys := migration.DefaultNumKeysToMigratePerBlock @@ -78,7 +78,7 @@ func (app *App) applyMigrationBatchSize(ctx sdk.Context) { if numKeys > uint64(math.MaxInt64) { numKeys = uint64(math.MaxInt64) } - if err := app.scStore.SetMigrationBatchSize(int(numKeys)); err != nil { + if err := app.rs.SetMigrationBatchSize(int(numKeys)); err != nil { logger.Error("failed to set SC migration batch size", "err", err) } } From 638806113324d59b224710db859ff57bc56ac794 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Thu, 25 Jun 2026 20:57:37 -0700 Subject: [PATCH 03/19] Remove config for num-keys-to-migrate --- AGENTS.md | 15 +- app/abci.go | 2 +- app/abci_test.go | 14 +- app/migration/params.go | 4 +- app/seidb.go | 21 +-- docker/localnode/config/app.toml | 9 -- .../contracts/verify_flatkv_evm_migrate.sh | 153 +++++++++++++++--- .../contracts/verify_flatkv_evm_store.sh | 2 +- .../contracts/verify_flatkv_only_statesync.sh | 2 +- .../storev2/rootmulti/flatkv_helpers_test.go | 15 +- .../rootmulti/flatkv_migration_test.go | 21 ++- .../storev2/rootmulti/set_write_mode_test.go | 9 +- sei-db/config/sc_config.go | 12 -- sei-db/config/toml.go | 13 -- sei-db/state_db/sc/composite/random_test.go | 9 +- .../composite/random_test_framework_test.go | 20 +++ sei-db/state_db/sc/composite/store.go | 31 ++-- .../state_db/sc/composite/store_auto_test.go | 27 ++-- .../sc/composite/store_migration_test.go | 4 +- sei-db/state_db/sc/composite/store_test.go | 46 ++---- 20 files changed, 245 insertions(+), 184 deletions(-) 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 13cc935b3a..bbefc0fd87 100644 --- a/app/abci.go +++ b/app/abci.go @@ -66,7 +66,7 @@ func (app *App) BeginBlock( // 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, falling back to the node-local sc-keys-to-migrate-per-block config. +// paused; it is the sole source of the rate (there is no node-local fallback). func (app *App) applyMigrationBatchSize(ctx sdk.Context) { if app.rs == nil { return diff --git a/app/abci_test.go b/app/abci_test.go index 1a5ab02f75..7777d43c11 100644 --- a/app/abci_test.go +++ b/app/abci_test.go @@ -39,21 +39,21 @@ func TestApplyMigrationBatchSize(t *testing.T) { // Unset param: the store receives the default (0 = paused). a.applyMigrationBatchSize(ctx) - got, ok := a.scStore.GetMigrationBatchSize() + got, ok := a.rs.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.scStore.GetMigrationBatchSize() + got, _ = a.rs.GetMigrationBatchSize() require.Equal(t, 500, got) // Out-of-int64-range values are clamped to MaxInt64 (defensive; gov // validation only type-checks). subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(math.MaxUint64)) a.applyMigrationBatchSize(ctx) - got, _ = a.scStore.GetMigrationBatchSize() + got, _ = a.rs.GetMigrationBatchSize() require.Equal(t, math.MaxInt64, got) } @@ -66,7 +66,7 @@ func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { 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.scStore.GetMigrationBatchSize() + before, ok := a.rs.GetMigrationBatchSize() require.True(t, ok) require.Equal(t, 0, before) @@ -80,7 +80,7 @@ func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { a.BeginBlock(ctx, 2, nil, nil, false) }) - after, _ := a.scStore.GetMigrationBatchSize() + after, _ := a.rs.GetMigrationBatchSize() require.Equal(t, 321, after, "BeginBlock should push the gov param into the SC store") } @@ -109,7 +109,7 @@ func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { // 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.scStore.GetMigrationBatchSize() + got, ok := a.rs.GetMigrationBatchSize() require.True(t, ok) require.Equal(t, 0, got, "param committed in block 1 must not take effect within block 1") @@ -119,6 +119,6 @@ func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { }) require.NoError(t, err) - got, _ = a.scStore.GetMigrationBatchSize() + got, _ = a.rs.GetMigrationBatchSize() require.Equal(t, 640, got, "migration rate must take effect on the block after the param is committed") } diff --git a/app/migration/params.go b/app/migration/params.go index b32d965fc2..6d57028dc8 100644 --- a/app/migration/params.go +++ b/app/migration/params.go @@ -24,8 +24,8 @@ const SubspaceName = "migration" var KeyNumKeysToMigratePerBlock = []byte("NumKeysToMigratePerBlock") // DefaultNumKeysToMigratePerBlock leaves the migration paused. While it is 0 -// (the default until a gov proposal sets it) the SC store falls back to the -// node-local sc-keys-to-migrate-per-block config. +// (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 // ParamKeyTable returns the key table for the migration subspace. diff --git a/app/seidb.go b/app/seidb.go index b7253b8fb3..0cf6a94a20 100644 --- a/app/seidb.go +++ b/app/seidb.go @@ -2,6 +2,7 @@ package app import ( "fmt" + "github.com/spf13/cast" gigaconfig "github.com/sei-protocol/sei-chain/giga/executor/config" @@ -28,15 +29,7 @@ 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" + FlagSCFlatKVReadWriteMetrics = "state-commit.flatkv.enable-read-write-metrics" // SS Store configs FlagSSEnable = "state-store.ss-enable" @@ -130,16 +123,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 - } - } return scConfig } diff --git a/docker/localnode/config/app.toml b/docker/localnode/config/app.toml index 990715e713..89ed5e9b22 100644 --- a/docker/localnode/config/app.toml +++ b/docker/localnode/config/app.toml @@ -236,15 +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 is the LOCAL FALLBACK for 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. It is only -# used when the governance-controlled NumKeysToMigratePerBlock param is unset -# (0); a positive param overrides it. Default 0 keeps the migration paused -# until governance raises the param. Migration rate is consensus-relevant, so -# do not set a non-zero value on a single node. -sc-keys-to-migrate-per-block = 0 - [state-store] # Enable defines if the state-store should be enabled for historical queries. 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/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..7cc7a4281b 100644 --- a/sei-cosmos/storev2/rootmulti/set_write_mode_test.go +++ b/sei-cosmos/storev2/rootmulti/set_write_mode_test.go @@ -8,10 +8,9 @@ import ( "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,7 +23,8 @@ 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()) + require.NoError(t, store.SetMigrationBatchSize(100)) defer func() { require.NoError(t, store.Close()) }() // Deposit EVM state while effectively MemiavlOnly. @@ -67,7 +67,8 @@ func TestRootMultiSetWriteMode_StaleViewsRouteCorrectly(t *testing.T) { // must refuse to run. func TestRootMultiSetWriteMode_RejectsPendingChanges(t *testing.T) { dir := t.TempDir() - store, storeKeys := newTestRootMulti(t, dir, autoModeConfig(100)) + store, storeKeys := newTestRootMulti(t, dir, autoModeConfig()) + require.NoError(t, store.SetMigrationBatchSize(100)) defer func() { require.NoError(t, store.Close()) }() addr := newEVMTestData(0xD2) diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index fa781cdd38..2ef0e3f4eb 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -50,14 +50,6 @@ type StateCommitConfig struct { // Token bucket burst for historical proof queries. HistoricalProofBurst int `mapstructure:"historical-proof-burst"` - - // KeysToMigratePerBlock is the local fallback for the number of keys to - // migrate from memiavl to flatkv per block. It is only used when the - // governance-controlled NumKeysToMigratePerBlock param is unset (0); - // otherwise the governance value wins. Defaults to 0, which (together - // with the param's 0 default) leaves the migration paused until - // governance raises the param. Ignored entirely outside migration modes. - KeysToMigratePerBlock int `mapstructure:"keys-to-migrate-per-block"` } // DefaultStateCommitConfig returns the default StateCommitConfig @@ -70,7 +62,6 @@ func DefaultStateCommitConfig() StateCommitConfig { HistoricalProofMaxInFlight: DefaultSCHistoricalProofMaxInFlight, HistoricalProofRateLimit: DefaultSCHistoricalProofRateLimit, HistoricalProofBurst: DefaultSCHistoricalProofBurst, - KeysToMigratePerBlock: 0, } } @@ -79,8 +70,5 @@ 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 not be negative") - } return nil } diff --git a/sei-db/config/toml.go b/sei-db/config/toml.go index 3b3222c81a..f8b1cde591 100644 --- a/sei-db/config/toml.go +++ b/sei-db/config/toml.go @@ -63,19 +63,6 @@ sc-snapshot-write-rate-mbps = {{ .StateCommit.MemIAVLConfig.SnapshotWriteRateMBp # memiavl. Nodes that start in flatkv mode must keep flatkv_only forever. sc-write-mode = "{{ .StateCommit.WriteMode }}" -# KeysToMigratePerBlock is the LOCAL FALLBACK for 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. It is only -# consulted when the governance-controlled NumKeysToMigratePerBlock param is -# unset (0); whenever that param is positive it overrides this value. Default -# 0 keeps the migration paused until governance raises the param. Must be >= 0; -# ignored entirely when not in a migration mode. -# -# WARNING: this fallback is consensus-relevant — migration writes feed the -# AppHash. Do not set a non-zero value on a single node; either leave it at 0 -# and drive the rate via governance, or set the same value fleet-wide. -sc-keys-to-migrate-per-block = {{ .StateCommit.KeysToMigratePerBlock }} - ############################################################################### ### FlatKV (EVM) Configuration ### ############################################################################### 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 f5bb6e22fb..7acfa49bbb 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -96,9 +96,9 @@ type CompositeCommitStore struct { // 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 "unset": the effective rate - // then falls back to config.KeysToMigratePerBlock. See - // effectiveMigrationBatchSize. + // 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 @@ -457,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.effectiveMigrationBatchSize()) + routerCtx, cs.currentWriteMode, cs.memIAVL, cs.flatKV, int(cs.migrationBatchSize.Load())) if err != nil { cancel() return fmt.Errorf("failed to build router: %w", err) @@ -470,22 +470,10 @@ func (cs *CompositeCommitStore) buildRouter() error { return nil } -// effectiveMigrationBatchSize resolves the number of keys to migrate per -// block. The governance-controlled value (pushed via SetMigrationBatchSize) -// wins when set to a positive number; otherwise it falls back to the local -// config.KeysToMigratePerBlock. With both at their 0 default the migration -// is paused until governance raises the param. -func (cs *CompositeCommitStore) effectiveMigrationBatchSize() int { - if gov := cs.migrationBatchSize.Load(); gov > 0 { - return int(gov) - } - return cs.config.KeysToMigratePerBlock -} - // SetMigrationBatchSize records the governance-controlled migration batch -// size and pushes the newly-resolved effective size 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. +// 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. // // Must be called between blocks (the app calls it from BeginBlock, before any // ApplyChangeSets). The router's threadSafeRouter wrapper serializes the push @@ -493,15 +481,14 @@ func (cs *CompositeCommitStore) effectiveMigrationBatchSize() int { func (cs *CompositeCommitStore) SetMigrationBatchSize(batchSize int) error { cs.migrationBatchSize.Store(int64(batchSize)) if cs.router != nil { - cs.router.SetMigrationBatchSize(cs.effectiveMigrationBatchSize()) + cs.router.SetMigrationBatchSize(batchSize) } return nil } // GetMigrationBatchSize returns the governance-controlled migration batch size // most recently pushed via SetMigrationBatchSize (0 when never set / paused). -// This is the raw governance value before the config fallback applied by -// effectiveMigrationBatchSize, and is intended for observability and tests. +// It is intended for observability and tests. func (cs *CompositeCommitStore) GetMigrationBatchSize() int { return int(cs.migrationBatchSize.Load()) } 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..a7b6b9f57a 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,8 +29,9 @@ 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) @@ -225,7 +225,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 +276,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 +318,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 +354,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 +373,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 +557,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 +606,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 20f1193ac2..16f838e199 100644 --- a/sei-db/state_db/sc/composite/store_test.go +++ b/sei-db/state_db/sc/composite/store_test.go @@ -445,10 +445,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) @@ -490,10 +489,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) @@ -527,10 +525,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) @@ -576,10 +573,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) @@ -769,7 +765,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 @@ -778,6 +773,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) @@ -814,10 +810,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) @@ -926,10 +921,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) @@ -959,10 +953,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") @@ -1182,10 +1175,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) @@ -1221,10 +1213,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) @@ -1919,10 +1910,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) @@ -1990,11 +1980,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) @@ -2056,10 +2046,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) @@ -2127,10 +2116,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) @@ -2164,10 +2152,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) @@ -2579,10 +2566,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) From 358df23b525575a115da26a411326cb5edb5c500 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Thu, 25 Jun 2026 22:38:08 -0700 Subject: [PATCH 04/19] Fix gov proposal --- app/abci.go | 8 ++++++++ sei-db/state_db/sc/types/types.go | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/abci.go b/app/abci.go index bbefc0fd87..1a75183d2d 100644 --- a/app/abci.go +++ b/app/abci.go @@ -73,6 +73,14 @@ func (app *App) applyMigrationBatchSize(ctx sdk.Context) { } 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) } if numKeys > uint64(math.MaxInt64) { diff --git a/sei-db/state_db/sc/types/types.go b/sei-db/state_db/sc/types/types.go index 0aefcc464e..e789c8c360 100644 --- a/sei-db/state_db/sc/types/types.go +++ b/sei-db/state_db/sc/types/types.go @@ -159,8 +159,8 @@ type Committer interface { // // A value of 0 pauses the migration: caller writes still route // normally, but no keys are pulled forward until the size is raised - // again. The composite store resolves this governance-supplied value - // against its local fallback config before applying it. + // 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. From 91e17668bcf4d307ddff8242fa792fdb7c17a7ce Mon Sep 17 00:00:00 2001 From: YimingZang Date: Fri, 26 Jun 2026 10:20:21 -0700 Subject: [PATCH 05/19] Address comment --- app/abci.go | 8 ++--- app/abci_test.go | 32 +++++++++---------- app/app.go | 12 ++----- app/receipt_store_config.go | 2 +- app/test_helpers.go | 2 +- sei-cosmos/baseapp/baseapp.go | 29 +++++++++++------ .../rootmulti/flatkv_migration_test.go | 2 +- sei-cosmos/storev2/rootmulti/store.go | 6 ++-- sei-db/state_db/sc/composite/random_test.go | 2 +- .../composite/random_test_framework_test.go | 2 +- sei-db/state_db/sc/composite/store.go | 12 +++---- .../state_db/sc/composite/store_auto_test.go | 4 +-- .../sc/composite/store_migration_test.go | 4 +-- sei-db/state_db/sc/memiavl/store.go | 2 +- .../sc/migration/dual_write_router.go | 2 +- .../sc/migration/migration_manager.go | 13 +++----- .../sc/migration/migration_manager_test.go | 21 +----------- .../migration_test_framework_test.go | 8 ++--- .../state_db/sc/migration/migration_types.go | 2 +- sei-db/state_db/sc/migration/module_router.go | 2 +- .../sc/migration/passthrough_router.go | 2 +- .../state_db/sc/migration/router_builder.go | 20 +++--------- .../sc/migration/router_kvstore_test.go | 2 +- .../sc/migration/thread_safe_router.go | 2 +- .../sc/migration/thread_safe_router_test.go | 2 +- sei-db/state_db/sc/types/types.go | 2 +- x/evm/keeper/trace_snapshot_test.go | 2 +- 27 files changed, 82 insertions(+), 117 deletions(-) diff --git a/app/abci.go b/app/abci.go index 1a75183d2d..0a4d55d8bb 100644 --- a/app/abci.go +++ b/app/abci.go @@ -4,7 +4,6 @@ import ( "context" "crypto/sha256" "fmt" - "math" "math/big" "time" @@ -68,7 +67,7 @@ func (app *App) BeginBlock( // 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.rs == nil { + if app.rootStore == nil { return } numKeys := migration.DefaultNumKeysToMigratePerBlock @@ -83,10 +82,7 @@ func (app *App) applyMigrationBatchSize(ctx sdk.Context) { } subspace.GetIfExists(ctx, migration.KeyNumKeysToMigratePerBlock, &numKeys) } - if numKeys > uint64(math.MaxInt64) { - numKeys = uint64(math.MaxInt64) - } - if err := app.rs.SetMigrationBatchSize(int(numKeys)); err != nil { + if err := app.rootStore.SetMigrationBatchSize(numKeys); err != nil { logger.Error("failed to set SC migration batch size", "err", err) } } diff --git a/app/abci_test.go b/app/abci_test.go index 7777d43c11..426bf1eaea 100644 --- a/app/abci_test.go +++ b/app/abci_test.go @@ -39,22 +39,22 @@ func TestApplyMigrationBatchSize(t *testing.T) { // Unset param: the store receives the default (0 = paused). a.applyMigrationBatchSize(ctx) - got, ok := a.rs.GetMigrationBatchSize() + got, ok := a.rootStore.GetMigrationBatchSize() require.True(t, ok, "SC store should track a migration batch size") - require.Equal(t, 0, got) + require.Equal(t, uint64(0), got) // Governance raises the rate: BeginBlock forwards the new value. subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(500)) a.applyMigrationBatchSize(ctx) - got, _ = a.rs.GetMigrationBatchSize() - require.Equal(t, 500, got) + got, _ = a.rootStore.GetMigrationBatchSize() + require.Equal(t, uint64(500), got) - // Out-of-int64-range values are clamped to MaxInt64 (defensive; gov - // validation only type-checks). + // The whole batch-size path is uint64 end-to-end, so the full param + // range is forwarded verbatim with no conversion or clamping. subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(math.MaxUint64)) a.applyMigrationBatchSize(ctx) - got, _ = a.rs.GetMigrationBatchSize() - require.Equal(t, math.MaxInt64, got) + got, _ = a.rootStore.GetMigrationBatchSize() + require.Equal(t, uint64(math.MaxUint64), got) } // TestBeginBlockAppliesMigrationBatchSize exercises the full BeginBlock path @@ -66,9 +66,9 @@ func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { 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.rs.GetMigrationBatchSize() + before, ok := a.rootStore.GetMigrationBatchSize() require.True(t, ok) - require.Equal(t, 0, before) + require.Equal(t, uint64(0), before) // Simulate the gov proposal landing in chain state. subspace, ok := a.ParamsKeeper.GetSubspace(migration.SubspaceName) @@ -80,8 +80,8 @@ func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { a.BeginBlock(ctx, 2, nil, nil, false) }) - after, _ := a.rs.GetMigrationBatchSize() - require.Equal(t, 321, after, "BeginBlock should push the gov param into the SC store") + after, _ := a.rootStore.GetMigrationBatchSize() + require.Equal(t, uint64(321), after, "BeginBlock should push the gov param into the SC store") } // TestMigrationBatchSizeTakesEffectNextBlock is the full end-to-end timing @@ -109,9 +109,9 @@ func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { // 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.rs.GetMigrationBatchSize() + 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") + require.Equal(t, uint64(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{ @@ -119,6 +119,6 @@ func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { }) require.NoError(t, err) - got, _ = a.rs.GetMigrationBatchSize() - require.Equal(t, 640, got, "migration rate must take effect on the block after the param is committed") + got, _ = a.rootStore.GetMigrationBatchSize() + require.Equal(t, uint64(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 8b172236b8..977b7afcb9 100644 --- a/app/app.go +++ b/app/app.go @@ -470,7 +470,7 @@ type App struct { genesisImportConfig genesistypes.GenesisImportConfig stateStore seidb.StateStore - rs *rootmulti.Store + rootStore *rootmulti.Store receiptStore receipt.ReceiptStore forkInitializer func(sdk.Context) @@ -551,15 +551,7 @@ 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. - rs, ok := app.CommitMultiStore().(*rootmulti.Store) - if !ok { - panic(fmt.Sprintf("unsupported commit multistore %T: expected *storev2_rootmulti.Store", app.CommitMultiStore())) - } - app.rs = rs + app.rootStore = app.V2RootMultiStore() app.ParamsKeeper = initParamsKeeper(appCodec, cdc, keys[paramstypes.StoreKey], tkeys[paramstypes.TStoreKey]) diff --git a/app/receipt_store_config.go b/app/receipt_store_config.go index afa07f854e..a6fffaeeb4 100644 --- a/app/receipt_store_config.go +++ b/app/receipt_store_config.go @@ -9,7 +9,7 @@ import ( ) const ( - receiptStoreBackendKey = "receipt-store.rs-backend" + receiptStoreBackendKey = "receipt-store.rootStore-backend" receiptStoreDBDirectoryKey = "receipt-store.db-directory" receiptStoreAsyncWriteBufferKey = "receipt-store.async-write-buffer" receiptStorePruneIntervalSecondsKey = "receipt-store.prune-interval-seconds" diff --git a/app/test_helpers.go b/app/test_helpers.go index 9a509d2149..93be422b92 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -483,7 +483,7 @@ func SetupWithDB(tb testing.TB, db dbm.DB, isCheckTx bool, enableEVMCustomPrecom // SetupWithScReceiptFromOpts is like SetupWithSc but does not inject a receipt store via AppOption. // The receipt store is created inside New() from testAppOpts. -// Use this to test the full app path with rs-backend from config. +// Use this to test the full app path with rootStore-backend from config. func SetupWithScReceiptFromOpts(t *testing.T, isCheckTx bool, enableEVMCustomPrecompiles bool, testAppOpts TestAppOpts, baseAppOptions ...func(*bam.BaseApp)) (res *App) { db := dbm.NewMemDB() encodingConfig := MakeEncodingConfig() diff --git a/sei-cosmos/baseapp/baseapp.go b/sei-cosmos/baseapp/baseapp.go index 25cec9bbae..673270e212 100644 --- a/sei-cosmos/baseapp/baseapp.go +++ b/sei-cosmos/baseapp/baseapp.go @@ -11,12 +11,23 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/gogo/protobuf/proto" "github.com/holiman/uint256" + "github.com/sei-protocol/seilog" + "github.com/spf13/cast" + leveldbutils "github.com/syndtr/goleveldb/leveldb/util" + dbm "github.com/tendermint/tm-db" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" cryptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types" "github.com/sei-protocol/sei-chain/sei-cosmos/server/config" servertypes "github.com/sei-protocol/sei-chain/sei-cosmos/server/types" "github.com/sei-protocol/sei-chain/sei-cosmos/snapshots" "github.com/sei-protocol/sei-chain/sei-cosmos/store" + "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" "github.com/sei-protocol/sei-chain/sei-cosmos/telemetry" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" @@ -26,15 +37,6 @@ import ( tmcfg "github.com/sei-protocol/sei-chain/sei-tendermint/config" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" sdbm "github.com/sei-protocol/sei-tm-db/backends" - "github.com/sei-protocol/seilog" - "github.com/spf13/cast" - leveldbutils "github.com/syndtr/goleveldb/leveldb/util" - dbm "github.com/tendermint/tm-db" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - otelmetric "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" ) const ( @@ -441,6 +443,15 @@ func (app *BaseApp) CommitMultiStore() sdk.CommitMultiStore { return app.cms } +// V2RootMultiStore The v2 rootmulti store is the only supported commit multistore; +// Fail fast if the legacy root multistore is somehow in use. +func (app *BaseApp) V2RootMultiStore() *rootmulti.Store { + if cms, ok := app.cms.(*rootmulti.Store); ok { + return cms + } + panic(fmt.Sprintf("unsupported commit multistore %T: expected storeV2 CMS", app.CommitMultiStore())) +} + // SnapshotManager returns the snapshot manager. // application use this to register extra extension snapshotters. func (app *BaseApp) SnapshotManager() *snapshots.Manager { diff --git a/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go b/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go index e9706a7a7d..5327299391 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go @@ -86,7 +86,7 @@ func driveRootMultiMigration( dir string, phase1Blocks, phase2Blocks int, phase1StorageKeysPerBlock int, - migrateBatchSize int, + migrateBatchSize uint64, ) (*Store, map[string]*types.KVStoreKey, []commitRecord) { t.Helper() diff --git a/sei-cosmos/storev2/rootmulti/store.go b/sei-cosmos/storev2/rootmulti/store.go index a2bc384b11..c55785d60d 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -656,7 +656,7 @@ func (rs *Store) SetInitialVersion(version int64) error { // Unlike SetWriteMode this does not swap the router or touch the cached // views, so no view rebuild (and no pending-changes guard) is needed. The // app calls it from BeginBlock, before the block's first write. -func (rs *Store) SetMigrationBatchSize(batchSize int) error { +func (rs *Store) SetMigrationBatchSize(batchSize uint64) error { if err := rs.scStore.SetMigrationBatchSize(batchSize); err != nil { return fmt.Errorf("failed to set SC store migration batch size: %w", err) } @@ -667,8 +667,8 @@ func (rs *Store) SetMigrationBatchSize(batchSize int) error { // 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 { +func (rs *Store) GetMigrationBatchSize() (uint64, bool) { + if g, ok := rs.scStore.(interface{ GetMigrationBatchSize() uint64 }); ok { return g.GetMigrationBatchSize(), true } return 0, false diff --git a/sei-db/state_db/sc/composite/random_test.go b/sei-db/state_db/sc/composite/random_test.go index 24ba2edf97..f5500f90c8 100644 --- a/sei-db/state_db/sc/composite/random_test.go +++ b/sei-db/state_db/sc/composite/random_test.go @@ -263,7 +263,7 @@ func runMigrationScenario(t *testing.T, sc migrationScenario) { // 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 + testMigrationBatchSize = uint64(migBatch) defer func() { testMigrationBatchSize = 0 }() t.Logf("migration scenario %s->%s keysToMigratePerBlock=%d", sc.migrationMode, sc.successorMode, migBatch) 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 302d3495b9..bcbe807128 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 @@ -1490,7 +1490,7 @@ func randomTestConfig(t *testing.T, rng *testutil.TestRandom, mode types.WriteMo // 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 +var testMigrationBatchSize uint64 // applyTestMigrationBatchSize re-applies testMigrationBatchSize to a freshly // opened store, mimicking the app's BeginBlock push of the gov param. diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 7acfa49bbb..94860830cd 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -104,7 +104,7 @@ type CompositeCommitStore struct { // 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 + migrationBatchSize atomic.Uint64 // migrationAdvancedThisCommit gates per-block migration progress // against rootmulti.Store's double-flush pattern. rootmulti calls @@ -457,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, int(cs.migrationBatchSize.Load())) + routerCtx, cs.currentWriteMode, cs.memIAVL, cs.flatKV, cs.migrationBatchSize.Load()) if err != nil { cancel() return fmt.Errorf("failed to build router: %w", err) @@ -478,8 +478,8 @@ func (cs *CompositeCommitStore) buildRouter() error { // 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 { - cs.migrationBatchSize.Store(int64(batchSize)) +func (cs *CompositeCommitStore) SetMigrationBatchSize(batchSize uint64) error { + cs.migrationBatchSize.Store(batchSize) if cs.router != nil { cs.router.SetMigrationBatchSize(batchSize) } @@ -489,8 +489,8 @@ func (cs *CompositeCommitStore) SetMigrationBatchSize(batchSize int) error { // 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()) +func (cs *CompositeCommitStore) GetMigrationBatchSize() uint64 { + return cs.migrationBatchSize.Load() } // SetWriteMode transitions the effective write mode at runtime. Only legal 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 a7b6b9f57a..0482a921bc 100644 --- a/sei-db/state_db/sc/composite/store_auto_test.go +++ b/sei-db/state_db/sc/composite/store_auto_test.go @@ -27,7 +27,7 @@ func autoConfig() config.StateCommitConfig { } // openAutoStore opens (or reopens) a composite store at dir in Auto mode. -func openAutoStore(t *testing.T, dir string, batch int) *CompositeCommitStore { +func openAutoStore(t *testing.T, dir string, batch uint64) *CompositeCommitStore { t.Helper() cs, err := NewCompositeCommitStore(t.Context(), dir, autoConfig()) require.NoError(t, err) @@ -284,7 +284,7 @@ func autoExportConfig() config.StateCommitConfig { } // openAutoStoreWithConfig mirrors openAutoStore for a caller-supplied config. -func openAutoStoreWithConfig(t *testing.T, dir string, cfg config.StateCommitConfig, batch int) *CompositeCommitStore { +func openAutoStoreWithConfig(t *testing.T, dir string, cfg config.StateCommitConfig, batch uint64) *CompositeCommitStore { t.Helper() cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) require.NoError(t, err) 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 2b5399c679..62230110dc 100644 --- a/sei-db/state_db/sc/composite/store_migration_test.go +++ b/sei-db/state_db/sc/composite/store_migration_test.go @@ -227,7 +227,7 @@ func driveMigrationWorkload( dir string, workload *migrationWorkload, phase1Blocks, phase2Blocks int, - keysToMigratePerBlock int, + keysToMigratePerBlock uint64, ) { t.Helper() @@ -284,7 +284,7 @@ func driveMigrationWorkload( // reopenInMigrateEVM is a small helper for the resume / migration paths // that need to peek at on-disk state from a MigrateEVM mode reopen. -func reopenInMigrateEVM(t *testing.T, dir string, batch int) *CompositeCommitStore { +func reopenInMigrateEVM(t *testing.T, dir string, batch uint64) *CompositeCommitStore { t.Helper() cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM diff --git a/sei-db/state_db/sc/memiavl/store.go b/sei-db/state_db/sc/memiavl/store.go index 290e6bdb7f..fb356bdb2d 100644 --- a/sei-db/state_db/sc/memiavl/store.go +++ b/sei-db/state_db/sc/memiavl/store.go @@ -105,7 +105,7 @@ func (cs *CommitStore) SetWriteMode(types.WriteMode) error { // 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 { +func (cs *CommitStore) SetMigrationBatchSize(uint64) error { return 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 32dc7ec82d..8c4e324f87 100644 --- a/sei-db/state_db/sc/migration/dual_write_router.go +++ b/sei-db/state_db/sc/migration/dual_write_router.go @@ -75,7 +75,7 @@ func (t *TestOnlyDualWriteRouter) Read(store string, key []byte) ([]byte, bool, // SetMigrationBatchSize is a no-op: the dual-write router duplicates // traffic and performs no boundary-advancing data migration. -func (t *TestOnlyDualWriteRouter) SetMigrationBatchSize(int) {} +func (t *TestOnlyDualWriteRouter) SetMigrationBatchSize(uint64) {} // BuildRoute returns a Route that dispatches the given module names to // this DualWriteRouter. Reads, writes and proof requests for those diff --git a/sei-db/state_db/sc/migration/migration_manager.go b/sei-db/state_db/sc/migration/migration_manager.go index d803ff786f..4a9cafff72 100644 --- a/sei-db/state_db/sc/migration/migration_manager.go +++ b/sei-db/state_db/sc/migration/migration_manager.go @@ -46,7 +46,7 @@ type MigrationManager struct { boundary MigrationBoundary // The number of key-value pairs to migrate after each write operation. - migrationBatchSize int + migrationBatchSize uint64 // The version we want to migrate to. targetVersion uint64 @@ -61,7 +61,7 @@ type MigrationManager struct { // Handles the migration of data from one database to another. func NewMigrationManager( // The number of key-value pairs to migrate after each write operation. Must be > 0. - migrationBatchSize int, + migrationBatchSize uint64, // The migration version the stored data is expected to be at on entry. If no prior migration // version is stored in the DB, startVersion should be 0. startVersion uint64, @@ -101,10 +101,7 @@ func NewMigrationManager( // 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). Only a negative size is rejected. - if migrationBatchSize < 0 { - return nil, fmt.Errorf("migration batch size must not be negative, got %d", migrationBatchSize) - } + // acts as the migration trigger). if startVersion >= targetVersion { return nil, fmt.Errorf("startVersion (%d) must be strictly less than targetVersion (%d)", startVersion, targetVersion) @@ -299,7 +296,7 @@ func (m *MigrationManager) ApplyChangeSets(changesets []*proto.NamedChangeSet, f batchStats := migrationBatchStats{} if advanceMigration { // Get the next batch of keys to migrate. - valuesToMigrate, newBoundary, err := m.iterator.NextBatch(m.migrationBatchSize) + valuesToMigrate, newBoundary, err := m.iterator.NextBatch(int(m.migrationBatchSize)) if err != nil { return fmt.Errorf("failed to get next batch: %w", err) } @@ -490,7 +487,7 @@ func (m *MigrationManager) GetProof(store string, key []byte) (*ics23.Commitment // 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) { +func (m *MigrationManager) SetMigrationBatchSize(batchSize uint64) { m.migrationBatchSize = batchSize } 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 14bba7b486..8edb6d8dae 100644 --- a/sei-db/state_db/sc/migration/migration_manager_test.go +++ b/sei-db/state_db/sc/migration/migration_manager_test.go @@ -101,7 +101,7 @@ func newTestManager( oldReader DBReader, oldWriter DBWriter, newReader DBReader, newWriter DBWriter, iter MigrationIterator, - size int, + size uint64, ) (*MigrationManager, error) { t.Helper() return NewMigrationManager( @@ -842,25 +842,6 @@ func fuzzApplyToReference(ref map[string]map[string][]byte, changesets []*proto. // --- Constructor validation (Issue 2, Issue 11) --- -func TestNewMigrationManager_RejectsNegativeBatchSize(t *testing.T) { - cases := []int{-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) - - _, 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 not be negative") - }) - } -} - // 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) { 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 34a938ff56..aa9d0267c9 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,7 +53,7 @@ func (m *TestMultiDB) GetProof(store string, key []byte) (*ics23.CommitmentProof panic("not implemented") } -func (m *TestMultiDB) SetMigrationBatchSize(batchSize int) { +func (m *TestMultiDB) SetMigrationBatchSize(batchSize uint64) { for _, nestedDB := range m.nestedDBs { nestedDB.SetMigrationBatchSize(batchSize) } @@ -109,7 +109,7 @@ func (r *TestFlatKVRouter) GetProof(store string, key []byte) (*ics23.Commitment return nil, errors.New("TestFlatKVRouter does not support proofs") } -func (r *TestFlatKVRouter) SetMigrationBatchSize(int) {} +func (r *TestFlatKVRouter) SetMigrationBatchSize(uint64) {} // TestMemIAVLRouter is a [Router] that sends all operations to a single underlying // memiavl.CommitStore. It does not support iteration or proofs. @@ -140,7 +140,7 @@ func (r *TestMemIAVLRouter) GetProof(store string, key []byte) (*ics23.Commitmen return nil, errors.New("TestMemIAVLRouter does not support proofs") } -func (r *TestMemIAVLRouter) SetMigrationBatchSize(int) {} +func (r *TestMemIAVLRouter) SetMigrationBatchSize(uint64) {} // 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 @@ -195,7 +195,7 @@ func (r *TestInMemoryRouter) GetProof(store string, key []byte) (*ics23.Commitme return nil, errors.New("TestInMemoryRouter does not support proofs") } -func (r *TestInMemoryRouter) SetMigrationBatchSize(int) {} +func (r *TestInMemoryRouter) SetMigrationBatchSize(uint64) {} // 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 diff --git a/sei-db/state_db/sc/migration/migration_types.go b/sei-db/state_db/sc/migration/migration_types.go index 244c67640f..96f73febed 100644 --- a/sei-db/state_db/sc/migration/migration_types.go +++ b/sei-db/state_db/sc/migration/migration_types.go @@ -85,5 +85,5 @@ type Router interface { // Must be called between blocks, on the same goroutine that drives // ApplyChangeSets. threadSafeRouter additionally serializes it against // concurrent reads. - SetMigrationBatchSize(batchSize int) + SetMigrationBatchSize(batchSize uint64) } diff --git a/sei-db/state_db/sc/migration/module_router.go b/sei-db/state_db/sc/migration/module_router.go index fcaf1e6c33..dc3c5289ee 100644 --- a/sei-db/state_db/sc/migration/module_router.go +++ b/sei-db/state_db/sc/migration/module_router.go @@ -180,7 +180,7 @@ func (m *ModuleRouter) GetProof(store string, key []byte) (*ics23.CommitmentProo // 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) { +func (m *ModuleRouter) SetMigrationBatchSize(batchSize uint64) { signalled := make(map[Router]struct{}, len(m.routes)) for _, r := range m.routes { if r.owner == nil { diff --git a/sei-db/state_db/sc/migration/passthrough_router.go b/sei-db/state_db/sc/migration/passthrough_router.go index 565d3cf348..47a78879a0 100644 --- a/sei-db/state_db/sc/migration/passthrough_router.go +++ b/sei-db/state_db/sc/migration/passthrough_router.go @@ -71,4 +71,4 @@ func (p *PassthroughRouter) GetProof(store string, key []byte) (*ics23.Commitmen // SetMigrationBatchSize is a no-op: a passthrough router targets a single // backend and performs no data migration. -func (p *PassthroughRouter) SetMigrationBatchSize(int) {} +func (p *PassthroughRouter) SetMigrationBatchSize(uint64) {} diff --git a/sei-db/state_db/sc/migration/router_builder.go b/sei-db/state_db/sc/migration/router_builder.go index 5f69833ec3..f63e2e2550 100644 --- a/sei-db/state_db/sc/migration/router_builder.go +++ b/sei-db/state_db/sc/migration/router_builder.go @@ -21,7 +21,7 @@ func BuildRouter( memIAVL *memiavl.CommitStore, flatKV flatkv.Store, // If this router will be doing data migration, this is the number of keys to migrate in each batch. - migrationBatchSize int, + migrationBatchSize uint64, ) (Router, error) { switch writeMode { @@ -149,7 +149,7 @@ func buildMigrateEVMRouter( ctx context.Context, memIAVL *memiavl.CommitStore, flatKV flatkv.Store, - migrationBatchSize int, + migrationBatchSize uint64, ) (Router, error) { if memIAVL == nil { @@ -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 not be negative") - } - // Manages migration and routing for keys in the evm/ module. migrationManager, err := NewMigrationManager( migrationBatchSize, @@ -273,7 +269,7 @@ func buildMigrateAllButBankRouter( ctx context.Context, memIAVL *memiavl.CommitStore, flatKV flatkv.Store, - migrationBatchSize int, + migrationBatchSize uint64, ) (Router, error) { if memIAVL == nil { @@ -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 not be negative") - } - allModulesButEvmAndBank, err := keys.AllModulesExcept(keys.EVMStoreKey, keys.BankStoreKey) if err != nil { return nil, fmt.Errorf("AllModulesExcept: %w", err) @@ -402,7 +394,7 @@ func buildMigrateBankRouter( ctx context.Context, memIAVL *memiavl.CommitStore, flatKV flatkv.Store, - migrationBatchSize int, + migrationBatchSize uint64, ) (Router, error) { if memIAVL == nil { @@ -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 not be negative") - } - 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 c0979b1d17..741e1368e7 100644 --- a/sei-db/state_db/sc/migration/router_kvstore_test.go +++ b/sei-db/state_db/sc/migration/router_kvstore_test.go @@ -215,7 +215,7 @@ 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 (f *failingRouter) SetMigrationBatchSize(uint64) {} func TestRouterCommitKVStore_GetPanicsOnRouterError(t *testing.T) { store := NewRouterCommitKVStore( 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 ebf6cacd69..90e52b26cb 100644 --- a/sei-db/state_db/sc/migration/thread_safe_router.go +++ b/sei-db/state_db/sc/migration/thread_safe_router.go @@ -60,7 +60,7 @@ func (r *threadSafeRouter) GetProof(store string, key []byte) (*ics23.Commitment // 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) { +func (r *threadSafeRouter) SetMigrationBatchSize(batchSize uint64) { 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 d34e6ab0bb..7a0e9ff403 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,7 +48,7 @@ func (b *blockingRouter) GetProof(_ string, _ []byte) (*ics23.CommitmentProof, e return nil, errors.New("proof: not implemented in blockingRouter") } -func (b *blockingRouter) SetMigrationBatchSize(int) {} +func (b *blockingRouter) SetMigrationBatchSize(uint64) {} // expectEntered receives one message from ch and asserts it equals expected. // Fails the test if no message arrives within timeout. diff --git a/sei-db/state_db/sc/types/types.go b/sei-db/state_db/sc/types/types.go index e789c8c360..428f736077 100644 --- a/sei-db/state_db/sc/types/types.go +++ b/sei-db/state_db/sc/types/types.go @@ -164,7 +164,7 @@ type Committer interface { // // Must be called between blocks, before the next block's first write // batch, on the consensus goroutine. - SetMigrationBatchSize(batchSize int) error + SetMigrationBatchSize(batchSize uint64) error // Closer releases all backing resources (open files, background // goroutines, locks). After Close the Committer must not be used. diff --git a/x/evm/keeper/trace_snapshot_test.go b/x/evm/keeper/trace_snapshot_test.go index b0c0ce47cd..a1a56e1e5e 100644 --- a/x/evm/keeper/trace_snapshot_test.go +++ b/x/evm/keeper/trace_snapshot_test.go @@ -30,7 +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) SetMigrationBatchSize(uint64) 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") From e74e6089c04345ef7970a6f36c1faac2a48d6a54 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Fri, 26 Jun 2026 10:40:32 -0700 Subject: [PATCH 06/19] Revert "Address comment" This reverts commit 91e17668bcf4d307ddff8242fa792fdb7c17a7ce. --- app/abci.go | 8 +++-- app/abci_test.go | 32 +++++++++---------- app/app.go | 12 +++++-- app/receipt_store_config.go | 2 +- app/test_helpers.go | 2 +- sei-cosmos/baseapp/baseapp.go | 29 ++++++----------- .../rootmulti/flatkv_migration_test.go | 2 +- sei-cosmos/storev2/rootmulti/store.go | 6 ++-- sei-db/state_db/sc/composite/random_test.go | 2 +- .../composite/random_test_framework_test.go | 2 +- sei-db/state_db/sc/composite/store.go | 12 +++---- .../state_db/sc/composite/store_auto_test.go | 4 +-- .../sc/composite/store_migration_test.go | 4 +-- sei-db/state_db/sc/memiavl/store.go | 2 +- .../sc/migration/dual_write_router.go | 2 +- .../sc/migration/migration_manager.go | 13 +++++--- .../sc/migration/migration_manager_test.go | 21 +++++++++++- .../migration_test_framework_test.go | 8 ++--- .../state_db/sc/migration/migration_types.go | 2 +- sei-db/state_db/sc/migration/module_router.go | 2 +- .../sc/migration/passthrough_router.go | 2 +- .../state_db/sc/migration/router_builder.go | 20 +++++++++--- .../sc/migration/router_kvstore_test.go | 2 +- .../sc/migration/thread_safe_router.go | 2 +- .../sc/migration/thread_safe_router_test.go | 2 +- sei-db/state_db/sc/types/types.go | 2 +- x/evm/keeper/trace_snapshot_test.go | 2 +- 27 files changed, 117 insertions(+), 82 deletions(-) diff --git a/app/abci.go b/app/abci.go index 0a4d55d8bb..1a75183d2d 100644 --- a/app/abci.go +++ b/app/abci.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "fmt" + "math" "math/big" "time" @@ -67,7 +68,7 @@ func (app *App) BeginBlock( // 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 { + if app.rs == nil { return } numKeys := migration.DefaultNumKeysToMigratePerBlock @@ -82,7 +83,10 @@ func (app *App) applyMigrationBatchSize(ctx sdk.Context) { } subspace.GetIfExists(ctx, migration.KeyNumKeysToMigratePerBlock, &numKeys) } - if err := app.rootStore.SetMigrationBatchSize(numKeys); err != nil { + if numKeys > uint64(math.MaxInt64) { + numKeys = uint64(math.MaxInt64) + } + if err := app.rs.SetMigrationBatchSize(int(numKeys)); err != nil { logger.Error("failed to set SC migration batch size", "err", err) } } diff --git a/app/abci_test.go b/app/abci_test.go index 426bf1eaea..7777d43c11 100644 --- a/app/abci_test.go +++ b/app/abci_test.go @@ -39,22 +39,22 @@ func TestApplyMigrationBatchSize(t *testing.T) { // Unset param: the store receives the default (0 = paused). a.applyMigrationBatchSize(ctx) - got, ok := a.rootStore.GetMigrationBatchSize() + got, ok := a.rs.GetMigrationBatchSize() require.True(t, ok, "SC store should track a migration batch size") - require.Equal(t, uint64(0), got) + 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, uint64(500), got) + got, _ = a.rs.GetMigrationBatchSize() + require.Equal(t, 500, got) - // The whole batch-size path is uint64 end-to-end, so the full param - // range is forwarded verbatim with no conversion or clamping. + // Out-of-int64-range values are clamped to MaxInt64 (defensive; gov + // validation only type-checks). subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(math.MaxUint64)) a.applyMigrationBatchSize(ctx) - got, _ = a.rootStore.GetMigrationBatchSize() - require.Equal(t, uint64(math.MaxUint64), got) + got, _ = a.rs.GetMigrationBatchSize() + require.Equal(t, math.MaxInt64, got) } // TestBeginBlockAppliesMigrationBatchSize exercises the full BeginBlock path @@ -66,9 +66,9 @@ func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { 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() + before, ok := a.rs.GetMigrationBatchSize() require.True(t, ok) - require.Equal(t, uint64(0), before) + require.Equal(t, 0, before) // Simulate the gov proposal landing in chain state. subspace, ok := a.ParamsKeeper.GetSubspace(migration.SubspaceName) @@ -80,8 +80,8 @@ func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { a.BeginBlock(ctx, 2, nil, nil, false) }) - after, _ := a.rootStore.GetMigrationBatchSize() - require.Equal(t, uint64(321), after, "BeginBlock should push the gov param into the SC store") + after, _ := a.rs.GetMigrationBatchSize() + require.Equal(t, 321, after, "BeginBlock should push the gov param into the SC store") } // TestMigrationBatchSizeTakesEffectNextBlock is the full end-to-end timing @@ -109,9 +109,9 @@ func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { // 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() + got, ok := a.rs.GetMigrationBatchSize() require.True(t, ok) - require.Equal(t, uint64(0), got, "param committed in block 1 must not take effect within block 1") + 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{ @@ -119,6 +119,6 @@ func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { }) require.NoError(t, err) - got, _ = a.rootStore.GetMigrationBatchSize() - require.Equal(t, uint64(640), got, "migration rate must take effect on the block after the param is committed") + got, _ = a.rs.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 977b7afcb9..8b172236b8 100644 --- a/app/app.go +++ b/app/app.go @@ -470,7 +470,7 @@ type App struct { genesisImportConfig genesistypes.GenesisImportConfig stateStore seidb.StateStore - rootStore *rootmulti.Store + rs *rootmulti.Store receiptStore receipt.ReceiptStore forkInitializer func(sdk.Context) @@ -551,7 +551,15 @@ func New( option(app) } - app.rootStore = app.V2RootMultiStore() + // 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. + rs, ok := app.CommitMultiStore().(*rootmulti.Store) + if !ok { + panic(fmt.Sprintf("unsupported commit multistore %T: expected *storev2_rootmulti.Store", app.CommitMultiStore())) + } + app.rs = rs app.ParamsKeeper = initParamsKeeper(appCodec, cdc, keys[paramstypes.StoreKey], tkeys[paramstypes.TStoreKey]) diff --git a/app/receipt_store_config.go b/app/receipt_store_config.go index a6fffaeeb4..afa07f854e 100644 --- a/app/receipt_store_config.go +++ b/app/receipt_store_config.go @@ -9,7 +9,7 @@ import ( ) const ( - receiptStoreBackendKey = "receipt-store.rootStore-backend" + receiptStoreBackendKey = "receipt-store.rs-backend" receiptStoreDBDirectoryKey = "receipt-store.db-directory" receiptStoreAsyncWriteBufferKey = "receipt-store.async-write-buffer" receiptStorePruneIntervalSecondsKey = "receipt-store.prune-interval-seconds" diff --git a/app/test_helpers.go b/app/test_helpers.go index 93be422b92..9a509d2149 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -483,7 +483,7 @@ func SetupWithDB(tb testing.TB, db dbm.DB, isCheckTx bool, enableEVMCustomPrecom // SetupWithScReceiptFromOpts is like SetupWithSc but does not inject a receipt store via AppOption. // The receipt store is created inside New() from testAppOpts. -// Use this to test the full app path with rootStore-backend from config. +// Use this to test the full app path with rs-backend from config. func SetupWithScReceiptFromOpts(t *testing.T, isCheckTx bool, enableEVMCustomPrecompiles bool, testAppOpts TestAppOpts, baseAppOptions ...func(*bam.BaseApp)) (res *App) { db := dbm.NewMemDB() encodingConfig := MakeEncodingConfig() diff --git a/sei-cosmos/baseapp/baseapp.go b/sei-cosmos/baseapp/baseapp.go index 673270e212..25cec9bbae 100644 --- a/sei-cosmos/baseapp/baseapp.go +++ b/sei-cosmos/baseapp/baseapp.go @@ -11,23 +11,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/gogo/protobuf/proto" "github.com/holiman/uint256" - "github.com/sei-protocol/seilog" - "github.com/spf13/cast" - leveldbutils "github.com/syndtr/goleveldb/leveldb/util" - dbm "github.com/tendermint/tm-db" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - otelmetric "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" cryptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types" "github.com/sei-protocol/sei-chain/sei-cosmos/server/config" servertypes "github.com/sei-protocol/sei-chain/sei-cosmos/server/types" "github.com/sei-protocol/sei-chain/sei-cosmos/snapshots" "github.com/sei-protocol/sei-chain/sei-cosmos/store" - "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" "github.com/sei-protocol/sei-chain/sei-cosmos/telemetry" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" @@ -37,6 +26,15 @@ import ( tmcfg "github.com/sei-protocol/sei-chain/sei-tendermint/config" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" sdbm "github.com/sei-protocol/sei-tm-db/backends" + "github.com/sei-protocol/seilog" + "github.com/spf13/cast" + leveldbutils "github.com/syndtr/goleveldb/leveldb/util" + dbm "github.com/tendermint/tm-db" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) const ( @@ -443,15 +441,6 @@ func (app *BaseApp) CommitMultiStore() sdk.CommitMultiStore { return app.cms } -// V2RootMultiStore The v2 rootmulti store is the only supported commit multistore; -// Fail fast if the legacy root multistore is somehow in use. -func (app *BaseApp) V2RootMultiStore() *rootmulti.Store { - if cms, ok := app.cms.(*rootmulti.Store); ok { - return cms - } - panic(fmt.Sprintf("unsupported commit multistore %T: expected storeV2 CMS", app.CommitMultiStore())) -} - // SnapshotManager returns the snapshot manager. // application use this to register extra extension snapshotters. func (app *BaseApp) SnapshotManager() *snapshots.Manager { diff --git a/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go b/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go index 5327299391..e9706a7a7d 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_migration_test.go @@ -86,7 +86,7 @@ func driveRootMultiMigration( dir string, phase1Blocks, phase2Blocks int, phase1StorageKeysPerBlock int, - migrateBatchSize uint64, + migrateBatchSize int, ) (*Store, map[string]*types.KVStoreKey, []commitRecord) { t.Helper() diff --git a/sei-cosmos/storev2/rootmulti/store.go b/sei-cosmos/storev2/rootmulti/store.go index c55785d60d..a2bc384b11 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -656,7 +656,7 @@ func (rs *Store) SetInitialVersion(version int64) error { // Unlike SetWriteMode this does not swap the router or touch the cached // views, so no view rebuild (and no pending-changes guard) is needed. The // app calls it from BeginBlock, before the block's first write. -func (rs *Store) SetMigrationBatchSize(batchSize uint64) error { +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) } @@ -667,8 +667,8 @@ func (rs *Store) SetMigrationBatchSize(batchSize uint64) error { // 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() (uint64, bool) { - if g, ok := rs.scStore.(interface{ GetMigrationBatchSize() uint64 }); ok { +func (rs *Store) GetMigrationBatchSize() (int, bool) { + if g, ok := rs.scStore.(interface{ GetMigrationBatchSize() int }); ok { return g.GetMigrationBatchSize(), true } return 0, false diff --git a/sei-db/state_db/sc/composite/random_test.go b/sei-db/state_db/sc/composite/random_test.go index f5500f90c8..24ba2edf97 100644 --- a/sei-db/state_db/sc/composite/random_test.go +++ b/sei-db/state_db/sc/composite/random_test.go @@ -263,7 +263,7 @@ func runMigrationScenario(t *testing.T, sc migrationScenario) { // 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 = uint64(migBatch) + testMigrationBatchSize = migBatch defer func() { testMigrationBatchSize = 0 }() t.Logf("migration scenario %s->%s keysToMigratePerBlock=%d", sc.migrationMode, sc.successorMode, migBatch) 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 bcbe807128..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 @@ -1490,7 +1490,7 @@ func randomTestConfig(t *testing.T, rng *testutil.TestRandom, mode types.WriteMo // 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 uint64 +var testMigrationBatchSize int // applyTestMigrationBatchSize re-applies testMigrationBatchSize to a freshly // opened store, mimicking the app's BeginBlock push of the gov param. diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 94860830cd..7acfa49bbb 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -104,7 +104,7 @@ type CompositeCommitStore struct { // 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.Uint64 + migrationBatchSize atomic.Int64 // migrationAdvancedThisCommit gates per-block migration progress // against rootmulti.Store's double-flush pattern. rootmulti calls @@ -457,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.migrationBatchSize.Load()) + routerCtx, cs.currentWriteMode, cs.memIAVL, cs.flatKV, int(cs.migrationBatchSize.Load())) if err != nil { cancel() return fmt.Errorf("failed to build router: %w", err) @@ -478,8 +478,8 @@ func (cs *CompositeCommitStore) buildRouter() error { // 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 uint64) error { - cs.migrationBatchSize.Store(batchSize) +func (cs *CompositeCommitStore) SetMigrationBatchSize(batchSize int) error { + cs.migrationBatchSize.Store(int64(batchSize)) if cs.router != nil { cs.router.SetMigrationBatchSize(batchSize) } @@ -489,8 +489,8 @@ func (cs *CompositeCommitStore) SetMigrationBatchSize(batchSize uint64) error { // 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() uint64 { - return cs.migrationBatchSize.Load() +func (cs *CompositeCommitStore) GetMigrationBatchSize() int { + return int(cs.migrationBatchSize.Load()) } // SetWriteMode transitions the effective write mode at runtime. Only legal 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 0482a921bc..a7b6b9f57a 100644 --- a/sei-db/state_db/sc/composite/store_auto_test.go +++ b/sei-db/state_db/sc/composite/store_auto_test.go @@ -27,7 +27,7 @@ func autoConfig() config.StateCommitConfig { } // openAutoStore opens (or reopens) a composite store at dir in Auto mode. -func openAutoStore(t *testing.T, dir string, batch uint64) *CompositeCommitStore { +func openAutoStore(t *testing.T, dir string, batch int) *CompositeCommitStore { t.Helper() cs, err := NewCompositeCommitStore(t.Context(), dir, autoConfig()) require.NoError(t, err) @@ -284,7 +284,7 @@ func autoExportConfig() config.StateCommitConfig { } // openAutoStoreWithConfig mirrors openAutoStore for a caller-supplied config. -func openAutoStoreWithConfig(t *testing.T, dir string, cfg config.StateCommitConfig, batch uint64) *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) 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 62230110dc..2b5399c679 100644 --- a/sei-db/state_db/sc/composite/store_migration_test.go +++ b/sei-db/state_db/sc/composite/store_migration_test.go @@ -227,7 +227,7 @@ func driveMigrationWorkload( dir string, workload *migrationWorkload, phase1Blocks, phase2Blocks int, - keysToMigratePerBlock uint64, + keysToMigratePerBlock int, ) { t.Helper() @@ -284,7 +284,7 @@ func driveMigrationWorkload( // reopenInMigrateEVM is a small helper for the resume / migration paths // that need to peek at on-disk state from a MigrateEVM mode reopen. -func reopenInMigrateEVM(t *testing.T, dir string, batch uint64) *CompositeCommitStore { +func reopenInMigrateEVM(t *testing.T, dir string, batch int) *CompositeCommitStore { t.Helper() cfg := config.DefaultStateCommitConfig() cfg.WriteMode = types.MigrateEVM diff --git a/sei-db/state_db/sc/memiavl/store.go b/sei-db/state_db/sc/memiavl/store.go index fb356bdb2d..290e6bdb7f 100644 --- a/sei-db/state_db/sc/memiavl/store.go +++ b/sei-db/state_db/sc/memiavl/store.go @@ -105,7 +105,7 @@ func (cs *CommitStore) SetWriteMode(types.WriteMode) error { // SetMigrationBatchSize implements types.Committer. The memiavl commit // store runs no migration of its own, so this is a no-op. -func (cs *CommitStore) SetMigrationBatchSize(uint64) error { +func (cs *CommitStore) SetMigrationBatchSize(int) error { return 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 8c4e324f87..32dc7ec82d 100644 --- a/sei-db/state_db/sc/migration/dual_write_router.go +++ b/sei-db/state_db/sc/migration/dual_write_router.go @@ -75,7 +75,7 @@ func (t *TestOnlyDualWriteRouter) Read(store string, key []byte) ([]byte, bool, // SetMigrationBatchSize is a no-op: the dual-write router duplicates // traffic and performs no boundary-advancing data migration. -func (t *TestOnlyDualWriteRouter) SetMigrationBatchSize(uint64) {} +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 diff --git a/sei-db/state_db/sc/migration/migration_manager.go b/sei-db/state_db/sc/migration/migration_manager.go index 4a9cafff72..d803ff786f 100644 --- a/sei-db/state_db/sc/migration/migration_manager.go +++ b/sei-db/state_db/sc/migration/migration_manager.go @@ -46,7 +46,7 @@ type MigrationManager struct { boundary MigrationBoundary // The number of key-value pairs to migrate after each write operation. - migrationBatchSize uint64 + migrationBatchSize int // The version we want to migrate to. targetVersion uint64 @@ -61,7 +61,7 @@ type MigrationManager struct { // Handles the migration of data from one database to another. func NewMigrationManager( // The number of key-value pairs to migrate after each write operation. Must be > 0. - migrationBatchSize uint64, + migrationBatchSize int, // The migration version the stored data is expected to be at on entry. If no prior migration // version is stored in the DB, startVersion should be 0. startVersion uint64, @@ -101,7 +101,10 @@ func NewMigrationManager( // 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). + // acts as the migration trigger). Only a negative size is rejected. + if migrationBatchSize < 0 { + return nil, fmt.Errorf("migration batch size must not be negative, got %d", migrationBatchSize) + } if startVersion >= targetVersion { return nil, fmt.Errorf("startVersion (%d) must be strictly less than targetVersion (%d)", startVersion, targetVersion) @@ -296,7 +299,7 @@ func (m *MigrationManager) ApplyChangeSets(changesets []*proto.NamedChangeSet, f batchStats := migrationBatchStats{} if advanceMigration { // Get the next batch of keys to migrate. - valuesToMigrate, newBoundary, err := m.iterator.NextBatch(int(m.migrationBatchSize)) + valuesToMigrate, newBoundary, err := m.iterator.NextBatch(m.migrationBatchSize) if err != nil { return fmt.Errorf("failed to get next batch: %w", err) } @@ -487,7 +490,7 @@ func (m *MigrationManager) GetProof(store string, key []byte) (*ics23.Commitment // 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 uint64) { +func (m *MigrationManager) SetMigrationBatchSize(batchSize int) { m.migrationBatchSize = batchSize } 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 8edb6d8dae..14bba7b486 100644 --- a/sei-db/state_db/sc/migration/migration_manager_test.go +++ b/sei-db/state_db/sc/migration/migration_manager_test.go @@ -101,7 +101,7 @@ func newTestManager( oldReader DBReader, oldWriter DBWriter, newReader DBReader, newWriter DBWriter, iter MigrationIterator, - size uint64, + size int, ) (*MigrationManager, error) { t.Helper() return NewMigrationManager( @@ -842,6 +842,25 @@ func fuzzApplyToReference(ref map[string]map[string][]byte, changesets []*proto. // --- Constructor validation (Issue 2, Issue 11) --- +func TestNewMigrationManager_RejectsNegativeBatchSize(t *testing.T) { + cases := []int{-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) + + _, 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 not be negative") + }) + } +} + // 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) { 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 aa9d0267c9..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,7 +53,7 @@ func (m *TestMultiDB) GetProof(store string, key []byte) (*ics23.CommitmentProof panic("not implemented") } -func (m *TestMultiDB) SetMigrationBatchSize(batchSize uint64) { +func (m *TestMultiDB) SetMigrationBatchSize(batchSize int) { for _, nestedDB := range m.nestedDBs { nestedDB.SetMigrationBatchSize(batchSize) } @@ -109,7 +109,7 @@ func (r *TestFlatKVRouter) GetProof(store string, key []byte) (*ics23.Commitment return nil, errors.New("TestFlatKVRouter does not support proofs") } -func (r *TestFlatKVRouter) SetMigrationBatchSize(uint64) {} +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. @@ -140,7 +140,7 @@ func (r *TestMemIAVLRouter) GetProof(store string, key []byte) (*ics23.Commitmen return nil, errors.New("TestMemIAVLRouter does not support proofs") } -func (r *TestMemIAVLRouter) SetMigrationBatchSize(uint64) {} +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 @@ -195,7 +195,7 @@ func (r *TestInMemoryRouter) GetProof(store string, key []byte) (*ics23.Commitme return nil, errors.New("TestInMemoryRouter does not support proofs") } -func (r *TestInMemoryRouter) SetMigrationBatchSize(uint64) {} +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 diff --git a/sei-db/state_db/sc/migration/migration_types.go b/sei-db/state_db/sc/migration/migration_types.go index 96f73febed..244c67640f 100644 --- a/sei-db/state_db/sc/migration/migration_types.go +++ b/sei-db/state_db/sc/migration/migration_types.go @@ -85,5 +85,5 @@ type Router interface { // Must be called between blocks, on the same goroutine that drives // ApplyChangeSets. threadSafeRouter additionally serializes it against // concurrent reads. - SetMigrationBatchSize(batchSize uint64) + 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 dc3c5289ee..fcaf1e6c33 100644 --- a/sei-db/state_db/sc/migration/module_router.go +++ b/sei-db/state_db/sc/migration/module_router.go @@ -180,7 +180,7 @@ func (m *ModuleRouter) GetProof(store string, key []byte) (*ics23.CommitmentProo // 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 uint64) { +func (m *ModuleRouter) SetMigrationBatchSize(batchSize int) { signalled := make(map[Router]struct{}, len(m.routes)) for _, r := range m.routes { if r.owner == nil { diff --git a/sei-db/state_db/sc/migration/passthrough_router.go b/sei-db/state_db/sc/migration/passthrough_router.go index 47a78879a0..565d3cf348 100644 --- a/sei-db/state_db/sc/migration/passthrough_router.go +++ b/sei-db/state_db/sc/migration/passthrough_router.go @@ -71,4 +71,4 @@ func (p *PassthroughRouter) GetProof(store string, key []byte) (*ics23.Commitmen // SetMigrationBatchSize is a no-op: a passthrough router targets a single // backend and performs no data migration. -func (p *PassthroughRouter) SetMigrationBatchSize(uint64) {} +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 f63e2e2550..5f69833ec3 100644 --- a/sei-db/state_db/sc/migration/router_builder.go +++ b/sei-db/state_db/sc/migration/router_builder.go @@ -21,7 +21,7 @@ func BuildRouter( memIAVL *memiavl.CommitStore, flatKV flatkv.Store, // If this router will be doing data migration, this is the number of keys to migrate in each batch. - migrationBatchSize uint64, + migrationBatchSize int, ) (Router, error) { switch writeMode { @@ -149,7 +149,7 @@ func buildMigrateEVMRouter( ctx context.Context, memIAVL *memiavl.CommitStore, flatKV flatkv.Store, - migrationBatchSize uint64, + migrationBatchSize int, ) (Router, error) { if memIAVL == nil { @@ -158,6 +158,10 @@ func buildMigrateEVMRouter( if flatKV == nil { return nil, fmt.Errorf("flatKV is nil") } + if migrationBatchSize < 0 { + return nil, fmt.Errorf("migrationBatchSize must not be negative") + } + // Manages migration and routing for keys in the evm/ module. migrationManager, err := NewMigrationManager( migrationBatchSize, @@ -269,7 +273,7 @@ func buildMigrateAllButBankRouter( ctx context.Context, memIAVL *memiavl.CommitStore, flatKV flatkv.Store, - migrationBatchSize uint64, + migrationBatchSize int, ) (Router, error) { if memIAVL == nil { @@ -278,6 +282,10 @@ func buildMigrateAllButBankRouter( if flatKV == nil { return nil, fmt.Errorf("flatKV is nil") } + if migrationBatchSize < 0 { + return nil, fmt.Errorf("migrationBatchSize must not be negative") + } + allModulesButEvmAndBank, err := keys.AllModulesExcept(keys.EVMStoreKey, keys.BankStoreKey) if err != nil { return nil, fmt.Errorf("AllModulesExcept: %w", err) @@ -394,7 +402,7 @@ func buildMigrateBankRouter( ctx context.Context, memIAVL *memiavl.CommitStore, flatKV flatkv.Store, - migrationBatchSize uint64, + migrationBatchSize int, ) (Router, error) { if memIAVL == nil { @@ -403,6 +411,10 @@ func buildMigrateBankRouter( if flatKV == nil { return nil, fmt.Errorf("flatKV is nil") } + if migrationBatchSize < 0 { + return nil, fmt.Errorf("migrationBatchSize must not be negative") + } + 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 741e1368e7..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,7 +215,7 @@ func (f *failingRouter) GetProof(string, []byte) (*ics23.CommitmentProof, error) return nil, errors.New("failingRouter.GetProof: not used by these tests") } -func (f *failingRouter) SetMigrationBatchSize(uint64) {} +func (f *failingRouter) SetMigrationBatchSize(int) {} func TestRouterCommitKVStore_GetPanicsOnRouterError(t *testing.T) { store := NewRouterCommitKVStore( 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 90e52b26cb..ebf6cacd69 100644 --- a/sei-db/state_db/sc/migration/thread_safe_router.go +++ b/sei-db/state_db/sc/migration/thread_safe_router.go @@ -60,7 +60,7 @@ func (r *threadSafeRouter) GetProof(store string, key []byte) (*ics23.Commitment // 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 uint64) { +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 7a0e9ff403..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,7 +48,7 @@ func (b *blockingRouter) GetProof(_ string, _ []byte) (*ics23.CommitmentProof, e return nil, errors.New("proof: not implemented in blockingRouter") } -func (b *blockingRouter) SetMigrationBatchSize(uint64) {} +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. diff --git a/sei-db/state_db/sc/types/types.go b/sei-db/state_db/sc/types/types.go index 428f736077..e789c8c360 100644 --- a/sei-db/state_db/sc/types/types.go +++ b/sei-db/state_db/sc/types/types.go @@ -164,7 +164,7 @@ type Committer interface { // // Must be called between blocks, before the next block's first write // batch, on the consensus goroutine. - SetMigrationBatchSize(batchSize uint64) error + SetMigrationBatchSize(batchSize int) error // Closer releases all backing resources (open files, background // goroutines, locks). After Close the Committer must not be used. diff --git a/x/evm/keeper/trace_snapshot_test.go b/x/evm/keeper/trace_snapshot_test.go index a1a56e1e5e..b0c0ce47cd 100644 --- a/x/evm/keeper/trace_snapshot_test.go +++ b/x/evm/keeper/trace_snapshot_test.go @@ -30,7 +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(uint64) 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") From 3327b28686b84e707fcf9ce46322f0862a5349fd Mon Sep 17 00:00:00 2001 From: YimingZang Date: Fri, 26 Jun 2026 10:42:13 -0700 Subject: [PATCH 07/19] Rename rs to rootStore --- app/abci.go | 4 ++-- app/abci_test.go | 14 +++++++------- app/app.go | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/abci.go b/app/abci.go index 1a75183d2d..43d787ecae 100644 --- a/app/abci.go +++ b/app/abci.go @@ -68,7 +68,7 @@ func (app *App) BeginBlock( // 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.rs == nil { + if app.rootStore == nil { return } numKeys := migration.DefaultNumKeysToMigratePerBlock @@ -86,7 +86,7 @@ func (app *App) applyMigrationBatchSize(ctx sdk.Context) { if numKeys > uint64(math.MaxInt64) { numKeys = uint64(math.MaxInt64) } - if err := app.rs.SetMigrationBatchSize(int(numKeys)); err != nil { + if err := app.rootStore.SetMigrationBatchSize(int(numKeys)); err != nil { logger.Error("failed to set SC migration batch size", "err", err) } } diff --git a/app/abci_test.go b/app/abci_test.go index 7777d43c11..91b19b0a75 100644 --- a/app/abci_test.go +++ b/app/abci_test.go @@ -39,21 +39,21 @@ func TestApplyMigrationBatchSize(t *testing.T) { // Unset param: the store receives the default (0 = paused). a.applyMigrationBatchSize(ctx) - got, ok := a.rs.GetMigrationBatchSize() + 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.rs.GetMigrationBatchSize() + got, _ = a.rootStore.GetMigrationBatchSize() require.Equal(t, 500, got) // Out-of-int64-range values are clamped to MaxInt64 (defensive; gov // validation only type-checks). subspace.Set(ctx, migration.KeyNumKeysToMigratePerBlock, uint64(math.MaxUint64)) a.applyMigrationBatchSize(ctx) - got, _ = a.rs.GetMigrationBatchSize() + got, _ = a.rootStore.GetMigrationBatchSize() require.Equal(t, math.MaxInt64, got) } @@ -66,7 +66,7 @@ func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { 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.rs.GetMigrationBatchSize() + before, ok := a.rootStore.GetMigrationBatchSize() require.True(t, ok) require.Equal(t, 0, before) @@ -80,7 +80,7 @@ func TestBeginBlockAppliesMigrationBatchSize(t *testing.T) { a.BeginBlock(ctx, 2, nil, nil, false) }) - after, _ := a.rs.GetMigrationBatchSize() + after, _ := a.rootStore.GetMigrationBatchSize() require.Equal(t, 321, after, "BeginBlock should push the gov param into the SC store") } @@ -109,7 +109,7 @@ func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { // 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.rs.GetMigrationBatchSize() + 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") @@ -119,6 +119,6 @@ func TestMigrationBatchSizeTakesEffectNextBlock(t *testing.T) { }) require.NoError(t, err) - got, _ = a.rs.GetMigrationBatchSize() + 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 8b172236b8..ed52d17018 100644 --- a/app/app.go +++ b/app/app.go @@ -470,7 +470,7 @@ type App struct { genesisImportConfig genesistypes.GenesisImportConfig stateStore seidb.StateStore - rs *rootmulti.Store + rootStore *rootmulti.Store receiptStore receipt.ReceiptStore forkInitializer func(sdk.Context) @@ -555,11 +555,11 @@ func New( // 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. - rs, ok := app.CommitMultiStore().(*rootmulti.Store) + rootStore, ok := app.CommitMultiStore().(*rootmulti.Store) if !ok { panic(fmt.Sprintf("unsupported commit multistore %T: expected *storev2_rootmulti.Store", app.CommitMultiStore())) } - app.rs = rs + app.rootStore = rootStore app.ParamsKeeper = initParamsKeeper(appCodec, cdc, keys[paramstypes.StoreKey], tkeys[paramstypes.TStoreKey]) From a5cf52863cdf905dd6e24edaf5951c82be11283f Mon Sep 17 00:00:00 2001 From: YimingZang Date: Fri, 26 Jun 2026 10:49:08 -0700 Subject: [PATCH 08/19] Fix negative batch size --- sei-db/state_db/sc/composite/store.go | 8 ++++++++ .../state_db/sc/composite/store_auto_test.go | 19 +++++++++++++++++++ .../sc/migration/migration_manager.go | 7 +++---- .../sc/migration/migration_manager_test.go | 19 ------------------- .../state_db/sc/migration/router_builder.go | 12 ------------ 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 7acfa49bbb..6604685a30 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -475,10 +475,18 @@ func (cs *CompositeCommitStore) buildRouter() error { // (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) 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 a7b6b9f57a..086f37df43 100644 --- a/sei-db/state_db/sc/composite/store_auto_test.go +++ b/sei-db/state_db/sc/composite/store_auto_test.go @@ -38,6 +38,25 @@ func openAutoStore(t *testing.T, dir string, batch int) *CompositeCommitStore { 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() diff --git a/sei-db/state_db/sc/migration/migration_manager.go b/sei-db/state_db/sc/migration/migration_manager.go index d803ff786f..daa51286f8 100644 --- a/sei-db/state_db/sc/migration/migration_manager.go +++ b/sei-db/state_db/sc/migration/migration_manager.go @@ -101,10 +101,9 @@ func NewMigrationManager( // 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). Only a negative size is rejected. - if migrationBatchSize < 0 { - return nil, fmt.Errorf("migration batch size must not be negative, got %d", migrationBatchSize) - } + // 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) 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 14bba7b486..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,25 +842,6 @@ func fuzzApplyToReference(ref map[string]map[string][]byte, changesets []*proto. // --- Constructor validation (Issue 2, Issue 11) --- -func TestNewMigrationManager_RejectsNegativeBatchSize(t *testing.T) { - cases := []int{-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) - - _, 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 not be negative") - }) - } -} - // 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) { diff --git a/sei-db/state_db/sc/migration/router_builder.go b/sei-db/state_db/sc/migration/router_builder.go index 5f69833ec3..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 not be negative") - } - // 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 not be negative") - } - 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 not be negative") - } - allButBankModules, err := keys.AllModulesExcept(keys.BankStoreKey) if err != nil { return nil, fmt.Errorf("AllModulesExcept: %w", err) From c20c5a891567e86fbb0c5c46c067ab150701e3b2 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Mon, 29 Jun 2026 08:25:06 -0700 Subject: [PATCH 09/19] Default to migrateEVM mode --- sei-db/config/sc_config.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index 2ef0e3f4eb..750f3c0a60 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -32,7 +32,9 @@ type StateCommitConfig struct { // 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. + // defaults to migrate_evm. While the NumKeysToMigratePerBlock gov param is 0 + // (the default), migrate_evm is paused and produces the same app hash as + // memiavl_only; raising the param via governance is the sole migration trigger. WriteMode types.WriteMode `mapstructure:"write-mode"` // MemIAVLConfig is the configuration for the MemIAVL (Cosmos) backend @@ -56,7 +58,7 @@ type StateCommitConfig struct { func DefaultStateCommitConfig() StateCommitConfig { return StateCommitConfig{ Enable: true, - WriteMode: types.MemiavlOnly, + WriteMode: types.MigrateEVM, MemIAVLConfig: memiavl.DefaultConfig(), FlatKVConfig: *config.DefaultConfig(), HistoricalProofMaxInFlight: DefaultSCHistoricalProofMaxInFlight, From 76e5040523982c86597bf22756f10edb6ccb3131 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Tue, 30 Jun 2026 07:51:31 -0700 Subject: [PATCH 10/19] Use auto as the default sc-write-mode --- .../storev2/rootmulti/set_write_mode_test.go | 118 +++++++++++++++++- sei-cosmos/storev2/rootmulti/store.go | 41 +++++- sei-db/config/sc_config.go | 11 +- sei-db/state_db/sc/composite/store.go | 9 ++ 4 files changed, 165 insertions(+), 14 deletions(-) diff --git a/sei-cosmos/storev2/rootmulti/set_write_mode_test.go b/sei-cosmos/storev2/rootmulti/set_write_mode_test.go index 7cc7a4281b..777ebb43b8 100644 --- a/sei-cosmos/storev2/rootmulti/set_write_mode_test.go +++ b/sei-cosmos/storev2/rootmulti/set_write_mode_test.go @@ -4,6 +4,7 @@ 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" ) @@ -24,10 +25,10 @@ func autoModeConfig() seidbconfig.StateCommitConfig { func TestRootMultiSetWriteMode_StaleViewsRouteCorrectly(t *testing.T) { dir := t.TempDir() store, storeKeys := newTestRootMulti(t, dir, autoModeConfig()) - require.NoError(t, store.SetMigrationBatchSize(100)) 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 @@ -68,7 +71,6 @@ func TestRootMultiSetWriteMode_StaleViewsRouteCorrectly(t *testing.T) { func TestRootMultiSetWriteMode_RejectsPendingChanges(t *testing.T) { dir := t.TempDir() store, storeKeys := newTestRootMulti(t, dir, autoModeConfig()) - require.NoError(t, store.SetMigrationBatchSize(100)) defer func() { require.NoError(t, store.Close()) }() addr := newEVMTestData(0xD2) @@ -102,3 +104,109 @@ func TestRootMultiSetWriteMode_RequiresAutoConfig(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "fixed") } + +// 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 a2bc384b11..eb40c8fea8 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -650,19 +650,50 @@ func (rs *Store) SetInitialVersion(version int64) error { } // SetMigrationBatchSize forwards the governance-controlled number of keys -// to migrate per block to the SC store. Only a composite store mid- -// migration acts on it; every other SC store treats it as a no-op. +// to migrate per block to the SC store, and — when migration has been +// requested (batch size > 0) while the store is still in the pre-migration +// steady state — kicks it off by transitioning memiavl_only -> migrate_evm. +// Only a composite store under types.Auto acts on either; every other SC +// store treats the forward as a no-op and never reports a memiavl_only +// effective mode, so the kick-off is skipped. // -// Unlike SetWriteMode this does not swap the router or touch the cached -// views, so no view rebuild (and no pending-changes guard) is needed. The -// app calls it from BeginBlock, before the block's first write. +// 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 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 before the block's first write, so the +// empty-buffer precondition of SetWriteMode (which rebuilds the cached +// views) holds. Forwarding the batch size first means the migration router +// built by the transition already observes the new rate. 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 { + if mode, ok := rs.GetWriteMode(); ok && mode == sctypes.MemiavlOnly { + 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 +} + // 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 diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index 750f3c0a60..a61b69233a 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -32,9 +32,12 @@ type StateCommitConfig struct { // 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 migrate_evm. While the NumKeysToMigratePerBlock gov param is 0 - // (the default), migrate_evm is paused and produces the same app hash as - // memiavl_only; raising the param via governance is the sole migration trigger. + // Defaults to auto: 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). + // Raising the NumKeysToMigratePerBlock gov param above 0 is the sole + // migration trigger — it advances an auto store from memiavl_only to + // migrate_evm. The other values pin a fixed mode and disable derivation. WriteMode types.WriteMode `mapstructure:"write-mode"` // MemIAVLConfig is the configuration for the MemIAVL (Cosmos) backend @@ -58,7 +61,7 @@ type StateCommitConfig struct { func DefaultStateCommitConfig() StateCommitConfig { return StateCommitConfig{ Enable: true, - WriteMode: types.MigrateEVM, + WriteMode: types.Auto, MemIAVLConfig: memiavl.DefaultConfig(), FlatKVConfig: *config.DefaultConfig(), HistoricalProofMaxInFlight: DefaultSCHistoricalProofMaxInFlight, diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 6604685a30..46406fd6f6 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -501,6 +501,15 @@ 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 +} + // 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. From 20390c1adf9eab3b99ce64b1608224bc9f8725cd Mon Sep 17 00:00:00 2001 From: YimingZang Date: Tue, 30 Jun 2026 09:13:43 -0700 Subject: [PATCH 11/19] Address comments --- app/abci.go | 10 +- .../storev2/rootmulti/set_write_mode_test.go | 82 +++++++---- sei-cosmos/storev2/rootmulti/store.go | 130 ++++++++++-------- sei-db/state_db/sc/composite/store.go | 11 ++ 4 files changed, 150 insertions(+), 83 deletions(-) diff --git a/app/abci.go b/app/abci.go index 43d787ecae..0a22739def 100644 --- a/app/abci.go +++ b/app/abci.go @@ -87,7 +87,15 @@ func (app *App) applyMigrationBatchSize(ctx sdk.Context) { numKeys = uint64(math.MaxInt64) } if err := app.rootStore.SetMigrationBatchSize(int(numKeys)); err != nil { - logger.Error("failed to set SC migration batch size", "err", err) + // 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) } } diff --git a/sei-cosmos/storev2/rootmulti/set_write_mode_test.go b/sei-cosmos/storev2/rootmulti/set_write_mode_test.go index 777ebb43b8..2aa91b6c61 100644 --- a/sei-cosmos/storev2/rootmulti/set_write_mode_test.go +++ b/sei-cosmos/storev2/rootmulti/set_write_mode_test.go @@ -64,45 +64,79 @@ 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()) + 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") +} + +// 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()) }() - // After a normal commit cycle the same transition succeeds. + 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 diff --git a/sei-cosmos/storev2/rootmulti/store.go b/sei-cosmos/storev2/rootmulti/store.go index eb40c8fea8..a89480d648 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -462,56 +462,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 } @@ -651,20 +633,27 @@ func (rs *Store) SetInitialVersion(version int64) error { // 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 the store is still in the pre-migration -// steady state — kicks it off by transitioning memiavl_only -> migrate_evm. -// Only a composite store under types.Auto acts on either; every other SC -// store treats the forward as a no-op and never reports a memiavl_only -// effective mode, so the kick-off is skipped. +// 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 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. +// 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 before the block's first write, so the // empty-buffer precondition of SetWriteMode (which rebuilds the cached @@ -674,12 +663,26 @@ 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 { - if mode, ok := rs.GetWriteMode(); ok && mode == sctypes.MemiavlOnly { - if err := rs.SetWriteMode(sctypes.MigrateEVM); err != nil { - return fmt.Errorf("failed to start EVM migration (memiavl_only -> migrate_evm): %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. + if configured, _ := rs.ConfiguredWriteMode(); 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 to auto 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 } @@ -694,6 +697,17 @@ func (rs *Store) GetWriteMode() (sctypes.WriteMode, bool) { 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 diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 46406fd6f6..3ec7a5f1ef 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -510,6 +510,17 @@ 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. From 63ba76a783a9e612ce83ad37d3eeef24181a1539 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Tue, 30 Jun 2026 09:27:59 -0700 Subject: [PATCH 12/19] Fix unit test --- sei-cosmos/server/config/config_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sei-cosmos/server/config/config_test.go b/sei-cosmos/server/config/config_test.go index 0961681e81..3d8b81931d 100644 --- a/sei-cosmos/server/config/config_test.go +++ b/sei-cosmos/server/config/config_test.go @@ -373,7 +373,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") } @@ -417,7 +417,7 @@ func TestDefaultStateCommitConfig(t *testing.T) { require.True(t, cfg.StateCommit.Enable) require.Empty(t, cfg.StateCommit.Directory) - require.Equal(t, sctypes.MemiavlOnly, cfg.StateCommit.WriteMode) + require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode) } func TestDefaultStateStoreConfig(t *testing.T) { From 453bb7606cb7d4397d70a1ba11d805f75b0a588e Mon Sep 17 00:00:00 2001 From: YimingZang Date: Tue, 30 Jun 2026 10:56:30 -0700 Subject: [PATCH 13/19] Fix test cases --- sei-ibc-go/modules/core/03-connection/types/msgs_test.go | 4 ++++ sei-ibc-go/modules/core/04-channel/types/msgs_test.go | 4 ++++ .../modules/core/23-commitment/types/commitment_test.go | 4 ++++ sei-ibc-go/testing/simapp/test_helpers.go | 6 ++++++ 4 files changed, 18 insertions(+) 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 624bcdbdb5..ac0212d497 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 From 9a276ac9d29f8d1953b3186e9834f26b4ad780bd Mon Sep 17 00:00:00 2001 From: YimingZang Date: Tue, 30 Jun 2026 12:57:37 -0700 Subject: [PATCH 14/19] Address comments --- app/seidb.go | 15 ++++++++++- app/seidb_test.go | 7 +++++- sei-cosmos/server/config/config.go | 17 ++++++++++--- sei-cosmos/server/config/config_test.go | 27 +++++++++++++++++++- sei-cosmos/storev2/rootmulti/store.go | 26 ++++++++++++++----- sei-db/config/sc_config.go | 33 +++++++++++++++++-------- sei-db/config/toml.go | 24 ++++++++++-------- sei-wasmd/app/test_helpers.go | 6 +++++ 8 files changed, 123 insertions(+), 32 deletions(-) diff --git a/app/seidb.go b/app/seidb.go index 0cf6a94a20..8595bfdc6f 100644 --- a/app/seidb.go +++ b/app/seidb.go @@ -29,6 +29,7 @@ const ( FlagSCHistoricalProofRateLimit = "state-commit.sc-historical-proof-rate-limit" FlagSCHistoricalProofBurst = "state-commit.sc-historical-proof-burst" FlagSCWriteMode = "state-commit.sc-write-mode" + FlagSCWriteModeEnableAuto = "state-commit.sc-write-mode-enable-auto" FlagSCFlatKVReadWriteMetrics = "state-commit.flatkv.enable-read-write-metrics" // SS Store configs @@ -106,7 +107,19 @@ func parseSCConfigs(appOpts servertypes.AppOptions) config.StateCommitConfig { scConfig.MemIAVLConfig.SnapshotWriteRateMBps = cast.ToInt(appOpts.Get(FlagSCSnapshotWriteRateMBps)) scConfig.FlatKVConfig.EnableReadWriteMetrics = cast.ToBool(appOpts.Get(FlagSCFlatKVReadWriteMetrics)) - if wm := cast.ToString(appOpts.Get(FlagSCWriteMode)); wm != "" { + // sc-write-mode-enable-auto (default true) decides whether the node derives + // its write mode automatically or honors the explicit sc-write-mode. An + // ABSENT key must keep 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) + } + if scConfig.WriteModeEnableAuto { + scConfig.WriteMode = sctypes.Auto + } else if wm := cast.ToString(appOpts.Get(FlagSCWriteMode)); wm != "" { parsedWM, err := sctypes.ParseWriteMode(wm) if err != nil { panic(fmt.Sprintf("invalid EVM SS write mode %q: %s", wm, err)) 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/sei-cosmos/server/config/config.go b/sei-cosmos/server/config/config.go index dc83bc5f9e..42dae81648 100644 --- a/sei-cosmos/server/config/config.go +++ b/sei-cosmos/server/config/config.go @@ -347,6 +347,16 @@ func GetConfig(v *viper.Viper) (Config, error) { } scWriteMode = parsed } + // sc-write-mode-enable-auto (default true) overrides sc-write-mode with + // auto. An absent key keeps the default so older configs (explicit + // memiavl_only, no auto key) still resolve to auto, mirroring app/seidb.go. + scWriteModeEnableAuto := config.DefaultStateCommitConfig().WriteModeEnableAuto + if v.IsSet("state-commit.sc-write-mode-enable-auto") { + scWriteModeEnableAuto = v.GetBool("state-commit.sc-write-mode-enable-auto") + } + if scWriteModeEnableAuto { + scWriteMode = sctypes.Auto + } flatKVConfig := config.DefaultStateCommitConfig().FlatKVConfig if v.IsSet("state-commit.flatkv.fsync") { @@ -431,9 +441,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 3d8b81931d..5617e77b45 100644 --- a/sei-cosmos/server/config/config_test.go +++ b/sei-cosmos/server/config/config_test.go @@ -327,6 +327,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) @@ -340,6 +342,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 @@ -365,6 +368,25 @@ func TestGetConfigRejectsInvalidWriteMode(t *testing.T) { require.Contains(t, err.Error(), "bogus_mode") } +// TestGetConfigLegacyMemiavlOnlyResolvesToAuto guards the existing-fleet +// upgrade path: a config written by an older binary carries an explicit +// sc-write-mode = "memiavl_only" but no sc-write-mode-enable-auto key. The absent +// key must default to true so the node resolves to auto and can follow a +// governance-driven migration without any app.toml edit. +func TestGetConfigLegacyMemiavlOnlyResolvesToAuto(t *testing.T) { + v := viper.New() + + v.Set("minimum-gas-prices", DefaultMinGasPrices) + v.Set("telemetry.global-labels", []interface{}{}) + v.Set("state-commit.sc-write-mode", "memiavl_only") + + cfg, err := GetConfig(v) + require.NoError(t, err) + require.True(t, cfg.StateCommit.WriteModeEnableAuto) + require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode, + "absent sc-write-mode-enable-auto must default to true and override an explicit memiavl_only") +} + func TestGetConfigEmptyWriteModeUsesDefault(t *testing.T) { v := viper.New() @@ -417,7 +439,10 @@ func TestDefaultStateCommitConfig(t *testing.T) { require.True(t, cfg.StateCommit.Enable) require.Empty(t, cfg.StateCommit.Directory) - require.Equal(t, sctypes.Auto, cfg.StateCommit.WriteMode) + // 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/store.go b/sei-cosmos/storev2/rootmulti/store.go index a89480d648..e5bcd32efe 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -655,10 +655,12 @@ func (rs *Store) SetInitialVersion(version int64) error { // past the persisted boundary derives migrate_evm directly and the guard // makes the re-fire a no-op. // -// The app calls this from BeginBlock before the block's first write, so the -// empty-buffer precondition of SetWriteMode (which rebuilds the cached -// views) holds. Forwarding the batch size first means the migration router -// built by the transition already observes the new rate. +// 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) @@ -672,12 +674,24 @@ func (rs *Store) SetMigrationBatchSize(batchSize int) error { } // 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. - if configured, _ := rs.ConfiguredWriteMode(); configured != sctypes.Auto { + 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 to auto to participate.", + "height — set sc-write-mode-enable-auto = true (the default) to participate.", "configuredWriteMode", configured, "batchSize", batchSize) return nil } diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index a61b69233a..7a0f70c28e 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -29,17 +29,29 @@ 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 auto: 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). - // Raising the NumKeysToMigratePerBlock gov param above 0 is the sole - // migration trigger — it advances an auto store from memiavl_only to - // migrate_evm. The other values pin a fixed mode and disable derivation. + // 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), makes the node ignore WriteMode + // and run in auto: 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 pin WriteMode (deliberate opt-out; such a node diverges once the chain + // migrates). + WriteModeEnableAuto bool `mapstructure:"write-mode-enable-auto"` + // MemIAVLConfig is the configuration for the MemIAVL (Cosmos) backend MemIAVLConfig memiavl.Config @@ -61,7 +73,8 @@ type StateCommitConfig struct { func DefaultStateCommitConfig() StateCommitConfig { return StateCommitConfig{ Enable: true, - WriteMode: types.Auto, + WriteMode: types.MemiavlOnly, + WriteModeEnableAuto: true, MemIAVLConfig: memiavl.DefaultConfig(), FlatKVConfig: *config.DefaultConfig(), HistoricalProofMaxInFlight: DefaultSCHistoricalProofMaxInFlight, diff --git a/sei-db/config/toml.go b/sei-db/config/toml.go index f8b1cde591..687e141115 100644 --- a/sei-db/config/toml.go +++ b/sei-db/config/toml.go @@ -50,17 +50,21 @@ 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 fixed write routing mode. By default the node ignores +# this value and runs 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. +# +# This value only takes effect for nodes that explicitly opt out of auto via +# the advanced, unrendered state-commit.sc-write-mode-enable-auto = false. Such +# a node pins this mode and diverges from the network once the chain migrates. +# A node whose history began in flatkv_only is the main reason to opt out: it +# must keep sc-write-mode = "flatkv_only", because auto on a flatkv_only node +# either fails every commit with a version-mismatch error or silently serves +# 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 }}" ############################################################################### 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 From 8f43d5be4436645f96ef14eeb1a794088225596d Mon Sep 17 00:00:00 2001 From: YimingZang Date: Tue, 30 Jun 2026 13:47:41 -0700 Subject: [PATCH 15/19] Address comments --- app/abci.go | 10 ++++-- app/abci_test.go | 7 ++-- app/app.go | 2 +- app/migration/params.go | 33 +++++++++++++++++-- app/migration/params_test.go | 11 +++++-- .../gov_module/gov_proposal_test.yaml | 5 +++ 6 files changed, 56 insertions(+), 12 deletions(-) diff --git a/app/abci.go b/app/abci.go index 0a22739def..cc9a111cbd 100644 --- a/app/abci.go +++ b/app/abci.go @@ -4,7 +4,6 @@ import ( "context" "crypto/sha256" "fmt" - "math" "math/big" "time" @@ -83,8 +82,13 @@ func (app *App) applyMigrationBatchSize(ctx sdk.Context) { } subspace.GetIfExists(ctx, migration.KeyNumKeysToMigratePerBlock, &numKeys) } - if numKeys > uint64(math.MaxInt64) { - numKeys = uint64(math.MaxInt64) + // 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 diff --git a/app/abci_test.go b/app/abci_test.go index 91b19b0a75..db6ea5298d 100644 --- a/app/abci_test.go +++ b/app/abci_test.go @@ -49,12 +49,13 @@ func TestApplyMigrationBatchSize(t *testing.T) { got, _ = a.rootStore.GetMigrationBatchSize() require.Equal(t, 500, got) - // Out-of-int64-range values are clamped to MaxInt64 (defensive; gov - // validation only type-checks). + // 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, math.MaxInt64, got) + require.Equal(t, int(migration.MaxNumKeysToMigratePerBlock), got) } // TestBeginBlockAppliesMigrationBatchSize exercises the full BeginBlock path diff --git a/app/app.go b/app/app.go index ed52d17018..abd1cedd6a 100644 --- a/app/app.go +++ b/app/app.go @@ -557,7 +557,7 @@ func New( // root multistore is somehow in use. rootStore, ok := app.CommitMultiStore().(*rootmulti.Store) if !ok { - panic(fmt.Sprintf("unsupported commit multistore %T: expected *storev2_rootmulti.Store", app.CommitMultiStore())) + panic(fmt.Sprintf("unsupported commit multistore %T: expected *rootmulti.Store", app.CommitMultiStore())) } app.rootStore = rootStore diff --git a/app/migration/params.go b/app/migration/params.go index 6d57028dc8..e47dd2c227 100644 --- a/app/migration/params.go +++ b/app/migration/params.go @@ -8,6 +8,14 @@ // 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 ( @@ -28,6 +36,15 @@ var KeyNumKeysToMigratePerBlock = []byte("NumKeysToMigratePerBlock") // 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( @@ -35,11 +52,21 @@ func ParamKeyTable() paramtypes.KeyTable { ) } -// validateNumKeysToMigratePerBlock only type-checks the value; any uint64 is a -// valid (consensus-deterministic) rate, with 0 meaning "paused". +// 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 { - if _, ok := i.(uint64); !ok { + 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 index 2b38732f2d..82b1ebc09f 100644 --- a/app/migration/params_test.go +++ b/app/migration/params_test.go @@ -1,6 +1,7 @@ package migration import ( + "math" "testing" paramtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/params/types" @@ -24,11 +25,17 @@ func TestParamKeyTableRegistersKey(t *testing.T) { } func TestValidateNumKeysToMigratePerBlock(t *testing.T) { - // Any uint64 is valid, including 0 (paused) and large values. - for _, v := range []uint64{0, 1, 1024, 1 << 40} { + // 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/integration_test/gov_module/gov_proposal_test.yaml b/integration_test/gov_module/gov_proposal_test.yaml index 03c746a052..c478315f14 100644 --- a/integration_test/gov_module/gov_proposal_test.yaml +++ b/integration_test/gov_module/gov_proposal_test.yaml @@ -34,6 +34,11 @@ - type: eval expr: NEW_ABCI_PARAM == "true" +# Scope: this case verifies only the governance plumbing for the migration +# rate — that a ParameterChangeProposal lands NumKeysToMigratePerBlock in the +# migration subspace. It intentionally does not assert that keys actually drain +# memiavl->flatkv; the end-to-end migration behavior is covered separately by +# integration_test/contracts/verify_flatkv_evm_migrate.sh. - name: Test migration param change proposal should update NumKeysToMigratePerBlock inputs: # Get the current migration batch size param (defaults to 0 = paused) From 81bccdaa419acc928aa1ba889eb712cf17a88d01 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Tue, 30 Jun 2026 14:45:42 -0700 Subject: [PATCH 16/19] Fix integration test --- app/seidb.go | 17 ++++---- .../scripts/step4_config_override.sh | 40 +++++++++++------- .../rpcnode/scripts/step1_configure_init.sh | 34 ++++++++++----- sei-cosmos/server/config/config.go | 12 +++--- sei-cosmos/server/config/config_test.go | 38 +++++++++++++++++ sei-db/config/sc_config.go | 40 ++++++++++++------ sei-db/config/sc_config_test.go | 41 +++++++++++++++++++ sei-db/config/toml.go | 24 ++++++----- 8 files changed, 184 insertions(+), 62 deletions(-) create mode 100644 sei-db/config/sc_config_test.go diff --git a/app/seidb.go b/app/seidb.go index a8fca8ce00..f419a98a60 100644 --- a/app/seidb.go +++ b/app/seidb.go @@ -115,25 +115,28 @@ 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 derives - // its write mode automatically or honors the explicit sc-write-mode. An - // ABSENT key must keep the default (true): nodes provisioned by older - // binaries carry an explicit sc-write-mode = "memiavl_only" but no + // 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) } - if scConfig.WriteModeEnableAuto { - scConfig.WriteMode = sctypes.Auto - } else if wm := cast.ToString(appOpts.Get(FlagSCWriteMode)); wm != "" { + // 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 { panic(fmt.Sprintf("invalid EVM SS write mode %q: %s", wm, err)) } 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) 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/sei-cosmos/server/config/config.go b/sei-cosmos/server/config/config.go index 42dae81648..61a404e498 100644 --- a/sei-cosmos/server/config/config.go +++ b/sei-cosmos/server/config/config.go @@ -347,16 +347,16 @@ func GetConfig(v *viper.Viper) (Config, error) { } scWriteMode = parsed } - // sc-write-mode-enable-auto (default true) overrides sc-write-mode with - // auto. An absent key keeps the default so older configs (explicit - // memiavl_only, no auto key) still resolve to auto, mirroring app/seidb.go. + // 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") } - if scWriteModeEnableAuto { - scWriteMode = sctypes.Auto - } + scWriteMode = config.ApplyWriteModeAuto(scWriteModeEnableAuto, scWriteMode) flatKVConfig := config.DefaultStateCommitConfig().FlatKVConfig if v.IsSet("state-commit.flatkv.fsync") { diff --git a/sei-cosmos/server/config/config_test.go b/sei-cosmos/server/config/config_test.go index 5617e77b45..c7d628a07f 100644 --- a/sei-cosmos/server/config/config_test.go +++ b/sei-cosmos/server/config/config_test.go @@ -387,6 +387,44 @@ func TestGetConfigLegacyMemiavlOnlyResolvesToAuto(t *testing.T) { "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() diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index 2761c1f2d7..2094ef15a1 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -36,20 +36,23 @@ type StateCommitConfig struct { // pre-migration fallback for a node that has explicitly opted out of auto. WriteMode types.WriteMode `mapstructure:"write-mode"` - // WriteModeEnableAuto, when true (the default), makes the node ignore WriteMode - // and run in auto: 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. + // 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 pin WriteMode (deliberate opt-out; such a node diverges once the chain - // migrates). + // 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 @@ -68,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 @@ -87,11 +87,25 @@ func DefaultStateCommitConfig() StateCommitConfig { 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() { 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 d392a3a56f..42d2e8480f 100644 --- a/sei-db/config/toml.go +++ b/sei-db/config/toml.go @@ -50,18 +50,20 @@ 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 }} -# sc-write-mode is the fixed write routing mode. By default the node ignores -# this value and runs 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. +# 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. # -# This value only takes effect for nodes that explicitly opt out of auto via -# the advanced, unrendered state-commit.sc-write-mode-enable-auto = false. Such -# a node pins this mode and diverges from the network once the chain migrates. -# A node whose history began in flatkv_only is the main reason to opt out: it -# must keep sc-write-mode = "flatkv_only", because auto on a flatkv_only node -# either fails every commit with a version-mismatch error or silently serves -# reads from an empty memiavl. +# 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). # # 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. From f08929b8853706ed3f475acd68407422187f8abf Mon Sep 17 00:00:00 2001 From: YimingZang Date: Tue, 30 Jun 2026 19:11:59 -0700 Subject: [PATCH 17/19] Enhance integration test --- .../gov_module/gov_proposal_test.yaml | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/integration_test/gov_module/gov_proposal_test.yaml b/integration_test/gov_module/gov_proposal_test.yaml index c478315f14..110dea405a 100644 --- a/integration_test/gov_module/gov_proposal_test.yaml +++ b/integration_test/gov_module/gov_proposal_test.yaml @@ -34,16 +34,25 @@ - type: eval expr: NEW_ABCI_PARAM == "true" -# Scope: this case verifies only the governance plumbing for the migration -# rate — that a ParameterChangeProposal lands NumKeysToMigratePerBlock in the -# migration subspace. It intentionally does not assert that keys actually drain -# memiavl->flatkv; the end-to-end migration behavior is covered separately by -# integration_test/contracts/verify_flatkv_evm_migrate.sh. -- name: Test migration param change proposal should update NumKeysToMigratePerBlock +# Scope: this case verifies the governance plumbing for the migration rate — +# that a ParameterChangeProposal lands NumKeysToMigratePerBlock in the migration +# subspace — AND that raising it above 0 actually triggers the migration on an +# auto node. The Gov job boots a default cluster, which now runs in auto mode +# (sc-write-mode-enable-auto defaults to true), so the first BeginBlock after the +# rate goes positive performs the live memiavl_only -> migrate_evm kick-off. We +# assert that transition via the composite store's "write mode transitioned" +# log. The full cross-validator key-drain / digest agreement is still covered +# separately by integration_test/contracts/verify_flatkv_evm_migrate.sh. +- name: Test migration param change proposal should update NumKeysToMigratePerBlock and start migration 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 + # Baseline: how many times the auto memiavl_only -> migrate_evm kick-off has + # been logged so far. On a fresh auto cluster this is 0; capturing it lets us + # attribute the transition below to THIS proposal rather than any prior state. + - cmd: c=$(grep -c 'write mode transitioned.*to=migrate_evm' /sei-protocol/sei-chain/build/generated/logs/seid-0.log 2>/dev/null || true); echo "${c:-0}" + env: OLD_MIGRATION_STARTED # 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 @@ -66,13 +75,26 @@ # 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 + # The kick-off is level-triggered in BeginBlock the block after the rate goes + # positive. Poll node-0's log (up to ~60s) for the composite store's + # memiavl_only -> migrate_evm transition, then report the match count. + - cmd: c=0; for i in $(seq 1 30); do c=$(grep -c 'write mode transitioned.*to=migrate_evm' /sei-protocol/sei-chain/build/generated/logs/seid-0.log 2>/dev/null || true); [ "${c:-0}" -gt 0 ] && break; sleep 2; done; echo "${c:-0}" + env: NEW_MIGRATION_STARTED 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 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 + expr: NEW_PARAM == "12345" + # The auto kick-off must have fired (migration actually started)... + - type: eval + expr: NEW_MIGRATION_STARTED >= 1 + # ...and it must be attributable to this proposal (not pre-existing state). + - type: eval + expr: NEW_MIGRATION_STARTED > OLD_MIGRATION_STARTED - name: Test expedited proposal should respect expedited_voting_period inputs: From 714e95935f3d9bc57c4513c1a44d126d3f92464f Mon Sep 17 00:00:00 2001 From: YimingZang Date: Wed, 1 Jul 2026 08:14:14 -0700 Subject: [PATCH 18/19] Make test idempodent --- .../gov_module/gov_proposal_test.yaml | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/integration_test/gov_module/gov_proposal_test.yaml b/integration_test/gov_module/gov_proposal_test.yaml index 110dea405a..76a934cac7 100644 --- a/integration_test/gov_module/gov_proposal_test.yaml +++ b/integration_test/gov_module/gov_proposal_test.yaml @@ -48,11 +48,6 @@ # 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 - # Baseline: how many times the auto memiavl_only -> migrate_evm kick-off has - # been logged so far. On a fresh auto cluster this is 0; capturing it lets us - # attribute the transition below to THIS proposal rather than any prior state. - - cmd: c=$(grep -c 'write mode transitioned.*to=migrate_evm' /sei-protocol/sei-chain/build/generated/logs/seid-0.log 2>/dev/null || true); echo "${c:-0}" - env: OLD_MIGRATION_STARTED # 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 @@ -78,6 +73,15 @@ # The kick-off is level-triggered in BeginBlock the block after the rate goes # positive. Poll node-0's log (up to ~60s) for the composite store's # memiavl_only -> migrate_evm transition, then report the match count. + # + # NOTE: this transition is a one-time, irreversible event for the life of the + # chain (once a node leaves memiavl_only it never returns). We therefore + # assert only that it has occurred (>= 1), NOT a per-run delta: on a fresh + # cluster this proposal is what drives 0 -> 1, and on a cluster that already + # migrated in a prior run the invariant "the rate is set AND the chain has + # migrated" still holds. A strict "count increased since before this proposal" + # check is not idempotent — it can only ever pass on the very first run + # against a given chain, and fails on every re-run of a long-lived cluster. - cmd: c=0; for i in $(seq 1 30); do c=$(grep -c 'write mode transitioned.*to=migrate_evm' /sei-protocol/sei-chain/build/generated/logs/seid-0.log 2>/dev/null || true); [ "${c:-0}" -gt 0 ] && break; sleep 2; done; echo "${c:-0}" env: NEW_MIGRATION_STARTED verifiers: @@ -89,12 +93,10 @@ # sibling verifiers; the eval engine coerces both sides to big.Int anyway. - type: eval expr: NEW_PARAM == "12345" - # The auto kick-off must have fired (migration actually started)... + # The auto kick-off must have fired at least once: migration has started on + # this chain (see NOTE above for why this is not a per-run delta). - type: eval expr: NEW_MIGRATION_STARTED >= 1 - # ...and it must be attributable to this proposal (not pre-existing state). - - type: eval - expr: NEW_MIGRATION_STARTED > OLD_MIGRATION_STARTED - name: Test expedited proposal should respect expedited_voting_period inputs: From a3c7291c1092f39a9c6614e738e60dbfe9c77701 Mon Sep 17 00:00:00 2001 From: YimingZang Date: Wed, 1 Jul 2026 09:48:45 -0700 Subject: [PATCH 19/19] Fix integration test --- .../gov_module/gov_proposal_test.yaml | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/integration_test/gov_module/gov_proposal_test.yaml b/integration_test/gov_module/gov_proposal_test.yaml index 76a934cac7..23d867bf8d 100644 --- a/integration_test/gov_module/gov_proposal_test.yaml +++ b/integration_test/gov_module/gov_proposal_test.yaml @@ -36,14 +36,13 @@ # Scope: this case verifies the governance plumbing for the migration rate — # that a ParameterChangeProposal lands NumKeysToMigratePerBlock in the migration -# subspace — AND that raising it above 0 actually triggers the migration on an -# auto node. The Gov job boots a default cluster, which now runs in auto mode -# (sc-write-mode-enable-auto defaults to true), so the first BeginBlock after the -# rate goes positive performs the live memiavl_only -> migrate_evm kick-off. We -# assert that transition via the composite store's "write mode transitioned" -# log. The full cross-validator key-drain / digest agreement is still covered -# separately by integration_test/contracts/verify_flatkv_evm_migrate.sh. -- name: Test migration param change proposal should update NumKeysToMigratePerBlock and start 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 "\"" @@ -70,20 +69,6 @@ # 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 - # The kick-off is level-triggered in BeginBlock the block after the rate goes - # positive. Poll node-0's log (up to ~60s) for the composite store's - # memiavl_only -> migrate_evm transition, then report the match count. - # - # NOTE: this transition is a one-time, irreversible event for the life of the - # chain (once a node leaves memiavl_only it never returns). We therefore - # assert only that it has occurred (>= 1), NOT a per-run delta: on a fresh - # cluster this proposal is what drives 0 -> 1, and on a cluster that already - # migrated in a prior run the invariant "the rate is set AND the chain has - # migrated" still holds. A strict "count increased since before this proposal" - # check is not idempotent — it can only ever pass on the very first run - # against a given chain, and fails on every re-run of a long-lived cluster. - - cmd: c=0; for i in $(seq 1 30); do c=$(grep -c 'write mode transitioned.*to=migrate_evm' /sei-protocol/sei-chain/build/generated/logs/seid-0.log 2>/dev/null || true); [ "${c:-0}" -gt 0 ] && break; sleep 2; done; echo "${c:-0}" - env: NEW_MIGRATION_STARTED verifiers: # The proposal must have passed - type: eval @@ -93,10 +78,6 @@ # sibling verifiers; the eval engine coerces both sides to big.Int anyway. - type: eval expr: NEW_PARAM == "12345" - # The auto kick-off must have fired at least once: migration has started on - # this chain (see NOTE above for why this is not a per-run delta). - - type: eval - expr: NEW_MIGRATION_STARTED >= 1 - name: Test expedited proposal should respect expedited_voting_period inputs: