From 1a3fb7c0dfc3a2fd3429bfdb4e3820dcf57f1b07 Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Wed, 7 Oct 2026 16:31:32 +0000 Subject: [PATCH] Refuse pinned write modes that disagree with persisted migration (#4484) A store pinned to a steady-state write mode (`evm_migrated`, `all_migrated_but_bank` or `flatkv_only`) copies the configured mode without looking at the migration state persisted in flatkv. When that state says a migration is still in flight, for example after `Rollback` to a height below a migration's completion, the pinned router and commit info no longer match the data. A `flatkv_only` pin never opens memiavl, so its commit info drops memiavl's stores. An `evm_migrated` pin stops running the in-flight migration, so the next block diverges. Either way it persists across restarts for as long as the pin stays. `resolveCurrentWriteMode`, which `LoadLatest` and, since #4477, `Rollback` both go through, now calls `requirePinnedModeMatchesMigration`. It derives the persisted mode and refuses the pin unless the two match or the persisted mode is `MemiavlOnly`, which is what a store whose history began in flatkv derives. The error tells the operator to re-enable auto mode, and reopening in Auto reconciles memiavl down to the rolled-back height. This builds on #4477 and is based on its branch, so the diff here shows only this change. It touches the same fields as #4475, so whichever lands second needs a small rebase. There is no AppHash change for supported configurations: the genesis-pinned `flatkv_only` localnode and CI clusters carry no migration metadata and are unaffected. New composite tests cover the rollback refusal and recovery in Auto, the in-flight refusal for each steady-state pin, and a genesis-pinned store, and the composite, migration and rootmulti packages pass under `-race`. (cherry picked from commit 8d4ac588a6f8c543f843f2a5ad5d4883745f39e7) --- sei-db/state_db/sc/composite/store.go | 34 ++++++- .../sc/composite/store_migration_test.go | 88 +++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 66393e6d8a..9113e4ab45 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -444,7 +444,8 @@ func (cs *CompositeCommitStore) adoptPersistedWriteMode() error { } // resolveCurrentWriteMode sets cs.currentWriteMode after the backends have been -// opened. For a fixed configured mode this is a copy; for types.Auto the +// opened. For a fixed configured mode this is a copy once +// requirePinnedModeMatchesMigration accepts it; for types.Auto the // mode is derived from the migration metadata persisted in flatkv. A nil // flatkv under types.Auto means the backend was never materialized // (lazy-open found no directory), which is definitionally MemiavlOnly. @@ -462,6 +463,9 @@ func (cs *CompositeCommitStore) adoptPersistedWriteMode() error { // anyway. func (cs *CompositeCommitStore) resolveCurrentWriteMode(closeIdleFlatKV bool) error { if cs.config.WriteMode != types.Auto { + if err := cs.requirePinnedModeMatchesMigration(); err != nil { + return err + } cs.storeWriteMode(cs.config.WriteMode) return nil } @@ -486,6 +490,34 @@ func (cs *CompositeCommitStore) resolveCurrentWriteMode(closeIdleFlatKV bool) er return nil } +// requirePinnedModeMatchesMigration returns an error when the configured +// steady-state write mode disagrees with the migration state persisted in +// flatkv. A store whose history began in flatkv carries no migration +// metadata and derives MemiavlOnly, so that state is accepted for any pin. +func (cs *CompositeCommitStore) requirePinnedModeMatchesMigration() error { + pinned := cs.config.WriteMode + switch pinned { + case types.EVMMigrated, types.AllMigratedButBank, types.FlatKVOnly: + default: + return nil + } + flatKV := cs.loadFlatKV() + if flatKV == nil { + return nil + } + persisted, err := migration.DeriveWriteMode(flatKV) + if err != nil { + return fmt.Errorf("failed to derive persisted write mode: %w", err) + } + if persisted == pinned || persisted == types.MemiavlOnly { + return nil + } + return fmt.Errorf( + "write mode is pinned to %q but the persisted migration state is %q; "+ + "set state-commit.sc-write-mode-enable-auto = true to follow the migration", + pinned, persisted) +} + // loadFlatKV returns the flatKV backend, or nil when none is open. func (cs *CompositeCommitStore) loadFlatKV() flatkv.Store { if store := cs.flatKV.Load(); store != nil { 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 83603ed7cc..4668ade1f6 100644 --- a/sei-db/state_db/sc/composite/store_migration_test.go +++ b/sei-db/state_db/sc/composite/store_migration_test.go @@ -1537,3 +1537,91 @@ func TestMigrateEVMPausedMidMigrationHashesEveryWrite(t *testing.T) { require.NotEqual(t, lattice, hashOf(cs, "evm_lattice"), "after a restart, a paused write to flatkv must still change the lattice hash") } + +// pinnedConfig returns a test config pinned to mode. +func pinnedConfig(mode types.WriteMode) config.StateCommitConfig { + cfg := autoConfig() + cfg.WriteMode = mode + return cfg +} + +// loadLatestError opens the store at dir with cfg and returns the LoadLatest error. +func loadLatestError(t *testing.T, dir string, cfg config.StateCommitConfig) error { + t.Helper() + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + err = cs.LoadLatest() + _ = cs.Close() + return err +} + +// TestComposite_PinnedFlatKVOnly_RefusesRollbackIntoBankMigration rolls a store pinned to FlatKVOnly back +// below the bank migration's completion. The rollback and every later pinned open must fail, and reopening +// in Auto must recover the canonical state at the target and reproduce the rolled-back blocks. +func TestComposite_PinnedFlatKVOnly_RefusesRollbackIntoBankMigration(t *testing.T) { + dir := t.TempDir() + workload := newMigrationWorkload(0xB0A7) + const batch = 8 + + cs := openAutoStore(t, dir, batch) + walkToMigrateBank(t, cs, workload) + target := commitRecorded(t, cs, workload.generateBlock(0, 2, 1, 0, 2)) + rolledBack := commitUntilAtMigrationVersion(t, cs, workload, migration.Version3_FlatKVOnly, 800) + require.NoError(t, cs.Close()) + + pinned := pinnedConfig(types.FlatKVOnly) + cs = openAutoStoreWithConfig(t, dir, pinned, batch) + require.ErrorContains(t, cs.Rollback(target.info.Version), `persisted migration state is "migrate_bank"`) + require.NoError(t, cs.Close()) + require.ErrorContains(t, loadLatestError(t, dir, pinned), `persisted migration state is "migrate_bank"`) + + cs = openAutoStore(t, dir, batch) + defer func() { _ = cs.Close() }() + require.Equal(t, types.MigrateBank, cs.loadWriteMode()) + requireCommitInfoEqual(t, target.info, cs.LastCommitInfo(), "commit info after reopening in Auto") + replayRecorded(t, cs, rolledBack, "replay after reopening in Auto") +} + +// TestComposite_PinnedSteadyStateModes_RefuseInFlightMigration opens a store whose EVM migration is in +// flight with each steady-state pin. Every pin must refuse to load. +func TestComposite_PinnedSteadyStateModes_RefuseInFlightMigration(t *testing.T) { + dir := t.TempDir() + workload := newMigrationWorkload(0xE7E8) + + cs := openAutoStore(t, dir, 8) + for i := 0; i < 12; i++ { + commitRecorded(t, cs, workload.generateBlock(6, 0, 0, 4, 0)) + } + require.NoError(t, cs.SetWriteMode(types.MigrateEVM)) + commitRecorded(t, cs, workload.generateBlock(2, 2, 1, 0, 0)) + require.NoError(t, cs.Close()) + + for _, mode := range []types.WriteMode{types.EVMMigrated, types.AllMigratedButBank, types.FlatKVOnly} { + require.ErrorContains(t, loadLatestError(t, dir, pinnedConfig(mode)), + `persisted migration state is "migrate_evm"`, "pinned %s", mode) + } + + cs = openAutoStore(t, dir, 8) + defer func() { _ = cs.Close() }() + require.Equal(t, types.MigrateEVM, cs.loadWriteMode(), "sanity: the migration is still in flight") +} + +// TestComposite_PinnedFlatKVOnly_GenesisStoreReopensAndRollsBack checks that a store pinned to FlatKVOnly +// from genesis, which has no migration metadata, still reopens and rolls back. +func TestComposite_PinnedFlatKVOnly_GenesisStoreReopensAndRollsBack(t *testing.T) { + dir := t.TempDir() + workload := newMigrationWorkload(0x6E5) + pinned := pinnedConfig(types.FlatKVOnly) + + cs := openAutoStoreWithConfig(t, dir, pinned, 0) + runBlocks(t, cs, workload, 5) + require.NoError(t, cs.Close()) + + cs = openAutoStoreWithConfig(t, dir, pinned, 0) + defer func() { _ = cs.Close() }() + require.Equal(t, int64(5), cs.Version()) + require.NoError(t, cs.Rollback(3)) + require.Equal(t, int64(3), cs.Version()) + require.Equal(t, types.FlatKVOnly, cs.loadWriteMode()) +}