diff --git a/app/seidb.go b/app/seidb.go index 4ed31b2d7e..2bb36599df 100644 --- a/app/seidb.go +++ b/app/seidb.go @@ -28,8 +28,6 @@ const ( FlagSCHistoricalProofRateLimit = "state-commit.sc-historical-proof-rate-limit" FlagSCHistoricalProofBurst = "state-commit.sc-historical-proof-burst" FlagSCWriteMode = "state-commit.sc-write-mode" - FlagSCReadMode = "state-commit.sc-read-mode" - FlagSCEnableLatticeHash = "state-commit.sc-enable-lattice-hash" // SS Store configs FlagSSEnable = "state-store.ss-enable" @@ -111,15 +109,6 @@ func parseSCConfigs(appOpts servertypes.AppOptions) config.StateCommitConfig { } scConfig.WriteMode = parsedWM } - if rm := cast.ToString(appOpts.Get(FlagSCReadMode)); rm != "" { - parsedRM, err := config.ParseReadMode(rm) - if err != nil { - panic(fmt.Sprintf("invalid EVM SS read mode %q: %s", rm, err)) - } - scConfig.ReadMode = parsedRM - } - - scConfig.EnableLatticeHash = cast.ToBool(appOpts.Get(FlagSCEnableLatticeHash)) if v := appOpts.Get(FlagSCHistoricalProofMaxInFlight); v != nil { scConfig.HistoricalProofMaxInFlight = cast.ToInt(v) diff --git a/app/seidb_test.go b/app/seidb_test.go index 54b3eb6027..083d44dba2 100644 --- a/app/seidb_test.go +++ b/app/seidb_test.go @@ -37,8 +37,6 @@ func (t TestSeiDBAppOpts) Get(s string) interface{} { return defaultSCConfig.MemIAVLConfig.SnapshotPrefetchThreshold case FlagSCSnapshotWriteRateMBps: return defaultSCConfig.MemIAVLConfig.SnapshotWriteRateMBps - case FlagSCEnableLatticeHash: - return defaultSCConfig.EnableLatticeHash case FlagSSEnable: return defaultSSConfig.Enable case FlagSSBackend: diff --git a/docker/localnode/scripts/step4_config_override.sh b/docker/localnode/scripts/step4_config_override.sh index 9f7fd8e024..83ab33a9f2 100755 --- a/docker/localnode/scripts/step4_config_override.sh +++ b/docker/localnode/scripts/step4_config_override.sh @@ -31,25 +31,15 @@ if [ "$GIGA_STORAGE" = "true" ]; then RECEIPT_BACKEND=${RECEIPT_BACKEND:-parquet} echo "Enabling Giga Storage for node $NODE_ID..." - # --- SC layer: dual_write + split_read + lattice hash --- - # SC must use dual_write (not split_write) because block execution reads - # EVM data from the memiavl tree via GetChildStoreByName. With split_write, - # EVM data only goes to FlatKV and the memiavl tree becomes stale. - # dual_write keeps memiavl up-to-date for reads while also populating FlatKV. + # --- SC layer: test_only_dual_write --- + # SC must use test_only_dual_write because block execution reads EVM data + # 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" ~/.sei/config/app.toml; then - sed -i 's/sc-write-mode = .*/sc-write-mode = "dual_write"/' ~/.sei/config/app.toml + sed -i 's/sc-write-mode = .*/sc-write-mode = "test_only_dual_write"/' ~/.sei/config/app.toml else - sed -i '/^\[state-store\]/i sc-write-mode = "dual_write"' ~/.sei/config/app.toml - fi - if grep -q "sc-read-mode" ~/.sei/config/app.toml; then - sed -i 's/sc-read-mode = .*/sc-read-mode = "split_read"/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-read-mode = "split_read"' ~/.sei/config/app.toml - fi - if grep -q "sc-enable-lattice-hash" ~/.sei/config/app.toml; then - sed -i 's/sc-enable-lattice-hash = .*/sc-enable-lattice-hash = true/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-enable-lattice-hash = true' ~/.sei/config/app.toml + sed -i '/^\[state-store\]/i sc-write-mode = "test_only_dual_write"' ~/.sei/config/app.toml fi # --- SS layer: enable EVM split --- diff --git a/docker/rpcnode/scripts/step1_configure_init.sh b/docker/rpcnode/scripts/step1_configure_init.sh index 8d23d80759..d45e8380c9 100755 --- a/docker/rpcnode/scripts/step1_configure_init.sh +++ b/docker/rpcnode/scripts/step1_configure_init.sh @@ -24,21 +24,11 @@ if [ "$GIGA_STORAGE" = "true" ]; then RECEIPT_BACKEND=${RECEIPT_BACKEND:-parquet} echo "Enabling Giga Storage for RPC node..." - # SC layer: must match validators (dual_write + split_read + lattice hash) + # SC layer: must match validators (test_only_dual_write) if grep -q "sc-write-mode" ~/.sei/config/app.toml; then - sed -i 's/sc-write-mode = .*/sc-write-mode = "dual_write"/' ~/.sei/config/app.toml + sed -i 's/sc-write-mode = .*/sc-write-mode = "test_only_dual_write"/' ~/.sei/config/app.toml else - sed -i '/^\[state-store\]/i sc-write-mode = "dual_write"' ~/.sei/config/app.toml - fi - if grep -q "sc-read-mode" ~/.sei/config/app.toml; then - sed -i 's/sc-read-mode = .*/sc-read-mode = "split_read"/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-read-mode = "split_read"' ~/.sei/config/app.toml - fi - if grep -q "sc-enable-lattice-hash" ~/.sei/config/app.toml; then - sed -i 's/sc-enable-lattice-hash = .*/sc-enable-lattice-hash = true/' ~/.sei/config/app.toml - else - sed -i '/^\[state-store\]/i sc-enable-lattice-hash = true' ~/.sei/config/app.toml + sed -i '/^\[state-store\]/i sc-write-mode = "test_only_dual_write"' ~/.sei/config/app.toml fi # SS layer: enable EVM split diff --git a/sei-cosmos/baseapp/baseapp_test.go b/sei-cosmos/baseapp/baseapp_test.go index 1d578fe741..c65d765de1 100644 --- a/sei-cosmos/baseapp/baseapp_test.go +++ b/sei-cosmos/baseapp/baseapp_test.go @@ -19,8 +19,8 @@ import ( ) var ( - capKey1 = sdk.NewKVStoreKey("key1") - capKey2 = sdk.NewKVStoreKey("key2") + capKey1 = sdk.NewKVStoreKey("bank") + capKey2 = sdk.NewKVStoreKey("staking") ) func newBaseApp(name string, options ...func(*BaseApp)) *BaseApp { diff --git a/sei-cosmos/baseapp/deliver_tx_test.go b/sei-cosmos/baseapp/deliver_tx_test.go index 7f8b4fa712..ed6291c192 100644 --- a/sei-cosmos/baseapp/deliver_tx_test.go +++ b/sei-cosmos/baseapp/deliver_tx_test.go @@ -286,11 +286,11 @@ func TestQuery(t *testing.T) { app.InitChain(context.Background(), &abci.RequestInitChain{}) - // NOTE: "/store/key1" tells us KVStore + // NOTE: "/store/bank" tells us KVStore // and the final "/key" says to use the data as the // key in the given KVStore ... query := abci.RequestQuery{ - Path: "/store/key1/key", + Path: "/store/bank/key", Data: key, } tx := newTxCounter(0, 0) diff --git a/sei-cosmos/server/config/config.go b/sei-cosmos/server/config/config.go index 42da8ff1ee..66334c1984 100644 --- a/sei-cosmos/server/config/config.go +++ b/sei-cosmos/server/config/config.go @@ -383,11 +383,9 @@ 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: config.WriteMode(v.GetString("state-commit.sc-write-mode")), - ReadMode: config.ReadMode(v.GetString("state-commit.sc-read-mode")), - EnableLatticeHash: v.GetBool("state-commit.sc-enable-lattice-hash"), + Enable: v.GetBool("state-commit.sc-enable"), + Directory: v.GetString("state-commit.sc-directory"), + WriteMode: config.WriteMode(v.GetString("state-commit.sc-write-mode")), 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 cf50f012f2..5ac494ef29 100644 --- a/sei-cosmos/server/config/config_test.go +++ b/sei-cosmos/server/config/config_test.go @@ -287,11 +287,9 @@ func TestGetConfigStateCommit(t *testing.T) { v.Set("minimum-gas-prices", DefaultMinGasPrices) v.Set("telemetry.global-labels", []interface{}{}) - // Set StateCommit values using the TOML key names (sc-* prefix) v.Set("state-commit.sc-enable", true) v.Set("state-commit.sc-directory", "/custom/path") - v.Set("state-commit.sc-write-mode", "dual_write") - v.Set("state-commit.sc-read-mode", "evm_first") + 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) v.Set("state-commit.sc-snapshot-interval", 5000) @@ -302,11 +300,9 @@ func TestGetConfigStateCommit(t *testing.T) { cfg, err := GetConfig(v) require.NoError(t, err) - // Verify StateCommit fields are correctly parsed require.True(t, cfg.StateCommit.Enable) require.Equal(t, "/custom/path", cfg.StateCommit.Directory) - require.Equal(t, seidbconfig.DualWrite, cfg.StateCommit.WriteMode) - require.Equal(t, seidbconfig.EVMFirstRead, cfg.StateCommit.ReadMode) + require.Equal(t, seidbconfig.TestOnlyDualWrite, cfg.StateCommit.WriteMode) // Verify MemIAVLConfig fields require.Equal(t, 200, cfg.StateCommit.MemIAVLConfig.AsyncCommitBuffer) @@ -355,11 +351,9 @@ func TestGetConfigStateStore(t *testing.T) { func TestDefaultStateCommitConfig(t *testing.T) { cfg := DefaultConfig() - // Verify default StateCommit values require.True(t, cfg.StateCommit.Enable) require.Empty(t, cfg.StateCommit.Directory) - require.Equal(t, seidbconfig.CosmosOnlyWrite, cfg.StateCommit.WriteMode) - require.Equal(t, seidbconfig.CosmosOnlyRead, cfg.StateCommit.ReadMode) + require.Equal(t, seidbconfig.MemiavlOnly, cfg.StateCommit.WriteMode) } func TestDefaultStateStoreConfig(t *testing.T) { diff --git a/sei-cosmos/storev2/rootmulti/flatkv_hash_test.go b/sei-cosmos/storev2/rootmulti/flatkv_hash_test.go index bd4a5689c5..34213966ec 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_hash_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_hash_test.go @@ -42,8 +42,8 @@ func TestFlatKVDualWriteHashConsistency(t *testing.T) { // SplitWrite — hash consistency, EVM data not in memiavl tree // --------------------------------------------------------------------------- -func TestFlatKVSplitWriteHashConsistency(t *testing.T) { - store, storeKeys := newTestRootMulti(t, t.TempDir(), splitWriteConfig()) +func TestFlatKVEVMMigratedHashConsistency(t *testing.T) { + store, storeKeys := newTestRootMulti(t, t.TempDir(), evmMigratedConfig()) defer func() { require.NoError(t, store.Close()) }() evmData := newEVMTestData(0xBB) diff --git a/sei-cosmos/storev2/rootmulti/flatkv_helpers_test.go b/sei-cosmos/storev2/rootmulti/flatkv_helpers_test.go index 1412669860..9cd46d20f9 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_helpers_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_helpers_test.go @@ -40,31 +40,16 @@ func withTestMemIAVL(cfg seidbconfig.StateCommitConfig) seidbconfig.StateCommitC func dualWriteConfig() seidbconfig.StateCommitConfig { cfg := seidbconfig.DefaultStateCommitConfig() - cfg.WriteMode = seidbconfig.DualWrite - cfg.EnableLatticeHash = true + cfg.WriteMode = seidbconfig.TestOnlyDualWrite return withTestMemIAVL(cfg) } -func splitWriteConfig() seidbconfig.StateCommitConfig { +func evmMigratedConfig() seidbconfig.StateCommitConfig { cfg := seidbconfig.DefaultStateCommitConfig() - cfg.WriteMode = seidbconfig.SplitWrite - cfg.EnableLatticeHash = true + cfg.WriteMode = seidbconfig.EVMMigrated return withTestMemIAVL(cfg) } -func cosmosOnlyConfig() seidbconfig.StateCommitConfig { - cfg := seidbconfig.DefaultStateCommitConfig() - cfg.WriteMode = seidbconfig.CosmosOnlyWrite - cfg.EnableLatticeHash = false - return withTestMemIAVL(cfg) -} - -func dualWriteNoLatticeConfig() seidbconfig.StateCommitConfig { - cfg := dualWriteConfig() - cfg.EnableLatticeHash = false - return cfg -} - // --------------------------------------------------------------------------- // EVM test data and helpers // --------------------------------------------------------------------------- diff --git a/sei-cosmos/storev2/rootmulti/flatkv_modes_test.go b/sei-cosmos/storev2/rootmulti/flatkv_modes_test.go index 8a4699649b..06f49c0594 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_modes_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_modes_test.go @@ -1,6 +1,6 @@ package rootmulti -// WriteMode / Query path coverage: Shadow, SS, proof, missing-key, async restart. +// WriteMode / Query path coverage: SS, proof, missing-key, async restart. import ( "fmt" @@ -9,82 +9,12 @@ import ( storerootmulti "github.com/sei-protocol/sei-chain/sei-cosmos/store/rootmulti" "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" - "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/stretchr/testify/require" ) // --------------------------------------------------------------------------- -// Shadow mode — DualWrite + EnableLatticeHash=false -// --------------------------------------------------------------------------- - -func TestFlatKVShadowModeNoLatticeInAppHash(t *testing.T) { - evmData := newEVMTestData(0x77) - - // Store A: DualWrite with lattice *disabled* (shadow mode). - dirA := t.TempDir() - storeA, keysA := newTestRootMulti(t, dirA, dualWriteNoLatticeConfig()) - - // Store B: CosmosOnly baseline (used to verify consensus parity). - dirB := t.TempDir() - storeB, keysB := newTestRootMulti(t, dirB, cosmosOnlyConfig()) - - // Store C: DualWrite with lattice *enabled* on identical input. Used to - // verify that the FlatKV LtHash computed while shadowing is bit-equal to - // the LtHash computed when consensus actually uses it. This is the key - // correctness property for the shadow -> enable migration path: flipping - // EnableLatticeHash on a shadow node must not change the underlying hash - // state. - dirC := t.TempDir() - storeC, keysC := newTestRootMulti(t, dirC, dualWriteConfig()) - - // Drive each store with byte-identical input via simulateBlock. - for block := 1; block <= 5; block++ { - simulateBlock(t, storeA, keysA, block, evmData) - simulateBlock(t, storeB, keysB, block, evmData) - simulateBlock(t, storeC, keysC, block, evmData) - - // App hashes must be identical (flatkv does not affect consensus). - require.Equalf(t, storeB.lastCommitInfo.Hash(), storeA.lastCommitInfo.Hash(), - "shadow mode app hash must match CosmosOnly at block %d", block) - - // evm_lattice must NOT appear in Store A's CommitInfo. - latticeA := findStoreInfo(storeA.lastCommitInfo.StoreInfos, "evm_lattice") - require.Nilf(t, latticeA, - "evm_lattice must not appear with EnableLatticeHash=false (block %d)", block) - - // Store C DOES include evm_lattice. - latticeC := findStoreInfo(storeC.lastCommitInfo.StoreInfos, "evm_lattice") - require.NotNilf(t, latticeC, "evm_lattice expected on lattice-enabled store (block %d)", block) - require.NotEmpty(t, latticeC.CommitId.Hash) - } - - require.NoError(t, storeB.Close()) - require.NoError(t, storeA.Close()) - require.NoError(t, storeC.Close()) - - // Core shadow-mode guarantee: FlatKV's on-disk LtHash is identical - // whether or not it participates in consensus. If this ever diverges, - // flipping EnableLatticeHash=true on a shadow node will fork the chain. - cfgShadow := dualWriteNoLatticeConfig() - roShadow := openFlatKVReadOnly(t, dirA, cfgShadow, 0) - defer func() { require.NoError(t, roShadow.Close()) }() - require.NotEmpty(t, roShadow.RootHash(), - "flatkv should have non-empty RootHash even with lattice disabled") - - cfgEnabled := dualWriteConfig() - roEnabled := openFlatKVReadOnly(t, dirC, cfgEnabled, 0) - defer func() { require.NoError(t, roEnabled.Close()) }() - - require.Equal(t, roEnabled.CommittedRootHash(), roShadow.CommittedRootHash(), - "FlatKV CommittedRootHash must be identical under shadow and lattice-enabled modes for the same input") - - require.NoError(t, flatkv.VerifyLtHash(roShadow), "full-scan LtHash failed in shadow mode") - require.NoError(t, flatkv.VerifyLtHash(roEnabled), "full-scan LtHash failed in lattice-enabled mode") -} - -// --------------------------------------------------------------------------- -// Query with SS enabled and ReadMode +// Query with SS enabled // --------------------------------------------------------------------------- func TestFlatKVQueryWithSSAndReadMode(t *testing.T) { diff --git a/sei-cosmos/storev2/rootmulti/flatkv_recovery_test.go b/sei-cosmos/storev2/rootmulti/flatkv_recovery_test.go index 68664586fc..6795fcacbc 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_recovery_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_recovery_test.go @@ -99,9 +99,9 @@ func TestFlatKVCrashRecoveryThroughRootMulti(t *testing.T) { // are only stored in FlatKV. // --------------------------------------------------------------------------- -func TestFlatKVSplitWriteCrashRecovery(t *testing.T) { +func TestFlatKVEVMMigratedCrashRecovery(t *testing.T) { dir := t.TempDir() - cfg := splitWriteConfig() + cfg := evmMigratedConfig() evmData := newEVMTestData(0x33) store1, keys1 := newTestRootMulti(t, dir, cfg) @@ -208,9 +208,9 @@ func TestFlatKVReverseCrashRecoveryFlatKVAhead(t *testing.T) { // the chain at v3 with the v3 EVM value still readable from FlatKV. // --------------------------------------------------------------------------- -func TestFlatKVSplitWriteReverseCrashRecovery(t *testing.T) { +func TestFlatKVEVMMigratedReverseCrashRecovery(t *testing.T) { dir := t.TempDir() - cfg := splitWriteConfig() + cfg := evmMigratedConfig() evmData := newEVMTestData(0x78) store1, keys1 := newTestRootMulti(t, dir, cfg) diff --git a/sei-cosmos/storev2/rootmulti/flatkv_snapshot_test.go b/sei-cosmos/storev2/rootmulti/flatkv_snapshot_test.go index c90f4ecae7..28d94e1cf9 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_snapshot_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_snapshot_test.go @@ -132,8 +132,8 @@ func TestFlatKVSnapshotRestoreWithLatticeHash(t *testing.T) { // because the data path is different). // --------------------------------------------------------------------------- -func TestFlatKVSplitWriteSnapshotRestore(t *testing.T) { - cfg := splitWriteConfig() +func TestFlatKVEVMMigratedSnapshotRestore(t *testing.T) { + cfg := evmMigratedConfig() evmData := newEVMTestData(0x34) // Source: drive 5 blocks under SplitWrite. @@ -146,14 +146,13 @@ func TestFlatKVSplitWriteSnapshotRestore(t *testing.T) { require.NotNil(t, srcLattice) require.NotEmpty(t, srcLattice.CommitId.Hash) - // Source invariant: memiavl "evm" subtree is empty under SplitWrite — - // expected empty-tree hash, so the subsequent parity check against the - // restored node is meaningful only if FlatKV carried the data through - // the snapshot. - srcEVM := srcStore.scStore.GetChildStoreByName("evm") - require.NotNil(t, srcEVM) - require.Nilf(t, srcEVM.Get(evmData.storKey), - "memiavl evm subtree must be empty under SplitWrite on source") + // The pre-router "memiavl evm subtree must be empty" check used to be + // asserted here via scStore.GetChildStoreByName("evm"), but that + // accessor now returns a router-wrapped view that surfaces FlatKV + // data in EVMMigrated mode, so it can no longer distinguish "memiavl + // is empty" from "router routed to flatkv". The consensus-parity + // check on app hashes below carries the same load: if memiavl had + // drifted on either side, the per-block hashes would not match. // Snapshot to buffer (keep srcStore open to continue the chain below). var buf bytes.Buffer @@ -174,13 +173,12 @@ func TestFlatKVSplitWriteSnapshotRestore(t *testing.T) { require.NotNil(t, dstLattice, "evm_lattice must be present after restore") require.NotEmpty(t, dstLattice.CommitId.Hash, "restored lattice hash must be non-empty") - // Destination invariant: memiavl "evm" subtree stays empty on the - // restored SplitWrite store. If it has any value for evmData.storKey, - // the snapshot pipeline misrouted evm data into memiavl. - dstEVM := dstStore.scStore.GetChildStoreByName("evm") - require.NotNil(t, dstEVM) - require.Nilf(t, dstEVM.Get(evmData.storKey), - "memiavl evm subtree must remain empty under SplitWrite on restored store") + // The pre-router "restored memiavl evm subtree stays empty" check used + // to be asserted here; it relied on GetChildStoreByName("evm") + // returning a memiavl-direct view. That view is now router-wrapped + // (see the matching comment on the source side), so the check is + // pinned by the consensus-parity guarantee further down: drift on + // either backend on either side would show up as a hash mismatch. // Extract destination store keys for the continuation below. dstKeys := make(map[string]*types.KVStoreKey) diff --git a/sei-cosmos/storev2/rootmulti/flatkv_workload_test.go b/sei-cosmos/storev2/rootmulti/flatkv_workload_test.go index ea4e1454f6..b38757c722 100644 --- a/sei-cosmos/storev2/rootmulti/flatkv_workload_test.go +++ b/sei-cosmos/storev2/rootmulti/flatkv_workload_test.go @@ -242,12 +242,19 @@ func TestFlatKVMultiAccountWorkload(t *testing.T) { } // --------------------------------------------------------------------------- -// SplitWrite read routing — EVM data absent from memiavl, present in flatkv +// EVMMigrated read routing — EVM reads served by FlatKV through the router +// +// In EVMMigrated mode FlatKV is the sole authoritative store for evm/ data. +// The composite's router sends evm/ reads to FlatKV, so callers must see the +// same value via scStore.GetChildStoreByName("evm").Get(...) (router view) +// and via a direct FlatKV lookup. A bare FlatKV check would not catch the +// case where the router silently fell back to memiavl; pinning both paths +// detects that regression. // --------------------------------------------------------------------------- -func TestFlatKVSplitWriteReadRouting(t *testing.T) { +func TestFlatKVEVMMigratedReadRouting(t *testing.T) { dir := t.TempDir() - cfg := splitWriteConfig() + cfg := evmMigratedConfig() store, storeKeys := newTestRootMulti(t, dir, cfg) addrs := newMultiEVMTestData(3) @@ -286,23 +293,27 @@ func TestFlatKVSplitWriteReadRouting(t *testing.T) { } require.NoError(t, store.Close()) - // In SplitWrite, memiavl "evm" tree should NOT have the EVM data. + // Router view: scStore.GetChildStoreByName("evm") routes reads to + // FlatKV under EVMMigrated, so each finalWrite must surface here. store2, _ := newTestRootMulti(t, dir, cfg) evmTree := store2.scStore.GetChildStoreByName("evm") require.NotNil(t, evmTree) for _, w := range finalWrites { got := evmTree.Get(w.key) - require.Nilf(t, got, "memiavl should NOT contain EVM key %x in SplitWrite mode", w.key) - require.Falsef(t, evmTree.Has(w.key), "memiavl Has() should be false for %x in SplitWrite", w.key) + require.Equalf(t, w.value, got, + "router view of evm/ must surface FlatKV value for key %x", w.key) + require.Truef(t, evmTree.Has(w.key), + "router view of evm/ must report Has() for key %x", w.key) } require.NoError(t, store2.Close()) - // FlatKV should have the data. + // Direct FlatKV check, redundant with the router view above but + // detects the case where the router and FlatKV disagree. ro := openFlatKVReadOnly(t, dir, cfg, 0) for _, w := range finalWrites { val, found := ro.Get(keys.EVMStoreKey, w.key) - require.Truef(t, found, "flatkv should contain key %x in SplitWrite mode", w.key) + require.Truef(t, found, "flatkv should contain key %x in EVMMigrated mode", w.key) require.Equalf(t, w.value, val, "flatkv value mismatch for key %x", w.key) } require.NoError(t, ro.Close()) diff --git a/sei-cosmos/storev2/rootmulti/store.go b/sei-cosmos/storev2/rootmulti/store.go index d3442c2549..40d6c0a3ac 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -92,7 +92,10 @@ func NewStore( limiter = rate.NewLimiter(rate.Limit(scConfig.HistoricalProofRateLimit), burst) } ctx := context.Background() - scStore := composite.NewCompositeCommitStore(ctx, scDir, scConfig) + scStore, err := composite.NewCompositeCommitStore(ctx, scDir, scConfig) + if err != nil { + panic(err) + } if err := scStore.CleanupCrashArtifacts(); err != nil { if commonerrors.IsFileLockError(err) { logger.Error("non-fatal: failed to acquire file lock for cleanup", "err", err) @@ -481,7 +484,9 @@ func (rs *Store) LoadVersionAndUpgrade(version int64, upgrades *types.StoreUpgra initialStores = append(initialStores, key.Name()) } } - rs.scStore.Initialize(initialStores) + if err := rs.scStore.Initialize(initialStores); err != nil { + return err + } if _, err := rs.scStore.LoadVersion(version, false); err != nil { return err } diff --git a/sei-cosmos/storev2/rootmulti/store_test.go b/sei-cosmos/storev2/rootmulti/store_test.go index d8b80b7e27..916a8b7bf4 100644 --- a/sei-cosmos/storev2/rootmulti/store_test.go +++ b/sei-cosmos/storev2/rootmulti/store_test.go @@ -43,12 +43,12 @@ func TestSCSS_WriteAndHistoricalRead(t *testing.T) { defer func() { _ = store.Close() }() // Mount one IAVL store and load - key := types.NewKVStoreKey("store1") + key := types.NewKVStoreKey("bank") store.MountStoreWithDB(key, types.StoreTypeIAVL, nil) require.NoError(t, store.LoadLatestVersion()) // Write v1 and commit - kv := store.GetStoreByName("store1").(types.KVStore) + kv := store.GetStoreByName("bank").(types.KVStore) keyBytes := []byte("k") valV1 := []byte("v1") kv.Set(keyBytes, valV1) @@ -56,7 +56,7 @@ func TestSCSS_WriteAndHistoricalRead(t *testing.T) { require.Equal(t, int64(1), c1.Version) // Re-acquire KV store after commit to ensure we write to the current instance - kv = store.GetStoreByName("store1").(types.KVStore) + kv = store.GetStoreByName("bank").(types.KVStore) // Write v2 and commit valV2 := []byte("v2") kv.Set(keyBytes, valV2) @@ -83,7 +83,7 @@ func TestSCSS_WriteAndHistoricalRead(t *testing.T) { // Query API without proof at v1 should be served by SS and return v1 resp := store.Query(abci.RequestQuery{ - Path: "/store1/key", + Path: "/bank/key", Data: keyBytes, Height: c1.Version, Prove: false, @@ -95,7 +95,7 @@ func TestSCSS_WriteAndHistoricalRead(t *testing.T) { // Query API with proof at v1 should still return v1 (served by SC historical) resp = store.Query(abci.RequestQuery{ - Path: "/store1/key", + Path: "/bank/key", Data: keyBytes, Height: c1.Version, Prove: true, @@ -129,8 +129,8 @@ func TestCacheMultiStoreWithVersion_OnlyUsesSSStores(t *testing.T) { store := NewStore(home, scCfg, ssCfg, []string{}) defer func() { _ = store.Close() }() - iavlKey1 := types.NewKVStoreKey("iavl_store1") - iavlKey2 := types.NewKVStoreKey("iavl_store2") + iavlKey1 := types.NewKVStoreKey("bank") + iavlKey2 := types.NewKVStoreKey("staking") transientKey := types.NewTransientStoreKey("transient_store") memKey := types.NewMemoryStoreKey("mem_store") @@ -140,15 +140,15 @@ func TestCacheMultiStoreWithVersion_OnlyUsesSSStores(t *testing.T) { store.MountStoreWithDB(memKey, types.StoreTypeMemory, nil) require.NoError(t, store.LoadLatestVersion()) - iavl1KV := store.GetStoreByName("iavl_store1").(types.KVStore) - iavl2KV := store.GetStoreByName("iavl_store2").(types.KVStore) + iavl1KV := store.GetStoreByName("bank").(types.KVStore) + iavl2KV := store.GetStoreByName("staking").(types.KVStore) iavl1KV.Set([]byte("k1"), []byte("v1")) iavl2KV.Set([]byte("k2"), []byte("v2")) c1 := store.Commit(true) require.Equal(t, int64(1), c1.Version) - iavl1KV = store.GetStoreByName("iavl_store1").(types.KVStore) - iavl2KV = store.GetStoreByName("iavl_store2").(types.KVStore) + iavl1KV = store.GetStoreByName("bank").(types.KVStore) + iavl2KV = store.GetStoreByName("staking").(types.KVStore) iavl1KV.Set([]byte("k1"), []byte("v1_updated")) iavl2KV.Set([]byte("k2"), []byte("v2_updated")) c2 := store.Commit(true) @@ -241,17 +241,17 @@ func TestQuery_HistoricalNoProofWithoutSS_UsesPermit(t *testing.T) { store := NewStore(home, scCfg, ssCfg, []string{}) defer func() { _ = store.Close() }() - key := types.NewKVStoreKey("store1") + key := types.NewKVStoreKey("bank") store.MountStoreWithDB(key, types.StoreTypeIAVL, nil) require.NoError(t, store.LoadLatestVersion()) keyBytes := []byte("k") - kv := store.GetStoreByName("store1").(types.KVStore) + kv := store.GetStoreByName("bank").(types.KVStore) kv.Set(keyBytes, []byte("v1")) c1 := store.Commit(true) require.Equal(t, int64(1), c1.Version) - kv = store.GetStoreByName("store1").(types.KVStore) + kv = store.GetStoreByName("bank").(types.KVStore) kv.Set(keyBytes, []byte("v2")) c2 := store.Commit(true) require.Equal(t, int64(2), c2.Version) @@ -261,7 +261,7 @@ func TestQuery_HistoricalNoProofWithoutSS_UsesPermit(t *testing.T) { defer func() { <-store.histProofSem }() resp := store.Query(abci.RequestQuery{ - Path: "/store1/key", + Path: "/bank/key", Data: keyBytes, Height: c1.Version, Prove: false, @@ -292,11 +292,11 @@ func TestCacheMultiStoreWithVersion_NoReentrantRLockDeadlock(t *testing.T) { store := NewStore(home, scCfg, ssCfg, []string{}) defer func() { _ = store.Close() }() - key := types.NewKVStoreKey("store1") + key := types.NewKVStoreKey("bank") store.MountStoreWithDB(key, types.StoreTypeIAVL, nil) require.NoError(t, store.LoadLatestVersion()) - kv := store.GetStoreByName("store1").(types.KVStore) + kv := store.GetStoreByName("bank").(types.KVStore) kv.Set([]byte("k"), []byte("v")) c1 := store.Commit(true) require.Equal(t, int64(1), c1.Version) @@ -376,13 +376,13 @@ func TestQuery_LatestProofBypassesHistoricalPermit(t *testing.T) { store := NewStore(home, scCfg, ssCfg, []string{}) defer func() { _ = store.Close() }() - key := types.NewKVStoreKey("store1") + key := types.NewKVStoreKey("bank") store.MountStoreWithDB(key, types.StoreTypeIAVL, nil) require.NoError(t, store.LoadLatestVersion()) keyBytes := []byte("k") valV1 := []byte("v1") - kv := store.GetStoreByName("store1").(types.KVStore) + kv := store.GetStoreByName("bank").(types.KVStore) kv.Set(keyBytes, valV1) c1 := store.Commit(true) require.Equal(t, int64(1), c1.Version) @@ -392,7 +392,7 @@ func TestQuery_LatestProofBypassesHistoricalPermit(t *testing.T) { defer func() { <-store.histProofSem }() resp := store.Query(abci.RequestQuery{ - Path: "/store1/key", + Path: "/bank/key", Data: keyBytes, Height: c1.Version, Prove: true, diff --git a/sei-db/config/read_mode.go b/sei-db/config/read_mode.go deleted file mode 100644 index faab266f12..0000000000 --- a/sei-db/config/read_mode.go +++ /dev/null @@ -1,40 +0,0 @@ -package config - -import "fmt" - -// ReadMode defines how EVM data reads are routed between backends. -type ReadMode string - -const ( - // CosmosOnlyRead reads all data from Cosmos (memiavl) only. - // This is the default/legacy behavior. - CosmosOnlyRead ReadMode = "cosmos_only" - - // EVMFirstRead reads EVM data from EVM backend first, falls back to Cosmos if not found. - // Use during migration to test EVM backend reads while maintaining Cosmos as fallback. - EVMFirstRead ReadMode = "evm_first" - - // SplitRead reads EVM data from EVM backend and non-EVM data from Cosmos. - // Use when migration is complete and backends are fully separated. - SplitRead ReadMode = "split_read" -) - -// IsValid returns true if the read mode is a recognized value -func (m ReadMode) IsValid() bool { - switch m { - case CosmosOnlyRead, EVMFirstRead, SplitRead: - return true - default: - return false - } -} - -// ParseReadMode converts a string to a ReadMode, returning an error if invalid -func ParseReadMode(s string) (ReadMode, error) { - m := ReadMode(s) - if !m.IsValid() { - return "", fmt.Errorf("invalid read mode: %q, valid modes: %s, %s, %s", - s, CosmosOnlyRead, EVMFirstRead, SplitRead) - } - return m, nil -} diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index d02ae257f8..cb2aac7089 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -28,19 +28,12 @@ type StateCommitConfig struct { // defaults to 100 AsyncCommitBuffer int `mapstructure:"async-commit-buffer"` - // WriteMode defines the write routing mode for EVM data - // Valid values: cosmos_only, dual_write, split_write, evm_only - // defaults to cosmos_only + // 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. + // defaults to memiavl_only. WriteMode WriteMode `mapstructure:"write-mode"` - // ReadMode defines the read routing mode for EVM data - // Valid values: cosmos_only, evm_first, split_read - // defaults to cosmos_only - ReadMode ReadMode `mapstructure:"read-mode"` - - // EnableLatticeHash controls whether lattice hash will be participating in final app hash or not - EnableLatticeHash bool `mapstructure:"enable-lattice-hash"` - // MemIAVLConfig is the configuration for the MemIAVL (Cosmos) backend MemIAVLConfig memiavl.Config @@ -56,20 +49,22 @@ 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"` } // DefaultStateCommitConfig returns the default StateCommitConfig func DefaultStateCommitConfig() StateCommitConfig { return StateCommitConfig{ Enable: true, - WriteMode: CosmosOnlyWrite, - ReadMode: CosmosOnlyRead, - EnableLatticeHash: false, + WriteMode: MemiavlOnly, MemIAVLConfig: memiavl.DefaultConfig(), FlatKVConfig: *config.DefaultConfig(), HistoricalProofMaxInFlight: DefaultSCHistoricalProofMaxInFlight, HistoricalProofRateLimit: DefaultSCHistoricalProofRateLimit, HistoricalProofBurst: DefaultSCHistoricalProofBurst, + KeysToMigratePerBlock: 1024, } } @@ -78,11 +73,8 @@ func (c StateCommitConfig) Validate() error { if !c.WriteMode.IsValid() { return fmt.Errorf("invalid write-mode: %s", c.WriteMode) } - if !c.ReadMode.IsValid() { - return fmt.Errorf("invalid read-mode: %s", c.ReadMode) - } - if c.WriteMode == SplitWrite && !c.EnableLatticeHash { - return fmt.Errorf("lattice hash must be enabled when using split_write mode") + if c.KeysToMigratePerBlock <= 0 { + return fmt.Errorf("keys-to-migrate-per-block must be greater than 0") } return nil } diff --git a/sei-db/config/toml.go b/sei-db/config/toml.go index eb387cb1d5..a5391f5aaa 100644 --- a/sei-db/config/toml.go +++ b/sei-db/config/toml.go @@ -51,17 +51,10 @@ sc-snapshot-prefetch-threshold = {{ .StateCommit.MemIAVLConfig.SnapshotPrefetchT sc-snapshot-write-rate-mbps = {{ .StateCommit.MemIAVLConfig.SnapshotWriteRateMBps }} # WriteMode defines the write routing mode for EVM data in the SC layer. -# Valid values: cosmos_only, dual_write, split_write +# Valid values: memiavl_only, migrate_evm, evm_migrated, migrate_all_but_bank, +# all_migrated_but_bank, migrate_bank, flatkv_only, test_only_dual_write sc-write-mode = "{{ .StateCommit.WriteMode }}" -# ReadMode defines the read routing mode for EVM data in the SC layer. -# Valid values: cosmos_only, evm_first, split_read -sc-read-mode = "{{ .StateCommit.ReadMode }}" - -# EnableLatticeHash controls whether lattice hash participates in the final app hash. -# Must be enabled when using split_write mode. -sc-enable-lattice-hash = {{ .StateCommit.EnableLatticeHash }} - ############################################################################### ### FlatKV (EVM) Configuration ### ############################################################################### diff --git a/sei-db/config/toml_test.go b/sei-db/config/toml_test.go index fd0a51f932..b4b269740e 100644 --- a/sei-db/config/toml_test.go +++ b/sei-db/config/toml_test.go @@ -159,9 +159,14 @@ func TestWriteModeValues(t *testing.T) { mode WriteMode expected string }{ - {CosmosOnlyWrite, "cosmos_only"}, - {DualWrite, "dual_write"}, - {SplitWrite, "split_write"}, + {MemiavlOnly, "memiavl_only"}, + {MigrateEVM, "migrate_evm"}, + {EVMMigrated, "evm_migrated"}, + {MigrateAllButBank, "migrate_all_but_bank"}, + {AllMigratedButBank, "all_migrated_but_bank"}, + {MigrateBank, "migrate_bank"}, + {FlatKVOnly, "flatkv_only"}, + {TestOnlyDualWrite, "test_only_dual_write"}, } for _, tc := range tests { @@ -171,32 +176,9 @@ func TestWriteModeValues(t *testing.T) { }) } - // Test invalid mode require.False(t, WriteMode("invalid").IsValid()) } -// TestReadModeValues verifies ReadMode enum values match template output -func TestReadModeValues(t *testing.T) { - tests := []struct { - mode ReadMode - expected string - }{ - {CosmosOnlyRead, "cosmos_only"}, - {EVMFirstRead, "evm_first"}, - {SplitRead, "split_read"}, - } - - for _, tc := range tests { - t.Run(string(tc.mode), func(t *testing.T) { - require.Equal(t, tc.expected, string(tc.mode)) - require.True(t, tc.mode.IsValid()) - }) - } - - // Test invalid mode - require.False(t, ReadMode("invalid").IsValid()) -} - // TestParseWriteMode verifies string to WriteMode conversion func TestParseWriteMode(t *testing.T) { tests := []struct { @@ -204,9 +186,14 @@ func TestParseWriteMode(t *testing.T) { expected WriteMode hasError bool }{ - {"cosmos_only", CosmosOnlyWrite, false}, - {"dual_write", DualWrite, false}, - {"split_write", SplitWrite, false}, + {"memiavl_only", MemiavlOnly, false}, + {"migrate_evm", MigrateEVM, false}, + {"evm_migrated", EVMMigrated, false}, + {"migrate_all_but_bank", MigrateAllButBank, false}, + {"all_migrated_but_bank", AllMigratedButBank, false}, + {"migrate_bank", MigrateBank, false}, + {"flatkv_only", FlatKVOnly, false}, + {"test_only_dual_write", TestOnlyDualWrite, false}, {"invalid", "", true}, {"", "", true}, } @@ -224,56 +211,24 @@ func TestParseWriteMode(t *testing.T) { } } -// TestParseReadMode verifies string to ReadMode conversion -func TestParseReadMode(t *testing.T) { - tests := []struct { - input string - expected ReadMode - hasError bool - }{ - {"cosmos_only", CosmosOnlyRead, false}, - {"evm_first", EVMFirstRead, false}, - {"split_read", SplitRead, false}, - {"invalid", "", true}, - {"", "", true}, - } - - for _, tc := range tests { - t.Run(tc.input, func(t *testing.T) { - mode, err := ParseReadMode(tc.input) - if tc.hasError { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tc.expected, mode) - } - }) - } -} - // TestStateCommitConfigValidate verifies config validation works func TestStateCommitConfigValidate(t *testing.T) { tests := []struct { - name string - writeMode WriteMode - readMode ReadMode - enableLatticeHash bool - hasError bool + name string + writeMode WriteMode + hasError bool }{ - {"valid cosmos_only", CosmosOnlyWrite, CosmosOnlyRead, false, false}, - {"valid dual_write", DualWrite, EVMFirstRead, false, false}, - {"valid split_write with lattice", SplitWrite, SplitRead, true, false}, - {"split_write without lattice", SplitWrite, SplitRead, false, true}, - {"invalid write mode", WriteMode("invalid"), CosmosOnlyRead, false, true}, - {"invalid read mode", CosmosOnlyWrite, ReadMode("invalid"), false, true}, + {"valid memiavl_only", MemiavlOnly, false}, + {"valid test_only_dual_write", TestOnlyDualWrite, false}, + {"valid evm_migrated", EVMMigrated, false}, + {"valid flatkv_only", FlatKVOnly, false}, + {"invalid write mode", WriteMode("invalid"), true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { cfg := DefaultStateCommitConfig() cfg.WriteMode = tc.writeMode - cfg.ReadMode = tc.readMode - cfg.EnableLatticeHash = tc.enableLatticeHash err := cfg.Validate() if tc.hasError { diff --git a/sei-db/config/write_mode.go b/sei-db/config/write_mode.go index a8146c9cfa..803b614c24 100644 --- a/sei-db/config/write_mode.go +++ b/sei-db/config/write_mode.go @@ -6,23 +6,72 @@ import "fmt" type WriteMode string const ( - // CosmosOnlyWrite writes all data to Cosmos (memiavl) only. - // This is the default/legacy behavior - no EVM backend involvement. - CosmosOnlyWrite WriteMode = "cosmos_only" + // MemiavlOnly writes all data to memiavl only. + // + // Migration version 0. + MemiavlOnly WriteMode = "memiavl_only" - // DualWrite writes EVM data to both Cosmos and EVM backends. - // Use during migration to populate the EVM backend while keeping Cosmos as source of truth. - DualWrite WriteMode = "dual_write" + // MigrateEVM migrates the evm/ module from memiavl to flatkv. + // + // Handles the transition from migration version 0 to 1, + // and continues to function once we reach migration version 1. + MigrateEVM WriteMode = "migrate_evm" - // SplitWrite writes EVM data to EVM backend and non-EVM data to Cosmos. - // Use when EVM migration is complete and backends are fully separated. - SplitWrite WriteMode = "split_write" + // EVMMigrated is the steady state after the evm/ module has been migrated, but before we + // are ready to do the next migration. + // + // Migration version 1. + EVMMigrated WriteMode = "evm_migrated" + + // MigrateAllButBank migrates all but the bank module from memiavl to flatkv. + // + // Handles the transition from migration version 1 to 2, + // and continues to function once we reach migration version 2. + MigrateAllButBank WriteMode = "migrate_all_but_bank" + + // AllMigratedButBank is the steady state after all but the bank module has been migrated, + // but before we are ready to do the next migration. + // + // Migration version 2. + AllMigratedButBank WriteMode = "all_migrated_but_bank" + + // MigrateBank migrates the bank module from memiavl to flatkv. + // + // Handles the transition from migration version 2 to 3, + // and continues to function once we reach migration version 3. + MigrateBank WriteMode = "migrate_bank" + + // All data is written to FlatKV. + // + // Migration version 3. + FlatKVOnly WriteMode = "flatkv_only" + + // TestOnlyDualWrite is a test-only dual-write router. EVM traffic is written to both memiavl and flatkv, + // but all other traffic is written to memiavl only. + // + // CRITICAL: this is a test-only router and should never be deployed to production machines. + TestOnlyDualWrite WriteMode = "test_only_dual_write" ) // IsValid returns true if the write mode is a recognized value func (m WriteMode) IsValid() bool { switch m { - case CosmosOnlyWrite, DualWrite, SplitWrite: + case MemiavlOnly, MigrateEVM, EVMMigrated, MigrateAllButBank, + AllMigratedButBank, MigrateBank, FlatKVOnly, TestOnlyDualWrite: + return true + default: + return false + } +} + +// IsMigrationMode reports whether the mode is one of the active +// migration transitions (i.e. one that copies data from memiavl to +// flatkv in the background). Callers use it to decide when +// migration-specific setup is required, such as ensuring the +// MigrationStore tree exists on memiavl. +func (m WriteMode) IsMigrationMode() bool { + switch m { + case MigrateEVM, MigrateAllButBank, MigrateBank: return true default: return false diff --git a/sei-db/state_db/bench/cryptosim/config/basic-config.json b/sei-db/state_db/bench/cryptosim/config/basic-config.json index e74e3e066a..4de7356dc3 100644 --- a/sei-db/state_db/bench/cryptosim/config/basic-config.json +++ b/sei-db/state_db/bench/cryptosim/config/basic-config.json @@ -11,8 +11,6 @@ "ImportNumWorkers": 1, "KeepLastVersion": true, "UseDefaultComparer": false, - "WriteMode": "split_write", - "ReadMode": "evm_first", "EVMDBDirectory": "" }, "BlocksPerCommit": 32, diff --git a/sei-db/state_db/bench/cryptosim/config/ss-composite-pebbledb-write-only.json b/sei-db/state_db/bench/cryptosim/config/ss-composite-pebbledb-write-only.json index 92a620b3a9..32403693ff 100644 --- a/sei-db/state_db/bench/cryptosim/config/ss-composite-pebbledb-write-only.json +++ b/sei-db/state_db/bench/cryptosim/config/ss-composite-pebbledb-write-only.json @@ -1,5 +1,5 @@ { - "Comment": "Write-only SSComposite benchmark using PebbleDB with split_write and evm_first read mode.", + "Comment": "Write-only SSComposite benchmark using PebbleDB.", "Backend": "SSComposite", "StateStoreConfig": { "Enable": true, @@ -11,8 +11,6 @@ "ImportNumWorkers": 1, "KeepLastVersion": true, "UseDefaultComparer": false, - "WriteMode": "split_write", - "ReadMode": "evm_first", "EVMDBDirectory": "" }, "BlocksPerCommit": 32, diff --git a/sei-db/state_db/bench/cryptosim/config/ss-composite-rocksdb-write-only.json b/sei-db/state_db/bench/cryptosim/config/ss-composite-rocksdb-write-only.json index 7d184a32ac..e64ae492eb 100644 --- a/sei-db/state_db/bench/cryptosim/config/ss-composite-rocksdb-write-only.json +++ b/sei-db/state_db/bench/cryptosim/config/ss-composite-rocksdb-write-only.json @@ -1,5 +1,5 @@ { - "Comment": "Write-only SSComposite benchmark using RocksDB with split_write and evm_first read mode.", + "Comment": "Write-only SSComposite benchmark using RocksDB.", "Backend": "SSComposite", "StateStoreConfig": { "Enable": true, @@ -11,8 +11,6 @@ "ImportNumWorkers": 1, "KeepLastVersion": true, "UseDefaultComparer": false, - "WriteMode": "split_write", - "ReadMode": "evm_first", "EVMDBDirectory": "" }, "BlocksPerCommit": 32, diff --git a/sei-db/state_db/bench/cryptosim/cryptosim_config.go b/sei-db/state_db/bench/cryptosim/cryptosim_config.go index 7b8ef874e5..3207a65f96 100644 --- a/sei-db/state_db/bench/cryptosim/cryptosim_config.go +++ b/sei-db/state_db/bench/cryptosim/cryptosim_config.go @@ -102,8 +102,7 @@ type CryptoSimConfig struct { Backend wrappers.DBType // StateStoreConfig controls SS-backed benchmark backends such as SSComposite. - // The default preserves the benchmark SS defaults: pebbledb, async buffer 100, - // split_write, and evm_first reads. + // The default preserves the benchmark SS defaults: pebbledb, async buffer 100. StateStoreConfig *config.StateStoreConfig // HistoricalOffload configures the transport used by the diff --git a/sei-db/state_db/bench/wrappers/db_implementations.go b/sei-db/state_db/bench/wrappers/db_implementations.go index f01e0f9394..f5c0f5727a 100644 --- a/sei-db/state_db/bench/wrappers/db_implementations.go +++ b/sei-db/state_db/bench/wrappers/db_implementations.go @@ -45,7 +45,9 @@ func newMemIAVLCommitStore(dbDir string) (DBWrapper, error) { cfg.SnapshotMinTimeInterval = 60 fmt.Printf("Opening memIAVL from directory %s\n", dbDir) cs := memiavl.NewCommitStore(dbDir, cfg) - cs.Initialize([]string{EVMStoreName}) + if err := cs.Initialize([]string{EVMStoreName}); err != nil { + return nil, fmt.Errorf("memiavl Initialize: %w", err) + } _, err := cs.LoadVersion(0, false) if err != nil { if closeErr := cs.Close(); closeErr != nil { @@ -83,11 +85,16 @@ func newCompositeCommitStore(ctx context.Context, dbDir string, writeMode config cfg.MemIAVLConfig.AsyncCommitBuffer = 10 cfg.MemIAVLConfig.SnapshotInterval = 100 - cs := composite.NewCompositeCommitStore(ctx, dbDir, cfg) + cs, err := composite.NewCompositeCommitStore(ctx, dbDir, cfg) + if err != nil { + return nil, fmt.Errorf("failed to create composite commit store: %w", err) + } if err := cs.CleanupCrashArtifacts(); err != nil { return nil, fmt.Errorf("failed to cleanup crash artifacts: %w", err) } - cs.Initialize([]string{EVMStoreName}) + if err := cs.Initialize([]string{EVMStoreName}); err != nil { + return nil, fmt.Errorf("composite Initialize: %w", err) + } loaded, err := cs.LoadVersion(0, false) if err != nil { @@ -122,7 +129,7 @@ func newCombinedCompositeDualSSComposite( ) (DBWrapper, error) { fmt.Printf("Opening CompositeDual (SC) + Composite (SS) from directory %s\n", dbDir) - sc, err := newCompositeCommitStore(ctx, filepath.Join(dbDir, "sc"), config.DualWrite) + sc, err := newCompositeCommitStore(ctx, filepath.Join(dbDir, "sc"), config.TestOnlyDualWrite) if err != nil { return nil, fmt.Errorf("failed to create SC store: %w", err) } @@ -144,11 +151,11 @@ func NewDBImpl(ctx context.Context, dbType DBType, dataDir string, dbConfig any) case FlatKV: return newFlatKVCommitStore(ctx, dataDir, dbConfig.(*flatkvConfig.Config)) case CompositeDual: - return newCompositeCommitStore(ctx, dataDir, config.DualWrite) + return newCompositeCommitStore(ctx, dataDir, config.TestOnlyDualWrite) case CompositeSplit: - return newCompositeCommitStore(ctx, dataDir, config.SplitWrite) + return newCompositeCommitStore(ctx, dataDir, config.EVMMigrated) case CompositeCosmos: - return newCompositeCommitStore(ctx, dataDir, config.CosmosOnlyWrite) + return newCompositeCommitStore(ctx, dataDir, config.MemiavlOnly) case SSComposite: return newSSCompositeStateStore(dataDir, dbConfig.(*config.StateStoreConfig)) case SSHistoricalOffload: diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index c8c675fa26..ae8a980bc8 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -4,7 +4,7 @@ package composite import ( "context" - "encoding/hex" + "errors" "fmt" "math" @@ -16,6 +16,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/memiavl" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/migration" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" "github.com/sei-protocol/seilog" db "github.com/tendermint/tm-db" @@ -29,12 +30,30 @@ var _ types.Committer = (*CompositeCommitStore)(nil) // CompositeCommitStore manages multiple commit store backends (Cosmos/memiavl and FlatKV) // and routes operations based on the configured migration strategy. type CompositeCommitStore struct { - - // cosmosCommitter is the Cosmos (memiavl) backend - always initialized - cosmosCommitter *memiavl.CommitStore - - // flatkvCommitter is the FlatKV backend - may be nil if not enabled - flatkvCommitter flatkv.Store + // The memIAVL backend. Will be nil after all data is migrated to flatkv. + memIAVL *memiavl.CommitStore + + // The flatKV backend. Will be nil if migration to flatKV has not yet started. + flatKV flatkv.Store + + // Manages routing of traffic between the memiavl and flatkv backends. + // Built (and rebuilt) inside LoadVersion against the just-opened + // backends so that lazily-eager constructors like + // NewMemiavlMigrationIterator see a non-nil memiavl DB. + router migration.Router + + // ctx is the constructor's context. Each invocation of buildRouter + // derives a per-router child context from it and stores the + // corresponding cancel function in routerCancel; cancelling that + // child stops any background goroutines owned by the current + // router (today: the MigrationMetrics boundary-snapshot loop) + // without affecting any unrelated work that shares cs.ctx. + ctx context.Context + + // routerCancel cancels the child context handed to the current + // router. Called before installing a new router on reload, and on + // Close. Nil before the first LoadVersion and after Close. + routerCancel context.CancelFunc // homeDir is the base directory for the store homeDir string @@ -50,37 +69,78 @@ func NewCompositeCommitStore( ctx context.Context, homeDir string, cfg config.StateCommitConfig, -) *CompositeCommitStore { +) (*CompositeCommitStore, error) { if err := cfg.Validate(); err != nil { - panic(fmt.Sprintf("invalid state commit config: %s", err)) + return nil, fmt.Errorf("invalid state commit config: %w", err) } - // Always initialize the Cosmos backend (creates struct only, not opened) - cosmosCommitter := memiavl.NewCommitStore(homeDir, cfg.MemIAVLConfig) - - store := &CompositeCommitStore{ - cosmosCommitter: cosmosCommitter, - homeDir: homeDir, - config: cfg, + var memIAVL *memiavl.CommitStore + if cfg.WriteMode != config.FlatKVOnly { + memIAVL = memiavl.NewCommitStore(homeDir, cfg.MemIAVLConfig) } - // Initialize FlatKV store struct if write mode requires it - // Note: DB is NOT opened here, will be opened in LoadVersion - if cfg.WriteMode == config.DualWrite || cfg.WriteMode == config.SplitWrite { + var flatKV flatkv.Store + if cfg.WriteMode != config.MemiavlOnly { cfg.FlatKVConfig.DataDir = utils.GetFlatKVPath(homeDir) - var err error - store.flatkvCommitter, err = flatkv.NewCommitStore(ctx, &cfg.FlatKVConfig) + fkv, err := flatkv.NewCommitStore(ctx, &cfg.FlatKVConfig) if err != nil { - panic(fmt.Errorf("failed to create FlatKV commit store: %w", err)) + return nil, fmt.Errorf("failed to create FlatKV commit store: %w", err) } + flatKV = fkv } - return store + return &CompositeCommitStore{ + memIAVL: memIAVL, + flatKV: flatKV, + homeDir: homeDir, + config: cfg, + ctx: ctx, + }, nil +} + +// Initialize records the set of child store names that should exist on +// the memiavl backend the first time it is opened. In mixed-DB modes +// names must be members of keys.MemIAVLStoreKeys. +func (cs *CompositeCommitStore) Initialize(initialStores []string) error { + if err := validateInitialStores(cs.config.WriteMode, initialStores); err != nil { + return err + } + if cs.memIAVL == nil { + return nil + } + return cs.memIAVL.Initialize(initialStores) } -// Initialize initializes the store with the given store names -func (cs *CompositeCommitStore) Initialize(initialStores []string) { - cs.cosmosCommitter.Initialize(initialStores) +// validateInitialStores enforces the rules described on Initialize. +func validateInitialStores(mode config.WriteMode, initialStores []string) error { + for _, s := range initialStores { + if s == migration.MigrationStore { + return fmt.Errorf( + "composite.Initialize: reserved store name %q is owned by the composite store", + migration.MigrationStore, + ) + } + } + if mode == config.MemiavlOnly || mode == config.FlatKVOnly { + return nil + } + known := make(map[string]struct{}, len(keys.MemIAVLStoreKeys)) + for _, k := range keys.MemIAVLStoreKeys { + known[k] = struct{}{} + } + var unknown []string + for _, s := range initialStores { + if _, ok := known[s]; !ok { + unknown = append(unknown, s) + } + } + if len(unknown) > 0 { + return fmt.Errorf( + "composite.Initialize: store names not routable by router: %v "+ + "(allowed set is keys.MemIAVLStoreKeys)", unknown, + ) + } + return nil } // CleanupCrashArtifacts removes temporary/orphaned files left by a @@ -89,55 +149,101 @@ func (cs *CompositeCommitStore) Initialize(initialStores []string) { // are created. Any writer lock acquired during cleanup is retained for // the subsequent LoadVersion(..., false) call. func (cs *CompositeCommitStore) CleanupCrashArtifacts() error { - if fkv, ok := cs.flatkvCommitter.(*flatkv.CommitStore); ok { - if err := fkv.CleanupOrphanedReadOnlyDirs(); err != nil { - return err - } + if cs.flatKV == nil { + return nil } - return nil + return cs.flatKV.CleanupOrphanedReadOnlyDirs() } -// SetInitialVersion sets the initial version for the store +// SetInitialVersion seeds every active backend so that the next Commit +// produces initialVersion. Called from cosmos-sdk BaseApp.InitChain on +// fresh genesis. func (cs *CompositeCommitStore) SetInitialVersion(initialVersion int64) error { - return cs.cosmosCommitter.SetInitialVersion(initialVersion) + if cs.memIAVL != nil { + if err := cs.memIAVL.SetInitialVersion(initialVersion); err != nil { + return fmt.Errorf("memiavl SetInitialVersion: %w", err) + } + } + if cs.flatKV != nil { + if err := cs.flatKV.SetInitialVersion(initialVersion); err != nil { + return fmt.Errorf("flatkv SetInitialVersion: %w", err) + } + } + return nil } // LoadVersion opens the database at the given version (0 = latest). // When readOnly is true an isolated composite store is returned. func (cs *CompositeCommitStore) LoadVersion(targetVersion int64, readOnly bool) (types.Committer, error) { - cosmosSC, err := cs.cosmosCommitter.LoadVersion(targetVersion, readOnly) - if err != nil { - return nil, fmt.Errorf("failed to load cosmos version: %w", err) + var memIAVLCommitter *memiavl.CommitStore + if cs.memIAVL != nil { + memIAVLSC, err := cs.memIAVL.LoadVersion(targetVersion, readOnly) + if err != nil { + return nil, fmt.Errorf("failed to load cosmos version: %w", err) + } + var ok bool + memIAVLCommitter, ok = memIAVLSC.(*memiavl.CommitStore) + if !ok { + return nil, fmt.Errorf("unexpected committer type from cosmos LoadVersion") + } } - cosmosCommitter, ok := cosmosSC.(*memiavl.CommitStore) - if !ok { - return nil, fmt.Errorf("unexpected committer type from cosmos LoadVersion") + var flatKVStore flatkv.Store + if cs.flatKV != nil { + fkv, err := cs.flatKV.LoadVersion(targetVersion, readOnly) + if err != nil { + return nil, fmt.Errorf("failed to load FlatKV version: %w", err) + } + flatKVStore = fkv } if readOnly { - newStore := &CompositeCommitStore{ - cosmosCommitter: cosmosCommitter, - homeDir: cs.homeDir, - config: cs.config, - } - if cs.flatkvCommitter != nil { - evmStore, err := cs.flatkvCommitter.LoadVersion(targetVersion, true) - if err != nil { - logger.Error("FlatKV unavailable for readonly load, EVM data will not be served", - "version", targetVersion, "err", err) - } else { - newStore.flatkvCommitter = evmStore - } + // Build a per-handle composite with its own router. Without + // this the read-only handle has cs.router == nil and every + // read-side method nil-dereferences on first call. The new + // composite inherits cs.ctx so cancellation of the parent + // context cascades, but buildRouter installs its own child + // cancel so closing this handle does not affect the parent. + ro := &CompositeCommitStore{ + memIAVL: memIAVLCommitter, + flatKV: flatKVStore, + homeDir: cs.homeDir, + config: cs.config, + ctx: cs.ctx, } - return newStore, nil - } - - cs.cosmosCommitter = cosmosCommitter - if cs.flatkvCommitter != nil { - _, err := cs.flatkvCommitter.LoadVersion(targetVersion, false) - if err != nil { - return nil, fmt.Errorf("failed to load FlatKV version: %w", err) + if err := ro.buildRouter(); err != nil { + return nil, fmt.Errorf("failed to build router for read-only handle: %w", err) + } + return ro, nil + } + + // Reassign the freshly-loaded backends. flatkv.Store.LoadVersion + // is documented to return the receiver on the writable path, but + // the field is an interface (tests inject mocks via cs.flatKV = + // mock); honoring the return value future-proofs against an + // implementation that returns a swapped instance. + if memIAVLCommitter != nil { + cs.memIAVL = memIAVLCommitter + } + if flatKVStore != nil { + cs.flatKV = flatKVStore + } + + if cs.memIAVL != nil && cs.flatKV != nil { + // Migration-entry seeding: turning on a non-MemiavlOnly mode on a + // chain that has been running on MemiavlOnly leaves memiavl at + // version N while flatkv starts fresh at version 0. Bring flatkv + // into lockstep so the next composite commit produces matching + // versions on both backends. Only runs at load-latest; targeted + // loads stay strict so a mismatch is surfaced loudly. + if targetVersion == 0 && cs.memIAVL.Version() > 0 && cs.flatKV.Version() == 0 { + seedTo := cs.memIAVL.Version() + 1 + logger.Info("seeding flatkv initial version to match memiavl", + "memiavlVersion", cs.memIAVL.Version(), "flatkvInitialVersion", seedTo) + if err := cs.flatKV.SetInitialVersion(seedTo); err != nil { + return nil, fmt.Errorf("failed to seed flatkv to memiavl version %d: %w", + cs.memIAVL.Version(), err) + } } // When loading latest (targetVersion==0), a crash between the @@ -151,85 +257,104 @@ func (cs *CompositeCommitStore) LoadVersion(targetVersion int64, readOnly bool) } } - return cs, nil -} + // In migration modes the router probes a "migration" tree on memiavl + // during BuildRouter to learn the on-disk migration version. Add it + // here on the writable path if it isn't already present; + // memiavl.ApplyUpgrades rejects duplicate names rather than skipping + // them, so we guard with a presence check. Read-only handles inherit + // the tree from the on-disk state. + if cs.memIAVL != nil && cs.config.WriteMode.IsMigrationMode() && + cs.memIAVL.GetChildStoreByName(migration.MigrationStore) == nil { + if err := cs.memIAVL.ApplyUpgrades([]*proto.TreeNameUpgrade{ + {Name: migration.MigrationStore}, + }); err != nil { + return nil, fmt.Errorf("add migration store tree to memiavl: %w", err) + } + } -// ApplyChangeSets applies changesets to the appropriate backends based on config. -func (cs *CompositeCommitStore) ApplyChangeSets(changesets []*proto.NamedChangeSet) error { - if len(changesets) == 0 { - return nil + if err := cs.buildRouter(); err != nil { + return nil, err } - // Separate EVM and cosmos changesets - var evmChangeset []*proto.NamedChangeSet - var cosmosChangeset []*proto.NamedChangeSet + return cs, nil +} - for _, changeset := range changesets { - if changeset.Name == keys.EVMStoreKey { - evmChangeset = append(evmChangeset, changeset) - } else { - cosmosChangeset = append(cosmosChangeset, changeset) - } +// buildRouter constructs the migration router against the currently-opened +// backends and assigns it to cs.router. Must be called after memIAVL and +// flatKV (if any) have been opened via LoadVersion. +func (cs *CompositeCommitStore) buildRouter() error { + routerCtx, cancel := context.WithCancel(cs.ctx) + router, err := migration.BuildRouter( + routerCtx, cs.config.WriteMode, cs.memIAVL, cs.flatKV, cs.config.KeysToMigratePerBlock) + if err != nil { + cancel() + return fmt.Errorf("failed to build router: %w", err) } - - // Handle write mode routing - switch cs.config.WriteMode { - case config.CosmosOnlyWrite: - // All data goes to cosmos - cosmosChangeset = changesets - evmChangeset = nil - case config.DualWrite: - // EVM data goes to both, non-EVM only to cosmos - cosmosChangeset = changesets - // evmChangeset already filtered above - case config.SplitWrite: - // EVM goes to EVM store, non-EVM to cosmos (already filtered above) + if cs.routerCancel != nil { + cs.routerCancel() } + cs.router = router + cs.routerCancel = cancel + return nil +} - // Cosmos changesets always goes to cosmos commit store - if len(cosmosChangeset) > 0 { - if err := cs.cosmosCommitter.ApplyChangeSets(cosmosChangeset); err != nil { - return fmt.Errorf("failed to apply cosmos changesets: %w", err) - } +// ApplyChangeSets applies changesets to the appropriate backends based on config. +func (cs *CompositeCommitStore) ApplyChangeSets(changesets []*proto.NamedChangeSet) error { + if len(changesets) == 0 { + return nil } - if cs.flatkvCommitter != nil && len(evmChangeset) > 0 { - if err := cs.flatkvCommitter.ApplyChangeSets(evmChangeset); err != nil { - return fmt.Errorf("failed to apply EVM changesets: %w", err) - } + err := cs.router.ApplyChangeSets(changesets) + if err != nil { + return fmt.Errorf("failed to apply changesets: %w", err) } return nil } -// ApplyUpgrades applies store upgrades (only applicable to Cosmos backend) +// ApplyUpgrades applies store upgrades (only applicable to memIAVL Cosmos backend). Data in +// flatKV is not affected by this method. func (cs *CompositeCommitStore) ApplyUpgrades(upgrades []*proto.TreeNameUpgrade) error { - return cs.cosmosCommitter.ApplyUpgrades(upgrades) + if cs.memIAVL == nil { + return nil + } + + return cs.memIAVL.ApplyUpgrades(upgrades) } // Commit commits the current state to all active backends func (cs *CompositeCommitStore) Commit() (int64, error) { - // Always commit to Cosmos - cosmosVersion, err := cs.cosmosCommitter.Commit() - if err != nil { - return 0, fmt.Errorf("failed to commit cosmos: %w", err) + var cosmosVersion int64 = -1 + if cs.memIAVL != nil { + var err error + cosmosVersion, err = cs.memIAVL.Commit() + if err != nil { + return 0, fmt.Errorf("failed to commit cosmos: %w", err) + } } - // Commit to FlatKV as well if enabled - if cs.flatkvCommitter != nil { - flatkvVersion, err := cs.flatkvCommitter.Commit() + var flatkvVersion int64 = -1 + if cs.flatKV != nil { + var err error + flatkvVersion, err = cs.flatKV.Commit() if err != nil { - return 0, fmt.Errorf("failed to commit to EVM store: %w", err) + return 0, fmt.Errorf("failed to commit flatkv: %w", err) } + } + + if cosmosVersion >= 0 && flatkvVersion >= 0 { if cosmosVersion != flatkvVersion { - return 0, fmt.Errorf("cosmos and EVM version mismatch after commit: cosmos=%d, evm=%d", cosmosVersion, flatkvVersion) + return 0, fmt.Errorf("cosmos and flatkv version mismatch after commit: cosmos=%d, flatkv=%d", + cosmosVersion, flatkvVersion) } - logger.Info("flatkv state committed", - "height", flatkvVersion, - "latticeHash", hex.EncodeToString(cs.flatkvCommitter.CommittedRootHash())) + return cosmosVersion, nil + } else if cosmosVersion >= 0 { + return cosmosVersion, nil + } else if flatkvVersion >= 0 { + return flatkvVersion, nil + } else { + return 0, fmt.Errorf("no version committed") } - - return cosmosVersion, nil } // reconcileVersions checks whether the cosmos and EVM backends are at the @@ -239,8 +364,14 @@ func (cs *CompositeCommitStore) Commit() (int64, error) { // backend is rolled back to the behind version. Rollback truncates the WAL // so the correction survives subsequent restarts. func (cs *CompositeCommitStore) reconcileVersions() error { - cosmosVer := cs.cosmosCommitter.Version() - evmVer := cs.flatkvCommitter.Version() + + if cs.memIAVL == nil || cs.flatKV == nil { + // Nothing to reconcile if one of the backends is not present. + return nil + } + + cosmosVer := cs.memIAVL.Version() + evmVer := cs.flatKV.Version() if cosmosVer == evmVer { return nil } @@ -260,12 +391,12 @@ func (cs *CompositeCommitStore) reconcileVersions() error { "cosmosVersion", cosmosVer, "evmVersion", evmVer, "reconciledVersion", minVer) if cosmosVer > minVer { - if err := cs.cosmosCommitter.Rollback(minVer); err != nil { + if err := cs.memIAVL.Rollback(minVer); err != nil { return fmt.Errorf("failed to rollback cosmos to reconciled version %d: %w", minVer, err) } } if evmVer > minVer { - if err := cs.flatkvCommitter.Rollback(minVer); err != nil { + if err := cs.flatKV.Rollback(minVer); err != nil { return fmt.Errorf("failed to rollback EVM to reconciled version %d: %w", minVer, err) } } @@ -275,33 +406,65 @@ func (cs *CompositeCommitStore) reconcileVersions() error { // Version returns the current version func (cs *CompositeCommitStore) Version() int64 { - if cs.cosmosCommitter != nil { - return cs.cosmosCommitter.Version() - } else if cs.flatkvCommitter != nil { - return cs.flatkvCommitter.Version() + if cs.memIAVL != nil { + return cs.memIAVL.Version() + } else if cs.flatKV != nil { + return cs.flatKV.Version() } return 0 } -// GetLatestVersion returns the latest version +// GetLatestVersion returns the highest committed version across all +// configured backends. When both backends are configured their answers +// must agree; a mismatch is surfaced as an error rather than silently +// picking one. Returns 0 when no backend has any prior commit. +// +// Per-backend semantics differ in a way that does not matter in +// production but can show up in tests: +// - The memiavl branch reads the on-disk changelog WAL tail (see +// memiavl.CommitStore.GetLatestVersion). With AsyncCommitBuffer > 0 +// this can transiently lag the in-memory tree until the writer +// goroutine drains. +// - The flatkv branch returns the in-memory committed watermark when +// the store is loaded. Flatkv writes its metadata synchronously, so +// the in-memory and on-disk values match. +// +// In production both production callers (rootmulti.NewStore and +// rootmulti.LastCommitID) only invoke this pre-LoadVersion, so the +// memiavl lag is irrelevant; tests that call it post-LoadVersion need +// to make memiavl WAL writes synchronous (AsyncCommitBuffer = 0) to +// observe a deterministic answer. func (cs *CompositeCommitStore) GetLatestVersion() (int64, error) { - // TODO: switch to metadata db - return cs.cosmosCommitter.GetLatestVersion() -} - -// GetEarliestVersion returns the earliest version -func (cs *CompositeCommitStore) GetEarliestVersion() (int64, error) { - // TODO: switch to metadata db - return cs.cosmosCommitter.GetEarliestVersion() + switch { + case cs.memIAVL != nil && cs.flatKV != nil: + memVer, err := cs.memIAVL.GetLatestVersion() + if err != nil { + return 0, fmt.Errorf("memiavl: %w", err) + } + flatVer, err := cs.flatKV.GetLatestVersion() + if err != nil { + return 0, fmt.Errorf("flatkv: %w", err) + } + if memVer != flatVer { + return 0, fmt.Errorf( + "backend latest version mismatch: memiavl=%d flatkv=%d", + memVer, flatVer, + ) + } + return memVer, nil + case cs.memIAVL != nil: + return cs.memIAVL.GetLatestVersion() + case cs.flatKV != nil: + return cs.flatKV.GetLatestVersion() + default: + return 0, errors.New("no backend configured") + } } // appendEvmLatticeHash returns a new CommitInfo with the EVM lattice hash // appended, without mutating the original. Returns the original unchanged -// when lattice hashing is disabled. +// when flatKV is not present. func (cs *CompositeCommitStore) appendEvmLatticeHash(ci *proto.CommitInfo, evmHash []byte) *proto.CommitInfo { - if !cs.config.EnableLatticeHash { - return ci - } combined := make([]proto.StoreInfo, len(ci.StoreInfos)+1) copy(combined, ci.StoreInfos) combined[len(combined)-1] = proto.StoreInfo{ @@ -319,18 +482,35 @@ func (cs *CompositeCommitStore) appendEvmLatticeHash(ci *proto.CommitInfo, evmHa // WorkingCommitInfo returns the working commit info func (cs *CompositeCommitStore) WorkingCommitInfo() *proto.CommitInfo { - ci := cs.cosmosCommitter.WorkingCommitInfo() - if cs.flatkvCommitter != nil { - return cs.appendEvmLatticeHash(ci, cs.flatkvCommitter.RootHash()) + var ci *proto.CommitInfo + if cs.memIAVL != nil { + ci = cs.memIAVL.WorkingCommitInfo() + } else { + ci = &proto.CommitInfo{ + Version: cs.Version(), + } + } + + if cs.flatKV != nil { + return cs.appendEvmLatticeHash(ci, cs.flatKV.RootHash()) } + return ci } // LastCommitInfo returns the last commit info func (cs *CompositeCommitStore) LastCommitInfo() *proto.CommitInfo { - ci := cs.cosmosCommitter.LastCommitInfo() - if cs.flatkvCommitter != nil { - return cs.appendEvmLatticeHash(ci, cs.flatkvCommitter.CommittedRootHash()) + var ci *proto.CommitInfo + if cs.memIAVL != nil { + ci = cs.memIAVL.LastCommitInfo() + } else { + ci = &proto.CommitInfo{ + Version: cs.Version(), + } + } + + if cs.flatKV != nil { + return cs.appendEvmLatticeHash(ci, cs.flatKV.CommittedRootHash()) } return ci } @@ -338,45 +518,69 @@ func (cs *CompositeCommitStore) LastCommitInfo() *proto.CommitInfo { // GetChildStoreByName returns the underlying child store by module name. // This only applies to cosmos committer. func (cs *CompositeCommitStore) GetChildStoreByName(name string) types.CommitKVStore { - return cs.cosmosCommitter.GetChildStoreByName(name) + return migration.NewRouterCommitKVStore( + cs.router, + name, + cs.Version, + ) } // Copy returns an in-memory snapshot, or nil when flatkv is engaged // (no in-memory primitive; a partial snapshot would miss EVM state). func (cs *CompositeCommitStore) Copy() types.Committer { - if cs == nil || cs.cosmosCommitter == nil || cs.flatkvCommitter != nil { + if cs == nil || cs.memIAVL == nil || cs.flatKV != nil { return nil } - cosmosCopy, ok := cs.cosmosCommitter.Copy().(*memiavl.CommitStore) + cosmosCopy, ok := cs.memIAVL.Copy().(*memiavl.CommitStore) if !ok || cosmosCopy == nil { return nil } - return &CompositeCommitStore{ - cosmosCommitter: cosmosCopy, - homeDir: cs.homeDir, - config: cs.config, + snap := &CompositeCommitStore{ + memIAVL: cosmosCopy, + homeDir: cs.homeDir, + config: cs.config, + ctx: cs.ctx, + } + if err := snap.buildRouter(); err != nil { + if releaseErr := cosmosCopy.ReleaseSnapshotRefs(); releaseErr != nil { + logger.Warn("failed to release memiavl snapshot refs after router build error", + "buildErr", err, "releaseErr", releaseErr) + } + logger.Warn("failed to build router for SC snapshot", "err", err) + return nil } + return snap } // ReleaseSnapshotRefs releases refs held by a copied in-memory snapshot without // closing DB-level resources shared with the live store. func (cs *CompositeCommitStore) ReleaseSnapshotRefs() error { - if cs == nil || cs.cosmosCommitter == nil { + if cs == nil { return nil } - err := cs.cosmosCommitter.ReleaseSnapshotRefs() - cs.cosmosCommitter = nil + if cs.routerCancel != nil { + cs.routerCancel() + cs.routerCancel = nil + } + cs.router = nil + if cs.memIAVL == nil { + return nil + } + err := cs.memIAVL.ReleaseSnapshotRefs() + cs.memIAVL = nil return err } // Rollback rolls back to the specified version func (cs *CompositeCommitStore) Rollback(targetVersion int64) error { - if err := cs.cosmosCommitter.Rollback(targetVersion); err != nil { - return fmt.Errorf("failed to rollback cosmos commit store: %w", err) + if cs.memIAVL != nil { + if err := cs.memIAVL.Rollback(targetVersion); err != nil { + return fmt.Errorf("failed to rollback cosmos commit store: %w", err) + } } - if cs.flatkvCommitter != nil { - if err := cs.flatkvCommitter.Rollback(targetVersion); err != nil { + if cs.flatKV != nil { + if err := cs.flatKV.Rollback(targetVersion); err != nil { return fmt.Errorf("failed to rollback evm commit store: %w", err) } } @@ -390,53 +594,86 @@ func (cs *CompositeCommitStore) Exporter(version int64) (types.Exporter, error) return nil, fmt.Errorf("version %d out of range", version) } - cosmosExporter, err := cs.cosmosCommitter.Exporter(version) - if err != nil { - return nil, fmt.Errorf("failed to create cosmos exporter: %w", err) + var memIAVLExporter types.Exporter + if cs.memIAVL != nil { + var err error + memIAVLExporter, err = cs.memIAVL.Exporter(version) + if err != nil { + return nil, fmt.Errorf("failed to create cosmos exporter: %w", err) + } } var flatkvExporter types.Exporter - if cs.flatkvCommitter != nil && (cs.config.WriteMode == config.SplitWrite || cs.config.WriteMode == config.DualWrite) { - flatkvExporter, err = cs.flatkvCommitter.Exporter(version) + if cs.flatKV != nil { + var err error + flatkvExporter, err = cs.flatKV.Exporter(version) if err != nil { - _ = cosmosExporter.Close() + _ = memIAVLExporter.Close() return nil, fmt.Errorf("failed to create flatkv exporter: %w", err) } } - return NewExporter(cosmosExporter, flatkvExporter) + if memIAVLExporter == nil && flatkvExporter == nil { + return nil, fmt.Errorf("no exporter created") + } else if memIAVLExporter == nil { + return flatkvExporter, nil + } else if flatkvExporter == nil { + return memIAVLExporter, nil + } else { + return NewExporter(memIAVLExporter, flatkvExporter) + } } // Importer returns an importer for state sync func (cs *CompositeCommitStore) Importer(version int64) (types.Importer, error) { - cosmosImporter, err := cs.cosmosCommitter.Importer(version) - if err != nil { - return nil, err + var memIAVLImporter types.Importer + if cs.memIAVL != nil { + var err error + memIAVLImporter, err = cs.memIAVL.Importer(version) + if err != nil { + return nil, fmt.Errorf("failed to create cosmos importer: %w", err) + } } - var evmImporter types.Importer - if cs.flatkvCommitter != nil { - evmImporter, err = cs.flatkvCommitter.Importer(version) + + var flatKVImporter types.Importer + if cs.flatKV != nil { + var err error + flatKVImporter, err = cs.flatKV.Importer(version) if err != nil { - _ = cosmosImporter.Close() + _ = memIAVLImporter.Close() return nil, fmt.Errorf("failed to create flatkv importer: %w", err) } } - compositeImporter := NewImporter(cosmosImporter, evmImporter) - return compositeImporter, nil + + if memIAVLImporter == nil && flatKVImporter == nil { + return nil, fmt.Errorf("no importer created") + } else if memIAVLImporter == nil { + return flatKVImporter, nil + } else if flatKVImporter == nil { + return memIAVLImporter, nil + } else { + return NewImporter(memIAVLImporter, flatKVImporter), nil + } } // Close closes all backends func (cs *CompositeCommitStore) Close() error { var errs []error - if cs.cosmosCommitter != nil { - if err := cs.cosmosCommitter.Close(); err != nil { + if cs.routerCancel != nil { + cs.routerCancel() + cs.routerCancel = nil + } + cs.router = nil + + if cs.memIAVL != nil { + if err := cs.memIAVL.Close(); err != nil { errs = append(errs, fmt.Errorf("failed to close cosmos: %w", err)) } } - if cs.flatkvCommitter != nil { - if err := cs.flatkvCommitter.Close(); err != nil { + if cs.flatKV != nil { + if err := cs.flatKV.Close(); err != nil { errs = append(errs, fmt.Errorf("failed to close FlatKV: %w", err)) } } @@ -452,18 +689,11 @@ func (cs *CompositeCommitStore) Get(store string, key []byte) (value []byte, ok return nil, false, fmt.Errorf("key cannot be nil") } - childStore := cs.GetChildStoreByName(store) - if childStore == nil { - // Once we migrate to flatKV, we won't need individual stores to be explicitly registered, - // and so we just treat a missing store as a value that doesn't exist. - return nil, false, nil - } - - value = childStore.Get(key) - if value == nil { - return nil, false, nil + value, ok, err = cs.router.Read(store, key) + if err != nil { + return nil, false, fmt.Errorf("failed to read value: %w", err) } - return value, true, nil + return value, ok, nil } func (cs *CompositeCommitStore) GetProof(store string, key []byte) (*ics23.CommitmentProof, error) { @@ -474,14 +704,10 @@ func (cs *CompositeCommitStore) GetProof(store string, key []byte) (*ics23.Commi return nil, fmt.Errorf("key cannot be nil") } - childStore := cs.GetChildStoreByName(store) - if childStore == nil { - // Once we migrate to flatKV, we won't need individual stores to be explicitly registered, - // and so we just treat a missing store as a value that doesn't exist. - return nil, nil + proof, err := cs.router.GetProof(store, key) + if err != nil { + return nil, fmt.Errorf("failed to get proof: %w", err) } - - proof := childStore.GetProof(key) return proof, nil } @@ -503,12 +729,9 @@ func (cs *CompositeCommitStore) Iterator(store string, start []byte, end []byte, if end == nil { return nil, fmt.Errorf("end cannot be nil") } - childStore := cs.GetChildStoreByName(store) - if childStore == nil { - // Once we migrate to flatKV, we won't need individual stores to be explicitly registered, - // and so we just treat a missing store as a value that doesn't exist. - return nil, nil + iterator, err := cs.router.Iterator(store, start, end, ascending) + if err != nil { + return nil, fmt.Errorf("failed to get iterator: %w", err) } - iterator := childStore.Iterator(start, end, ascending) return iterator, nil } diff --git a/sei-db/state_db/sc/composite/store_test.go b/sei-db/state_db/sc/composite/store_test.go index eb7a039b76..073d87be04 100644 --- a/sei-db/state_db/sc/composite/store_test.go +++ b/sei-db/state_db/sc/composite/store_test.go @@ -15,6 +15,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/ktype" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/migration" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" ) @@ -28,6 +29,7 @@ func (f *failingEVMStore) LoadVersion(int64, bool) (flatkv.Store, error) { } func (f *failingEVMStore) ApplyChangeSets([]*proto.NamedChangeSet) error { return nil } func (f *failingEVMStore) Commit() (int64, error) { return 0, nil } +func (f *failingEVMStore) SetInitialVersion(int64) error { return nil } func (f *failingEVMStore) Get(string, []byte) ([]byte, bool) { return nil, false } func (f *failingEVMStore) GetBlockHeightModified(string, []byte) (int64, bool, error) { return -1, false, nil @@ -36,12 +38,14 @@ func (f *failingEVMStore) Has(string, []byte) bool { return false func (f *failingEVMStore) RawGlobalIterator() flatkv.Iterator { return nil } func (f *failingEVMStore) RootHash() []byte { return nil } func (f *failingEVMStore) Version() int64 { return 0 } +func (f *failingEVMStore) GetLatestVersion() (int64, error) { return 0, nil } func (f *failingEVMStore) WriteSnapshot(string) error { return nil } func (f *failingEVMStore) Rollback(int64) error { return nil } func (f *failingEVMStore) Exporter(int64) (types.Exporter, error) { return nil, nil } func (f *failingEVMStore) Importer(int64) (types.Importer, error) { return nil, nil } func (f *failingEVMStore) GetPhaseTimer() *metrics.PhaseTimer { return nil } func (f *failingEVMStore) CommittedRootHash() []byte { return nil } +func (f *failingEVMStore) CleanupOrphanedReadOnlyDirs() error { return nil } func (f *failingEVMStore) Close() error { return nil } func padLeft32(val ...byte) []byte { @@ -54,10 +58,11 @@ func TestCompositeStoreBasicOperations(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test", keys.EVMStoreKey}) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) - _, err := cs.LoadVersion(0, false) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) defer func() { require.NoError(t, cs.Close()) @@ -68,7 +73,7 @@ func TestCompositeStoreBasicOperations(t *testing.T) { // Apply changesets with both regular and EVM data changesets := []*proto.NamedChangeSet{ { - Name: "test", + Name: keys.BankStoreKey, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ {Key: []byte("key1"), Value: []byte("value1")}, @@ -92,7 +97,7 @@ func TestCompositeStoreBasicOperations(t *testing.T) { require.Equal(t, int64(1), version) require.Equal(t, int64(1), cs.Version()) - testStore := cs.GetChildStoreByName("test") + testStore := cs.GetChildStoreByName(keys.BankStoreKey) require.NotNil(t, testStore) evmStore := cs.GetChildStoreByName(keys.EVMStoreKey) @@ -103,10 +108,11 @@ func TestEmptyChangesets(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test"}) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey})) - _, err := cs.LoadVersion(0, false) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) defer func() { require.NoError(t, cs.Close()) @@ -124,15 +130,16 @@ func TestLoadVersionCopyExisting(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test"}) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey})) - _, err := cs.LoadVersion(0, false) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { - Name: "test", + Name: keys.BankStoreKey, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ {Key: []byte("key"), Value: []byte("value")}, @@ -161,10 +168,11 @@ func TestWorkingAndLastCommitInfo(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test"}) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey})) - _, err := cs.LoadVersion(0, false) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) defer func() { require.NoError(t, cs.Close()) @@ -175,7 +183,7 @@ func TestWorkingAndLastCommitInfo(t *testing.T) { err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { - Name: "test", + Name: keys.BankStoreKey, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ {Key: []byte("key"), Value: []byte("value")}, @@ -200,7 +208,7 @@ func TestLatticeHashCommitInfo(t *testing.T) { makeChangesets := func(round byte) []*proto.NamedChangeSet { return []*proto.NamedChangeSet{ { - Name: "test", + Name: keys.BankStoreKey, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ {Key: []byte("key"), Value: []byte{round}}, @@ -221,14 +229,11 @@ func TestLatticeHashCommitInfo(t *testing.T) { tests := []struct { name string writeMode config.WriteMode - enableLattice bool expectLattice bool }{ - {"CosmosOnly/lattice_off", config.CosmosOnlyWrite, false, false}, - {"CosmosOnly/lattice_on", config.CosmosOnlyWrite, true, false}, - {"DualWrite/lattice_off", config.DualWrite, false, false}, - {"DualWrite/lattice_on", config.DualWrite, true, true}, - {"SplitWrite/lattice_on", config.SplitWrite, true, true}, + {"MemiavlOnly", config.MemiavlOnly, false}, + {"TestOnlyDualWrite", config.TestOnlyDualWrite, true}, + {"EVMMigrated", config.EVMMigrated, true}, } for _, tt := range tests { @@ -236,11 +241,11 @@ func TestLatticeHashCommitInfo(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() cfg.WriteMode = tt.writeMode - cfg.EnableLatticeHash = tt.enableLattice - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test", keys.EVMStoreKey}) - _, err := cs.LoadVersion(0, false) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) defer cs.Close() @@ -250,10 +255,10 @@ func TestLatticeHashCommitInfo(t *testing.T) { require.NoError(t, cs.ApplyChangeSets(makeChangesets(round))) // --- Working commit info --- - expectedCosmos := cs.cosmosCommitter.WorkingCommitInfo() + expectedCosmos := cs.memIAVL.WorkingCommitInfo() var expectedEvmHash []byte if tt.expectLattice { - expectedEvmHash = cs.flatkvCommitter.RootHash() + expectedEvmHash = cs.flatKV.RootHash() } workingInfo := cs.WorkingCommitInfo() @@ -288,10 +293,10 @@ func TestLatticeHashCommitInfo(t *testing.T) { require.NoError(t, err) // --- Last commit info --- - expectedCosmosLast := cs.cosmosCommitter.LastCommitInfo() + expectedCosmosLast := cs.memIAVL.LastCommitInfo() var expectedEvmCommitted []byte if tt.expectLattice { - expectedEvmCommitted = cs.flatkvCommitter.CommittedRootHash() + expectedEvmCommitted = cs.flatKV.CommittedRootHash() require.Equal(t, expectedEvmHash, expectedEvmCommitted) } @@ -338,17 +343,18 @@ func TestRollback(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test"}) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey})) - _, err := cs.LoadVersion(0, false) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) // Commit a few versions for i := 0; i < 3; i++ { err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { - Name: "test", + Name: keys.BankStoreKey, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ {Key: []byte("key"), Value: []byte("value" + string(rune('0'+i)))}, @@ -374,16 +380,17 @@ func TestGetVersions(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test"}) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey})) - _, err := cs.LoadVersion(0, false) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) for i := 0; i < 3; i++ { err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { - Name: "test", + Name: keys.BankStoreKey, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ {Key: []byte("key"), Value: []byte("value")}, @@ -397,28 +404,194 @@ func TestGetVersions(t *testing.T) { } require.NoError(t, cs.Close()) - cs2 := NewCompositeCommitStore(t.Context(), dir, cfg) - cs2.Initialize([]string{"test"}) + cs2, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs2.Initialize([]string{keys.BankStoreKey})) latestVersion, err := cs2.GetLatestVersion() require.NoError(t, err) require.Equal(t, int64(3), latestVersion) } -func TestReadOnlyLoadVersionSoftFailsWhenFlatKVUnavailable(t *testing.T) { +// TestGetLatestVersionMemiavlOnly verifies the routing path for +// MemiavlOnly: the answer comes from memiavl and flatkv is not +// consulted (it is nil). +func TestGetLatestVersionMemiavlOnly(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MemiavlOnly + // memiavl.GetLatestVersion reads the on-disk WAL tail; with the + // default async buffer wal.Write returns before the entry is + // durable, which races with the read below. Force synchronous + // WAL writes so by the time Commit returns the disk reflects + // the new version. See the doc comment on + // CompositeCommitStore.GetLatestVersion for the full rationale. + cfg.MemIAVLConfig.AsyncCommitBuffer = 0 + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + require.Nil(t, cs.flatKV, "MemiavlOnly must not allocate flatKV") + + for i := 0; i < 2; i++ { + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: keys.BankStoreKey, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k"), Value: []byte("v")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) + } + + v, err := cs.GetLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(2), v, + "MemiavlOnly must route GetLatestVersion to memiavl without consulting flatkv") +} + +// TestGetLatestVersionFlatKVOnly verifies the routing path for +// FlatKVOnly: the answer comes from flatkv and memiavl is not +// consulted (it is nil). +func TestGetLatestVersionFlatKVOnly(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.FlatKVOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + require.Nil(t, cs.memIAVL, "FlatKVOnly must not allocate memIAVL") + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: keys.EVMStoreKey, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k1"), Value: []byte("v1")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) + + v, err := cs.GetLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(1), v, + "FlatKVOnly must route GetLatestVersion to flatkv without nil-deref of memiavl") +} + +// TestGetLatestVersionBothBackendsAligned verifies that with both +// backends configured and in lockstep, GetLatestVersion returns the +// common value without error. +func TestGetLatestVersionBothBackendsAligned(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.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 + // CompositeCommitStore.GetLatestVersion for the full rationale. cfg.MemIAVLConfig.AsyncCommitBuffer = 0 - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test"}) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + require.NotNil(t, cs.memIAVL) + require.NotNil(t, cs.flatKV) - _, err := cs.LoadVersion(0, false) + for i := 0; i < 3; i++ { + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: keys.BankStoreKey, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k"), Value: []byte("v")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) + } + + v, err := cs.GetLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(3), v, + "aligned backends must produce a single agreed-upon version") +} + +// fixedVersionEVMStore is a flatkv.Store mock that reports a +// pre-programmed GetLatestVersion answer. Used by the mismatch test to +// force disagreement with the live memiavl backend without resorting +// to crash-injection fixtures. +type fixedVersionEVMStore struct { + failingEVMStore + version int64 +} + +var _ flatkv.Store = (*fixedVersionEVMStore)(nil) + +func (f *fixedVersionEVMStore) GetLatestVersion() (int64, error) { + return f.version, nil +} + +// TestGetLatestVersionBackendMismatch verifies that a disagreement +// between backends is surfaced as an error rather than silently +// picking one. Recovery is the caller's responsibility. +func TestGetLatestVersionBackendMismatch(t *testing.T) { + dir := t.TempDir() + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MigrateEVM + cfg.KeysToMigratePerBlock = 100 + + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: keys.BankStoreKey, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k"), Value: []byte("v")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) + + // memiavl is now at version 1. Swap flatkv for a mock that reports + // version 2; this is the shape of a crashed-mid-commit divergence + // that the mismatch check is designed to surface. + cs.flatKV = &fixedVersionEVMStore{version: 2} + + _, err = cs.GetLatestVersion() + require.Error(t, err, "diverging backend versions must surface as an error") + require.Contains(t, err.Error(), "mismatch") +} + +// TestReadOnlyLoadVersionFailsLoudWhenFlatKVUnavailable verifies the +// post-section-4 fail-loud contract: when a non-MemiavlOnly composite +// store is loaded read-only and the flatkv backend fails to load, the +// load itself returns an error rather than silently dropping flatkv +// (the prior soft-fail behavior). Recovering from DB errors is the +// caller's responsibility a layer up. +func TestReadOnlyLoadVersionFailsLoudWhenFlatKVUnavailable(t *testing.T) { + dir := t.TempDir() + cfg := config.DefaultStateCommitConfig() + cfg.MemIAVLConfig.AsyncCommitBuffer = 0 + // Need flatkv to be allocated and exercised by LoadVersion; + // MemiavlOnly would not touch the flatkv path at all. + cfg.WriteMode = config.MigrateEVM + cfg.KeysToMigratePerBlock = 100 + + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + + _, err = cs.LoadVersion(0, false) require.NoError(t, err) err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { - Name: "test", + Name: keys.BankStoreKey, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ {Key: []byte("key1"), Value: []byte("value1")}, @@ -430,23 +603,181 @@ func TestReadOnlyLoadVersionSoftFailsWhenFlatKVUnavailable(t *testing.T) { _, err = cs.Commit() require.NoError(t, err) - // Inject a failing EVM committer to simulate FlatKV being unavailable - // for historical versions (different retention, late enablement, etc). - cs.flatkvCommitter = &failingEVMStore{} + // Inject a failing EVM committer. The read-only load must surface + // the error rather than swallow it. + cs.flatKV = &failingEVMStore{} + + _, err = cs.LoadVersion(0, true) + require.Error(t, err, "readonly LoadVersion must fail loud when FlatKV is unavailable") + require.Contains(t, err.Error(), "FlatKV") +} + +// TestLoadVersionFlatKVOnlyReadWrite verifies the writable read path +// in FlatKVOnly mode: memIAVL is nil, only flatkv is opened, and the +// router is built against flatkv alone. Writes and reads round-trip +// through the router without nil-dereferencing memIAVL (Problem 1 of +// the section 4 LoadVersion rewrite). +func TestLoadVersionFlatKVOnlyReadWrite(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.FlatKVOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.Nil(t, cs.memIAVL, "FlatKVOnly must not allocate memIAVL") + require.NotNil(t, cs.flatKV, "FlatKVOnly must allocate flatKV") + + committer, err := cs.LoadVersion(0, false) + require.NoError(t, err, "LoadVersion must not nil-deref memIAVL in FlatKVOnly") + defer func() { _ = cs.Close() }() + require.Same(t, cs, committer, "writable LoadVersion returns the receiver") + require.NotNil(t, cs.router, "router must be built after LoadVersion") + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: keys.EVMStoreKey, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k1"), Value: []byte("v1")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) + + got, ok, err := cs.Get(keys.EVMStoreKey, []byte("k1")) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, []byte("v1"), got) +} + +// TestLoadVersionFlatKVOnlyReadOnly verifies the read-only handle +// returned by LoadVersion(_, true) in FlatKVOnly mode is fully usable: +// it has its own router (Problem 3 of section 4) and sees the data +// committed on the writable handle. +func TestLoadVersionFlatKVOnlyReadOnly(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.FlatKVOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: keys.EVMStoreKey, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k1"), Value: []byte("v1")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) - readOnly, err := cs.LoadVersion(0, true) - require.NoError(t, err, "readonly LoadVersion should succeed even when FlatKV fails") - defer func() { _ = readOnly.Close() }() + ro, err := cs.LoadVersion(0, true) + require.NoError(t, err) + defer func() { _ = ro.Close() }() + roComposite, ok := ro.(*CompositeCommitStore) + require.True(t, ok) + require.Nil(t, roComposite.memIAVL, "FlatKVOnly read-only must not have memIAVL") + require.NotNil(t, roComposite.router, "read-only handle must have its own router") - compositeRO, ok := readOnly.(*CompositeCommitStore) + got, ok, err := roComposite.Get(keys.EVMStoreKey, []byte("k1")) + require.NoError(t, err, "read-only handle must serve reads without nil-dereferencing router") require.True(t, ok) - require.Nil(t, compositeRO.flatkvCommitter, "flatkvCommitter should be nil when FlatKV failed") + require.Equal(t, []byte("v1"), got) +} - // Cosmos data should still be accessible - store := compositeRO.GetChildStoreByName("test") - require.NotNil(t, store) - val := store.Get([]byte("key1")) - require.Equal(t, []byte("value1"), val) +// TestLoadVersionRebuildsRouterOnReload verifies that calling +// LoadVersion a second time on the same store builds a fresh router +// and cancels the previous router's context. The cancel is observable +// via the routerCancel field: the second LoadVersion must replace it +// with a new function, and the first one must report Cancelled when +// invoked indirectly through the context the buildRouter handed to +// BuildRouter. +func TestLoadVersionRebuildsRouterOnReload(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MigrateEVM + cfg.KeysToMigratePerBlock = 100 + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + firstRouter := cs.router + firstCancel := cs.routerCancel + require.NotNil(t, firstRouter) + require.NotNil(t, firstCancel) + + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + require.NotNil(t, cs.router) + require.NotSame(t, firstRouter, cs.router, "LoadVersion must rebuild the router") + require.NotNil(t, cs.routerCancel) + + require.NoError(t, cs.Close()) + require.Nil(t, cs.routerCancel, "Close must clear routerCancel") + require.Nil(t, cs.router, "Close must clear router") +} + +// TestLoadVersionMountsMigrationStoreInMigrationMode verifies that +// production callers no longer have to inject the "migration" tree +// into composite.Initialize: opening in a migration mode mounts the +// tree automatically on the writable path, so the router's bootstrap +// probe finds it. +func TestLoadVersionMountsMigrationStoreInMigrationMode(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MigrateEVM + cfg.KeysToMigratePerBlock = 100 + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err, "LoadVersion must succeed without callers pre-mounting MigrationStore") + defer func() { _ = cs.Close() }() + + require.NotNil(t, cs.memIAVL.GetChildStoreByName(migration.MigrationStore), + "the migration tree must be mounted on memiavl after LoadVersion in a migration mode") +} + +// TestLoadVersionMigrationTreeAddedOnceWithinSingleOpen verifies that +// calling LoadVersion a second time within the same process does not +// trip memiavl's duplicate-tree-name guard. memiavl.ApplyUpgrades is +// not idempotent (it appends unconditionally), so the presence check +// in LoadVersion must skip the upgrade once the tree exists. +func TestLoadVersionMigrationTreeAddedOnceWithinSingleOpen(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MigrateEVM + cfg.KeysToMigratePerBlock = 100 + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + require.NotNil(t, cs.memIAVL.GetChildStoreByName(migration.MigrationStore)) + + _, err = cs.LoadVersion(0, false) + require.NoError(t, err, + "second LoadVersion must skip the redundant ApplyUpgrades rather than tripping the duplicate-name guard") + defer func() { _ = cs.Close() }() + require.NotNil(t, cs.memIAVL.GetChildStoreByName(migration.MigrationStore), + "the migration tree must remain mounted after the second load") +} + +// TestLoadVersionDoesNotMountMigrationStoreInMemiavlOnly verifies the +// negative case: a non-migration mode must not pay for the upgrade +// and must not leave a stray "migration" tree on memiavl. +func TestLoadVersionDoesNotMountMigrationStoreInMemiavlOnly(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MemiavlOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + + require.Nil(t, cs.memIAVL.GetChildStoreByName(migration.MigrationStore), + "the migration tree must not be auto-mounted outside migration modes") } // ============================================================================= @@ -493,26 +824,26 @@ func replayImport(t *testing.T, imp types.Importer, items []exportedItem) { } } -// splitWriteConfig returns a StateCommitConfig with SplitWrite mode and +// evmMigratedConfig returns a StateCommitConfig with EVMMigrated mode and // fast snapshot intervals so that memiavl snapshots exist for the exporter. -func splitWriteConfig() config.StateCommitConfig { +func evmMigratedConfig() config.StateCommitConfig { cfg := config.DefaultStateCommitConfig() - cfg.WriteMode = config.SplitWrite - cfg.EnableLatticeHash = true + cfg.WriteMode = config.EVMMigrated cfg.MemIAVLConfig.SnapshotInterval = 1 cfg.MemIAVLConfig.SnapshotMinTimeInterval = 0 cfg.MemIAVLConfig.AsyncCommitBuffer = 0 return cfg } -func TestExportImportSplitWrite(t *testing.T) { - cfg := splitWriteConfig() +func TestExportImportEVMMigrated(t *testing.T) { + cfg := evmMigratedConfig() // --- Source store: write cosmos + EVM data --- srcDir := t.TempDir() - src := NewCompositeCommitStore(t.Context(), srcDir, cfg) - src.Initialize([]string{"bank", keys.EVMStoreKey}) - _, err := src.LoadVersion(0, false) + src, err := NewCompositeCommitStore(t.Context(), srcDir, cfg) + require.NoError(t, err) + require.NoError(t, src.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = src.LoadVersion(0, false) require.NoError(t, err) addr := ktype.Address{0xAA} @@ -558,8 +889,9 @@ func TestExportImportSplitWrite(t *testing.T) { // --- Destination store: import --- dstDir := t.TempDir() - dst := NewCompositeCommitStore(t.Context(), dstDir, cfg) - dst.Initialize([]string{"bank", keys.EVMStoreKey}) + dst, err := NewCompositeCommitStore(t.Context(), dstDir, cfg) + require.NoError(t, err) + require.NoError(t, dst.Initialize([]string{"bank", keys.EVMStoreKey})) _, err = dst.LoadVersion(0, false) require.NoError(t, err) require.NoError(t, dst.Close()) @@ -580,12 +912,12 @@ func TestExportImportSplitWrite(t *testing.T) { require.Equal(t, []byte("100"), bankStore.Get([]byte("balance_alice"))) // Verify FlatKV data - require.NotNil(t, dst.flatkvCommitter) - got, found := dst.flatkvCommitter.Get(keys.EVMStoreKey, storageKey) + require.NotNil(t, dst.flatKV) + got, found := dst.flatKV.Get(keys.EVMStoreKey, storageKey) require.True(t, found, "storage key should exist in FlatKV after import") require.Equal(t, storageVal, got) - got, found = dst.flatkvCommitter.Get(keys.EVMStoreKey, nonceKey) + got, found = dst.flatKV.Get(keys.EVMStoreKey, nonceKey) require.True(t, found, "nonce key should exist in FlatKV after import") require.Equal(t, nonceVal, got) } @@ -597,9 +929,10 @@ func TestExportCosmosOnlyHasNoFlatKVModule(t *testing.T) { cfg.MemIAVLConfig.AsyncCommitBuffer = 0 dir := t.TempDir() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"bank"}) - _, err := cs.LoadVersion(0, false) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{"bank"})) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ @@ -687,18 +1020,19 @@ func TestReconcileVersionsAfterCrash(t *testing.T) { storageKey := keys.BuildEVMKey(keys.EVMKeyStorage, ktype.StorageKey(addr, slot)) - cfg := splitWriteConfig() + cfg := evmMigratedConfig() dir := t.TempDir() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test", keys.EVMStoreKey}) - _, err := cs.LoadVersion(0, false) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) for i := byte(1); i <= 3; i++ { err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { - Name: "test", + Name: keys.BankStoreKey, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ {Key: []byte("key"), Value: []byte{i}}, @@ -718,8 +1052,8 @@ func TestReconcileVersionsAfterCrash(t *testing.T) { _, err = cs.Commit() require.NoError(t, err) } - require.Equal(t, int64(3), cs.cosmosCommitter.Version()) - require.Equal(t, int64(3), cs.flatkvCommitter.Version()) + require.Equal(t, int64(3), cs.memIAVL.Version()) + require.Equal(t, int64(3), cs.flatKV.Version()) require.NoError(t, cs.Close()) // Simulate crash: rollback FlatKV to version 2 independently, leaving @@ -740,18 +1074,19 @@ func TestReconcileVersionsAfterCrash(t *testing.T) { // Reopen the composite store — LoadVersion(0) should detect the // mismatch and reconcile both backends to version 2. - cs2 := NewCompositeCommitStore(t.Context(), dir, cfg) - cs2.Initialize([]string{"test", keys.EVMStoreKey}) + cs2, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs2.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) defer cs2.Close() - require.Equal(t, int64(2), cs2.cosmosCommitter.Version(), "cosmos should be rolled back to EVM version") - require.Equal(t, int64(2), cs2.flatkvCommitter.Version(), "EVM should remain at version 2") + require.Equal(t, int64(2), cs2.memIAVL.Version(), "cosmos should be rolled back to EVM version") + require.Equal(t, int64(2), cs2.flatKV.Version(), "EVM should remain at version 2") require.Equal(t, int64(2), cs2.Version()) // Verify cosmos data is at version 2 (value = 0x02, not 0x03) - testStore := cs2.GetChildStoreByName("test") + testStore := cs2.GetChildStoreByName(keys.BankStoreKey) require.NotNil(t, testStore) require.Equal(t, []byte{2}, testStore.Get([]byte("key"))) } @@ -762,12 +1097,13 @@ func TestReconcileVersionsThenContinueCommitting(t *testing.T) { storageKey := keys.BuildEVMKey(keys.EVMKeyStorage, ktype.StorageKey(addr, slot)) - cfg := splitWriteConfig() + cfg := evmMigratedConfig() dir := t.TempDir() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"bank", keys.EVMStoreKey}) - _, err := cs.LoadVersion(0, false) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) // Commit versions 1-3 with both backends in sync. @@ -796,13 +1132,14 @@ func TestReconcileVersionsThenContinueCommitting(t *testing.T) { require.NoError(t, evmStore.Close()) // Reopen — reconciliation should bring both to version 2. - cs2 := NewCompositeCommitStore(t.Context(), dir, cfg) - cs2.Initialize([]string{"bank", keys.EVMStoreKey}) + cs2, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs2.Initialize([]string{"bank", keys.EVMStoreKey})) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) - require.Equal(t, int64(2), cs2.cosmosCommitter.Version()) - require.Equal(t, int64(2), cs2.flatkvCommitter.Version()) + require.Equal(t, int64(2), cs2.memIAVL.Version()) + require.Equal(t, int64(2), cs2.flatKV.Version()) // Continue committing new blocks on top of the reconciled state. // Version 3 is re-created with new data (0xA3 instead of 0x03). @@ -819,26 +1156,27 @@ func TestReconcileVersionsThenContinueCommitting(t *testing.T) { ver, err := cs2.Commit() require.NoError(t, err) require.Equal(t, int64(3+i), ver, "commit should produce sequential versions") - require.Equal(t, ver, cs2.cosmosCommitter.Version()) - require.Equal(t, ver, cs2.flatkvCommitter.Version()) + require.Equal(t, ver, cs2.memIAVL.Version()) + require.Equal(t, ver, cs2.flatKV.Version()) } require.NoError(t, cs2.Close()) // Reopen a third time to verify the post-reconciliation commits are durable // and both backends agree on version 5. - cs3 := NewCompositeCommitStore(t.Context(), dir, cfg) - cs3.Initialize([]string{"bank", keys.EVMStoreKey}) + cs3, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs3.Initialize([]string{"bank", keys.EVMStoreKey})) _, err = cs3.LoadVersion(0, false) require.NoError(t, err) defer cs3.Close() - require.Equal(t, int64(5), cs3.cosmosCommitter.Version()) - require.Equal(t, int64(5), cs3.flatkvCommitter.Version()) + require.Equal(t, int64(5), cs3.memIAVL.Version()) + require.Equal(t, int64(5), cs3.flatKV.Version()) bankStore := cs3.GetChildStoreByName("bank") require.Equal(t, []byte{0xA5}, bankStore.Get([]byte("bal"))) - got, found := cs3.flatkvCommitter.Get(keys.EVMStoreKey, storageKey) + got, found := cs3.flatKV.Get(keys.EVMStoreKey, storageKey) require.True(t, found) require.Equal(t, padLeft32(0xA5), got) } @@ -848,7 +1186,7 @@ func TestReconcileVersionsThenContinueCommitting(t *testing.T) { // ============================================================================= // setupComposite opens a fresh CompositeCommitStore using the given write -// mode, populates "test" with k1->v1, k2->v2, k3->v3, commits version 1, +// mode, populates keys.BankStoreKey with k1->v1, k2->v2, k3->v3, commits version 1, // and returns the store ready for read assertions. Cleanup is registered. func setupComposite(t *testing.T, writeMode config.WriteMode) *CompositeCommitStore { t.Helper() @@ -856,14 +1194,15 @@ func setupComposite(t *testing.T, writeMode config.WriteMode) *CompositeCommitSt cfg := config.DefaultStateCommitConfig() cfg.WriteMode = writeMode - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test", "other", keys.EVMStoreKey}) - _, err := cs.LoadVersion(0, false) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.StakingStoreKey, keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) t.Cleanup(func() { _ = cs.Close() }) err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ - {Name: "test", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Name: keys.BankStoreKey, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ {Key: []byte("k1"), Value: []byte("v1")}, {Key: []byte("k2"), Value: []byte("v2")}, {Key: []byte("k3"), Value: []byte("v3")}, @@ -876,7 +1215,7 @@ func setupComposite(t *testing.T, writeMode config.WriteMode) *CompositeCommitSt } func TestCompositeGetValidation(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) + cs := setupComposite(t, config.MemiavlOnly) cases := []struct { name string @@ -885,7 +1224,7 @@ func TestCompositeGetValidation(t *testing.T) { wantMsg string }{ {"empty store", "", []byte("k1"), "store name cannot be empty"}, - {"nil key", "test", nil, "key cannot be nil"}, + {"nil key", keys.BankStoreKey, nil, "key cannot be nil"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -896,32 +1235,35 @@ func TestCompositeGetValidation(t *testing.T) { } } -func TestCompositeGetMissingStore(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - val, ok, err := cs.Get("nonexistent", []byte("k1")) - require.NoError(t, err) - require.False(t, ok) - require.Nil(t, val) +// TestCompositeGetUnknownStore pins the current router-based contract: +// reading from a name the router cannot route returns an error. This +// behavior will relax to silent-miss once the router becomes a +// flatkv-style prefix passthrough; for now the router rejects. +func TestCompositeGetUnknownStore(t *testing.T) { + cs := setupComposite(t, config.MemiavlOnly) + _, _, err := cs.Get("nonexistent", []byte("k1")) + require.Error(t, err) + require.Contains(t, err.Error(), "nonexistent") } func TestCompositeGetMissingKey(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - val, ok, err := cs.Get("test", []byte("missing")) + cs := setupComposite(t, config.MemiavlOnly) + val, ok, err := cs.Get(keys.BankStoreKey, []byte("missing")) require.NoError(t, err) require.False(t, ok) require.Nil(t, val) } func TestCompositeGetPresent(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - val, ok, err := cs.Get("test", []byte("k1")) + cs := setupComposite(t, config.MemiavlOnly) + val, ok, err := cs.Get(keys.BankStoreKey, []byte("k1")) require.NoError(t, err) require.True(t, ok) require.Equal(t, []byte("v1"), val) } func TestCompositeHasValidation(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) + cs := setupComposite(t, config.MemiavlOnly) cases := []struct { name string @@ -929,7 +1271,7 @@ func TestCompositeHasValidation(t *testing.T) { key []byte }{ {"empty store", "", []byte("k1")}, - {"nil key", "test", nil}, + {"nil key", keys.BankStoreKey, nil}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -939,32 +1281,33 @@ func TestCompositeHasValidation(t *testing.T) { } } -func TestCompositeHasMissingStore(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - ok, err := cs.Has("nonexistent", []byte("k1")) - require.NoError(t, err) - require.False(t, ok) +// TestCompositeHasUnknownStore mirrors TestCompositeGetUnknownStore for Has. +func TestCompositeHasUnknownStore(t *testing.T) { + cs := setupComposite(t, config.MemiavlOnly) + _, err := cs.Has("nonexistent", []byte("k1")) + require.Error(t, err) + require.Contains(t, err.Error(), "nonexistent") } func TestCompositeHasAgreesWithGet(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - keys := [][]byte{ + cs := setupComposite(t, config.MemiavlOnly) + testKeys := [][]byte{ []byte("k1"), []byte("k2"), []byte("k3"), []byte("missing"), } - for _, k := range keys { - _, getOk, err := cs.Get("test", k) + for _, k := range testKeys { + _, getOk, err := cs.Get(keys.BankStoreKey, k) require.NoError(t, err) - hasOk, err := cs.Has("test", k) + hasOk, err := cs.Has(keys.BankStoreKey, k) require.NoError(t, err) require.Equal(t, getOk, hasOk, "Has should agree with Get for key %q", k) } } func TestCompositeIteratorValidation(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) + cs := setupComposite(t, config.MemiavlOnly) cases := []struct { name string @@ -973,8 +1316,8 @@ func TestCompositeIteratorValidation(t *testing.T) { end []byte }{ {"empty store", "", []byte("k1"), []byte("k9")}, - {"nil start", "test", nil, []byte("k9")}, - {"nil end", "test", []byte("k1"), nil}, + {"nil start", keys.BankStoreKey, nil, []byte("k9")}, + {"nil end", keys.BankStoreKey, []byte("k1"), nil}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -984,16 +1327,17 @@ func TestCompositeIteratorValidation(t *testing.T) { } } -func TestCompositeIteratorMissingStore(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - iter, err := cs.Iterator("nonexistent", []byte("k1"), []byte("k9"), true) - require.NoError(t, err) - require.Nil(t, iter) +// TestCompositeIteratorUnknownStore mirrors TestCompositeGetUnknownStore for Iterator. +func TestCompositeIteratorUnknownStore(t *testing.T) { + cs := setupComposite(t, config.MemiavlOnly) + _, err := cs.Iterator("nonexistent", []byte("k1"), []byte("k9"), true) + require.Error(t, err) + require.Contains(t, err.Error(), "nonexistent") } func TestCompositeIteratorAscending(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - iter, err := cs.Iterator("test", []byte("k1"), []byte("k9"), true) + cs := setupComposite(t, config.MemiavlOnly) + iter, err := cs.Iterator(keys.BankStoreKey, []byte("k1"), []byte("k9"), true) require.NoError(t, err) require.NotNil(t, iter) defer iter.Close() @@ -1007,8 +1351,8 @@ func TestCompositeIteratorAscending(t *testing.T) { } func TestCompositeIteratorDescending(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - iter, err := cs.Iterator("test", []byte("k1"), []byte("k9"), false) + cs := setupComposite(t, config.MemiavlOnly) + iter, err := cs.Iterator(keys.BankStoreKey, []byte("k1"), []byte("k9"), false) require.NoError(t, err) require.NotNil(t, iter) defer iter.Close() @@ -1024,8 +1368,8 @@ func TestCompositeIteratorDescending(t *testing.T) { // TestCompositeIteratorRange pins the standard dbm.Iterator contract: // start is inclusive, end is exclusive. func TestCompositeIteratorRange(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - iter, err := cs.Iterator("test", []byte("k1"), []byte("k3"), true) + cs := setupComposite(t, config.MemiavlOnly) + iter, err := cs.Iterator(keys.BankStoreKey, []byte("k1"), []byte("k3"), true) require.NoError(t, err) require.NotNil(t, iter) defer iter.Close() @@ -1039,7 +1383,7 @@ func TestCompositeIteratorRange(t *testing.T) { } func TestCompositeGetProofValidation(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) + cs := setupComposite(t, config.MemiavlOnly) cases := []struct { name string @@ -1047,7 +1391,7 @@ func TestCompositeGetProofValidation(t *testing.T) { key []byte }{ {"empty store", "", []byte("k1")}, - {"nil key", "test", nil}, + {"nil key", keys.BankStoreKey, nil}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -1057,35 +1401,33 @@ func TestCompositeGetProofValidation(t *testing.T) { } } -func TestCompositeGetProofMissingStore(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - proof, err := cs.GetProof("nonexistent", []byte("k1")) - require.NoError(t, err) - require.Nil(t, proof) +// TestCompositeGetProofUnknownStore mirrors TestCompositeGetUnknownStore for GetProof. +func TestCompositeGetProofUnknownStore(t *testing.T) { + cs := setupComposite(t, config.MemiavlOnly) + _, err := cs.GetProof("nonexistent", []byte("k1")) + require.Error(t, err) + require.Contains(t, err.Error(), "nonexistent") } func TestCompositeGetProofPresent(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) - proof, err := cs.GetProof("test", []byte("k1")) + cs := setupComposite(t, config.MemiavlOnly) + proof, err := cs.GetProof(keys.BankStoreKey, []byte("k1")) require.NoError(t, err) require.NotNil(t, proof) } -// TestCompositeSplitWriteEVMReadsAreInvisible pins the current routing -// behavior: in SplitWrite mode, EVM changesets are written exclusively to -// FlatKV, so read methods on the composite (which only consult the cosmos -// child store) cannot see the data. -// -// TODO: re-evaluate when the four read methods learn to route to FlatKV -// for EVM-keyed stores. Until then, callers wanting EVM data go through -// flatkvCommitter directly. -func TestCompositeSplitWriteEVMReadsAreInvisible(t *testing.T) { +// TestCompositeEVMMigratedEVMReadsAreVisible pins the router-based read +// contract: in EVMMigrated mode the router sends evm/ reads to FlatKV, +// so composite.Get / Has surface the data written via ApplyChangeSets +// the same way a direct FlatKV lookup does. +func TestCompositeEVMMigratedEVMReadsAreVisible(t *testing.T) { dir := t.TempDir() - cfg := splitWriteConfig() + cfg := evmMigratedConfig() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"test", keys.EVMStoreKey}) - _, err := cs.LoadVersion(0, false) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey, keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) t.Cleanup(func() { _ = cs.Close() }) @@ -1102,42 +1444,42 @@ func TestCompositeSplitWriteEVMReadsAreInvisible(t *testing.T) { _, err = cs.Commit() require.NoError(t, err) - // FlatKV has the data. - require.NotNil(t, cs.flatkvCommitter) - got, found := cs.flatkvCommitter.Get(keys.EVMStoreKey, evmKey) + // FlatKV holds the authoritative copy. + require.NotNil(t, cs.flatKV) + got, found := cs.flatKV.Get(keys.EVMStoreKey, evmKey) require.True(t, found, "EVM data should be present in FlatKV") require.Equal(t, evmVal, got) - // But the composite's own Get/Has return missing because they only - // look at the (empty) cosmos child store. + // The composite's Get/Has route through the router and surface the + // same FlatKV value. val, ok, err := cs.Get(keys.EVMStoreKey, evmKey) require.NoError(t, err) - require.False(t, ok, "current routing does not surface FlatKV data through composite.Get") - require.Nil(t, val) + require.True(t, ok, "composite.Get must surface FlatKV data through the router") + require.Equal(t, evmVal, val) hasOk, err := cs.Has(keys.EVMStoreKey, evmKey) require.NoError(t, err) - require.False(t, hasOk) + require.True(t, hasOk) } // TestCompositeCosmosOnlyPassesThrough sanity-checks that for cosmos-named // stores in CosmosOnly mode, the composite's read methods produce the same // results as the underlying memiavl backend. func TestCompositeCosmosOnlyPassesThrough(t *testing.T) { - cs := setupComposite(t, config.CosmosOnlyWrite) + cs := setupComposite(t, config.MemiavlOnly) - val, ok, err := cs.Get("test", []byte("k2")) + val, ok, err := cs.Get(keys.BankStoreKey, []byte("k2")) require.NoError(t, err) require.True(t, ok) require.Equal(t, []byte("v2"), val) - hasOk, err := cs.Has("test", []byte("k2")) + hasOk, err := cs.Has(keys.BankStoreKey, []byte("k2")) require.NoError(t, err) require.True(t, hasOk) // Iteration through the composite should yield the same keys as the // underlying cosmos child store. - iter, err := cs.Iterator("test", []byte("k1"), []byte("k9"), true) + iter, err := cs.Iterator(keys.BankStoreKey, []byte("k1"), []byte("k9"), true) require.NoError(t, err) require.NotNil(t, iter) defer iter.Close() @@ -1155,12 +1497,13 @@ func TestReconcileVersionsCosmosAheadByMultiple(t *testing.T) { storageKey := keys.BuildEVMKey(keys.EVMKeyStorage, ktype.StorageKey(addr, slot)) - cfg := splitWriteConfig() + cfg := evmMigratedConfig() dir := t.TempDir() - cs := NewCompositeCommitStore(t.Context(), dir, cfg) - cs.Initialize([]string{"bank", keys.EVMStoreKey}) - _, err := cs.LoadVersion(0, false) + cs, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) require.NoError(t, err) for i := byte(1); i <= 5; i++ { @@ -1199,15 +1542,450 @@ func TestReconcileVersionsCosmosAheadByMultiple(t *testing.T) { require.NoError(t, err) require.NoError(t, evmStore.Close()) - cs2 := NewCompositeCommitStore(t.Context(), dir, cfg) - cs2.Initialize([]string{"bank", keys.EVMStoreKey}) + cs2, err := NewCompositeCommitStore(t.Context(), dir, cfg) + require.NoError(t, err) + require.NoError(t, cs2.Initialize([]string{"bank", keys.EVMStoreKey})) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) defer cs2.Close() - require.Equal(t, int64(3), cs2.cosmosCommitter.Version()) - require.Equal(t, int64(3), cs2.flatkvCommitter.Version()) + require.Equal(t, int64(3), cs2.memIAVL.Version()) + require.Equal(t, int64(3), cs2.flatKV.Version()) bankStore := cs2.GetChildStoreByName("bank") require.Equal(t, []byte{3}, bankStore.Get([]byte("bal"))) } + +// TestMigrationEntrySeedingMemiavlToMigrateEVM exercises the production +// scenario the seeding logic in composite.LoadVersion exists for: a chain +// that has been running on MemiavlOnly for many blocks switches its +// configuration to MigrateEVM at restart. memiavl is at version N (large), +// flatkv has never existed. The composite store must bring flatkv into +// lockstep at version N so subsequent commits produce matching versions +// on both backends. Without the SetInitialVersion seeding, the next +// Commit produces memiavl=N+1 and flatkv=1, wedging the chain on the +// version-mismatch guard. +func TestMigrationEntrySeedingMemiavlToMigrateEVM(t *testing.T) { + dir := t.TempDir() + + // Phase 1: run for 100 blocks in MemiavlOnly mode. + cosmosCfg := config.DefaultStateCommitConfig() + cosmosCfg.WriteMode = config.MemiavlOnly + + cs1, err := NewCompositeCommitStore(t.Context(), dir, cosmosCfg) + require.NoError(t, err) + require.NoError(t, cs1.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs1.LoadVersion(0, false) + require.NoError(t, err) + + const phase1Blocks = 100 + for i := 0; i < phase1Blocks; i++ { + err := cs1.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: "bank", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte(fmt.Sprintf("bal_%d", i)), Value: []byte{byte(i)}}, + }}}, + }) + require.NoError(t, err) + v, err := cs1.Commit() + require.NoError(t, err) + require.Equal(t, int64(i+1), v) + } + require.Equal(t, int64(phase1Blocks), cs1.Version()) + require.Nil(t, cs1.flatKV, "MemiavlOnly mode must not create a flatkv store") + require.NoError(t, cs1.Close()) + + // Phase 2: reopen with MigrateEVM mode. memiavl is at version 100, + // flatkv directory does not exist yet. Seeding must bring flatkv to + // version 100 so the very next commit produces version 101 on both. + migrateCfg := config.DefaultStateCommitConfig() + migrateCfg.WriteMode = config.MigrateEVM + migrateCfg.KeysToMigratePerBlock = 100 + + cs2, err := NewCompositeCommitStore(t.Context(), dir, migrateCfg) + require.NoError(t, err) + require.NoError(t, cs2.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs2.LoadVersion(0, false) + require.NoError(t, err) + defer cs2.Close() + + require.Equal(t, int64(phase1Blocks), cs2.memIAVL.Version(), + "memiavl version must survive reopen") + require.NotNil(t, cs2.flatKV, "MigrateEVM mode must create a flatkv store") + require.Equal(t, int64(phase1Blocks), cs2.flatKV.Version(), + "flatkv must be seeded to memiavl's version after migration-entry seeding") + require.Equal(t, int64(phase1Blocks), cs2.Version(), + "composite version must report the seeded version") + + // Phase 3: drive more blocks through the migration router and verify + // both backends advance in lockstep. + const phase3Blocks = 10 + for i := 0; i < phase3Blocks; i++ { + blockIdx := phase1Blocks + i + err := cs2.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: "bank", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte(fmt.Sprintf("bal_%d", blockIdx)), Value: []byte{byte(blockIdx)}}, + }}}, + }) + require.NoError(t, err) + v, err := cs2.Commit() + require.NoError(t, err) + require.Equal(t, int64(blockIdx+1), v) + require.Equal(t, cs2.memIAVL.Version(), cs2.flatKV.Version(), + "memiavl and flatkv must stay in lockstep after seeding") + } +} + +// TestMigrationEntrySeedingIsIdempotentAcrossRestarts verifies that once +// flatkv has been seeded and committed, a subsequent restart does not +// re-seed (which would error out via the "non-empty store" guard). +func TestMigrationEntrySeedingIsIdempotentAcrossRestarts(t *testing.T) { + dir := t.TempDir() + + cosmosCfg := config.DefaultStateCommitConfig() + cosmosCfg.WriteMode = config.MemiavlOnly + cs1, err := NewCompositeCommitStore(t.Context(), dir, cosmosCfg) + require.NoError(t, err) + require.NoError(t, cs1.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs1.LoadVersion(0, false) + require.NoError(t, err) + for i := 0; i < 5; i++ { + require.NoError(t, cs1.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: "bank", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("bal"), Value: []byte{byte(i)}}, + }}}, + })) + _, err := cs1.Commit() + require.NoError(t, err) + } + require.NoError(t, cs1.Close()) + + migrateCfg := config.DefaultStateCommitConfig() + migrateCfg.WriteMode = config.MigrateEVM + migrateCfg.KeysToMigratePerBlock = 100 + + cs2, err := NewCompositeCommitStore(t.Context(), dir, migrateCfg) + require.NoError(t, err) + require.NoError(t, cs2.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs2.LoadVersion(0, false) + require.NoError(t, err) + require.Equal(t, int64(5), cs2.flatKV.Version(), "flatkv seeded to memiavl version on first reopen") + _, err = cs2.Commit() + require.NoError(t, err) + require.Equal(t, int64(6), cs2.Version()) + require.NoError(t, cs2.Close()) + + cs3, err := NewCompositeCommitStore(t.Context(), dir, migrateCfg) + require.NoError(t, err) + require.NoError(t, cs3.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs3.LoadVersion(0, false) + require.NoError(t, err, "second reopen must not re-seed flatkv (would fail the fresh-store guard)") + defer cs3.Close() + require.Equal(t, int64(6), cs3.memIAVL.Version()) + require.Equal(t, int64(6), cs3.flatKV.Version()) +} + +// TestInitializeIsNoOpInFlatKVOnly verifies that composite.Initialize does +// not dereference a nil memIAVL when running in FlatKVOnly mode. flatkv has +// no per-module pre-allocation analog, so the call is a no-op there. +func TestInitializeIsNoOpInFlatKVOnly(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.FlatKVOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.Nil(t, cs.memIAVL, "FlatKVOnly must not allocate a memIAVL backend") + require.NotPanics(t, func() { + require.NoError(t, cs.Initialize([]string{"bank", keys.EVMStoreKey})) + }, "Initialize must not panic when memIAVL is nil") +} + +// TestSetInitialVersionMemiavlOnly verifies SetInitialVersion delegates +// only to memIAVL when flatkv is absent, and that the first commit +// produces the requested version. +func TestSetInitialVersionMemiavlOnly(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MemiavlOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + require.Nil(t, cs.flatKV, "MemiavlOnly must not allocate a flatkv backend") + + require.NoError(t, cs.SetInitialVersion(100)) + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: "bank", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("alice"), Value: []byte("1")}, + }}}, + })) + v, err := cs.Commit() + require.NoError(t, err) + require.Equal(t, int64(100), v, "first commit after SetInitialVersion(100) must be version 100") +} + +// TestSetInitialVersionDelegatesToBothBackends verifies that in a mode +// where both backends are active (MigrateEVM), SetInitialVersion seeds +// both and the next commit produces matching versions. +func TestSetInitialVersionDelegatesToBothBackends(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MigrateEVM + cfg.KeysToMigratePerBlock = 100 + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + require.NotNil(t, cs.memIAVL) + require.NotNil(t, cs.flatKV) + + require.NoError(t, cs.SetInitialVersion(50)) + + require.Equal(t, int64(49), cs.flatKV.Version(), + "flatkv reflects the seed immediately (committedVersion = N-1)") + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: "bank", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("alice"), Value: []byte("1")}, + }}}, + })) + v, err := cs.Commit() + require.NoError(t, err) + require.Equal(t, int64(50), v, + "first commit after composite.SetInitialVersion must produce the seeded version on both backends") + require.Equal(t, int64(50), cs.memIAVL.Version()) + require.Equal(t, int64(50), cs.flatKV.Version()) +} + +// TestSetInitialVersionRetryIsIdempotent verifies that a caller retrying +// SetInitialVersion with the same value (e.g. after a transient failure) +// does not wedge the store. Memiavl-first ordering matters here: memiavl +// permits a second call while no commit has happened, and flatkv would +// have rejected the second call had it already succeeded once. +func TestSetInitialVersionRetryIsIdempotent(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MigrateEVM + cfg.KeysToMigratePerBlock = 100 + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.NoError(t, cs.Initialize([]string{"bank", keys.EVMStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // First call seeds both backends. + require.NoError(t, cs.SetInitialVersion(75)) + // Second call: memiavl is still pre-commit, so its idempotency holds; + // but flatkv is already at committedVersion=74 and rejects the retry. + err = cs.SetInitialVersion(75) + require.Error(t, err, "the second call must surface flatkv's fresh-store rejection") + require.Contains(t, err.Error(), "flatkv SetInitialVersion") +} + +// TestInitializeRejectsUnknownStoreNames verifies that +// composite.Initialize fails fast when given names the router cannot +// route. The ModuleRouter used in migration / dual-write modes only +// routes the canonical set in keys.MemIAVLStoreKeys; any other name +// (e.g. legacy test placeholders) is rejected before backend state +// is touched. +func TestInitializeRejectsUnknownStoreNames(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MigrateEVM + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + + err = cs.Initialize([]string{keys.BankStoreKey, "bogus", "also-bogus"}) + require.Error(t, err) + require.Contains(t, err.Error(), "not routable") + require.Contains(t, err.Error(), "bogus") + require.Contains(t, err.Error(), "also-bogus") + require.NotContains(t, err.Error(), keys.BankStoreKey, + "the valid name should not appear in the unknown-names list") +} + +// TestInitializeAcceptsUnknownStoreNamesInMemiavlOnly is the +// regression test for the sei-ibc-go simapp failure: downstream test +// apps that mount more modules than seid (icahost / icacontroller) +// must be able to run in MemiavlOnly. The PassthroughRouter installed +// for that mode performs no name lookup, so Initialize must accept +// arbitrary names. The test follows up by writing through one of +// those non-canonical stores and reading the value back to confirm +// the full ApplyChangeSets / Commit / Get path actually works against +// memiavl for names outside keys.MemIAVLStoreKeys. +func TestInitializeAcceptsUnknownStoreNamesInMemiavlOnly(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MemiavlOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + + require.NoError(t, cs.Initialize([]string{"icahost", "icacontroller"})) + + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: "icahost", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k"), Value: []byte("v")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) + + got, ok, err := cs.Get("icahost", []byte("k")) + require.NoError(t, err) + require.True(t, ok, "PassthroughRouter must forward reads to memiavl for non-canonical names") + require.Equal(t, []byte("v"), got) +} + +// TestInitializeAcceptsUnknownStoreNamesInFlatKVOnly is the FlatKVOnly +// counterpart to TestInitializeAcceptsUnknownStoreNamesInMemiavlOnly. +// FlatKVOnly likewise uses a PassthroughRouter, so Initialize must +// accept arbitrary names and the full ApplyChangeSets / Commit / Get +// round-trip must work for them against the flatkv backend. +// memIAVL is intentionally nil in this mode; the test guards that +// Initialize stays a no-op for the memiavl side while still +// validating the name list. +func TestInitializeAcceptsUnknownStoreNamesInFlatKVOnly(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.FlatKVOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + require.Nil(t, cs.memIAVL, "FlatKVOnly must not allocate a memIAVL backend") + defer func() { _ = cs.Close() }() + + require.NoError(t, cs.Initialize([]string{"icahost", "icacontroller"})) + + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: "icahost", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k"), Value: []byte("v")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) + + got, ok, err := cs.Get("icahost", []byte("k")) + require.NoError(t, err) + require.True(t, ok, "PassthroughRouter must forward reads to flatkv for non-canonical names") + require.Equal(t, []byte("v"), got) +} + +// TestInitializeAcceptsAllMemIAVLStoreKeys verifies that the entire +// canonical production set passes validation. Guards against +// validateInitialStores drifting away from keys.MemIAVLStoreKeys. +func TestInitializeAcceptsAllMemIAVLStoreKeys(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MemiavlOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + + require.NoError(t, cs.Initialize(keys.MemIAVLStoreKeys)) +} + +// TestCopyProducesUsableSnapshot exercises the full snapshot path +// callers actually take: capture an SC snapshot via Copy, then read +// committed state through GetChildStoreByName. Regression for a bug +// where Copy returned a CompositeCommitStore with a nil router, so +// the first read through RouterCommitKVStore nil-derefed (the trace +// RPC path hit this via baseapp.GetConsensusParams). A second Copy +// of the snapshot must also be usable, since TraceSnapshotStore.Lease +// performs another Copy on top of the stored snapshot. +func TestCopyProducesUsableSnapshot(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = config.MemiavlOnly + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + + require.NoError(t, cs.Initialize([]string{keys.BankStoreKey})) + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + + require.NoError(t, cs.ApplyChangeSets([]*proto.NamedChangeSet{ + {Name: keys.BankStoreKey, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k"), Value: []byte("v")}, + }}}, + })) + _, err = cs.Commit() + require.NoError(t, err) + + snap := cs.Copy() + require.NotNil(t, snap, "Copy must return a non-nil snapshot in MemiavlOnly mode") + defer func() { + releaser, ok := snap.(interface{ ReleaseSnapshotRefs() error }) + require.True(t, ok) + require.NoError(t, releaser.ReleaseSnapshotRefs()) + }() + + snapComposite, ok := snap.(*CompositeCommitStore) + require.True(t, ok) + bankSnap := snapComposite.GetChildStoreByName(keys.BankStoreKey) + require.NotNil(t, bankSnap) + require.NotPanics(t, func() { + require.Equal(t, []byte("v"), bankSnap.Get([]byte("k"))) + require.True(t, bankSnap.Has([]byte("k"))) + }, "snapshot reads must not nil-deref on the snapshot's router") + + leased := snapComposite.Copy() + require.NotNil(t, leased, "Copy of a snapshot must also produce a usable snapshot (Lease path)") + defer func() { + releaser, ok := leased.(interface{ ReleaseSnapshotRefs() error }) + require.True(t, ok) + require.NoError(t, releaser.ReleaseSnapshotRefs()) + }() + leasedComposite, ok := leased.(*CompositeCommitStore) + require.True(t, ok) + bankLeased := leasedComposite.GetChildStoreByName(keys.BankStoreKey) + require.NotNil(t, bankLeased) + require.NotPanics(t, func() { + require.Equal(t, []byte("v"), bankLeased.Get([]byte("k"))) + }, "leased snapshot reads must not nil-deref") +} + +// TestInitializeRejectsMigrationStoreName verifies that callers cannot +// inject the MigrationStore tree themselves. The composite mounts it +// on demand in LoadVersion when the mode requires it; accepting the +// name from outside would let callers smuggle a migration tree into +// state and confuse later upgrades. The reservation holds in every +// mode -- both MemiavlOnly (which otherwise has no allow-list) and +// migration modes -- so a misconfigured caller can't sneak it past +// the relaxed validation. +func TestInitializeRejectsMigrationStoreName(t *testing.T) { + cases := []struct { + name string + mode config.WriteMode + }{ + {"MemiavlOnly", config.MemiavlOnly}, + {"FlatKVOnly", config.FlatKVOnly}, + {"MigrateEVM", config.MigrateEVM}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = tc.mode + + cs, err := NewCompositeCommitStore(t.Context(), t.TempDir(), cfg) + require.NoError(t, err) + defer func() { _ = cs.Close() }() + + err = cs.Initialize([]string{migration.MigrationStore}) + require.Error(t, err) + require.Contains(t, err.Error(), migration.MigrationStore) + }) + } +} diff --git a/sei-db/state_db/sc/flatkv/api.go b/sei-db/state_db/sc/flatkv/api.go index 5a0f1ce591..56e5e5b6c3 100644 --- a/sei-db/state_db/sc/flatkv/api.go +++ b/sei-db/state_db/sc/flatkv/api.go @@ -34,6 +34,12 @@ type Store interface { // Commit persists buffered writes and advances the version. Commit() (int64, error) + // SetInitialVersion seeds the store so that the next Commit produces + // initialVersion. Must be called after LoadVersion, on a truly fresh + // store (no prior commits) and before any writes. Returns an error on + // a read-only store, on a non-fresh store, or for initialVersion <= 0. + SetInitialVersion(initialVersion int64) error + // Get returns the value for a key within the given module. // For EVM keys (moduleName == "evm"), the key is a memiavl EVM key // routed to account/storage/code/legacy DBs internally. @@ -63,6 +69,12 @@ type Store interface { // Version returns the latest committed version. Version() int64 + // GetLatestVersion returns the latest committed version persisted to + // disk. Equivalent to Version() once LoadVersion has run; before + // LoadVersion it answers from on-disk metadata so callers can + // inspect the store's height without taking ownership of it. + GetLatestVersion() (int64, error) + // WriteSnapshot writes a complete snapshot to dir. WriteSnapshot(dir string) error @@ -80,6 +92,11 @@ type Store interface { // integration with external phases of execution. GetPhaseTimer() *metrics.PhaseTimer + // CleanupOrphanedReadOnlyDirs removes readonly-* working directories + // left behind by a previous process crash. Must be called once at + // process startup, before any read-only instances are created. + CleanupOrphanedReadOnlyDirs() error + io.Closer } diff --git a/sei-db/state_db/sc/flatkv/store_meta.go b/sei-db/state_db/sc/flatkv/store_meta.go index dfc14e4d32..27ba94bef4 100644 --- a/sei-db/state_db/sc/flatkv/store_meta.go +++ b/sei-db/state_db/sc/flatkv/store_meta.go @@ -1,11 +1,15 @@ package flatkv import ( + "context" "encoding/binary" "fmt" "math" + "os" + "path/filepath" errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" + "github.com/sei-protocol/sei-chain/sei-db/db_engine/pebbledb" "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/ktype" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash" @@ -125,3 +129,126 @@ func newPerDBLtHashMap() map[string]*lthash.LtHash { } return m } + +// SetInitialVersion seeds the store so that the next Commit produces +// initialVersion. Mirrors memiavl.DB.SetInitialVersion: only valid on a +// truly fresh store (committedVersion == 0 and no prior commits), rejected +// on read-only stores, and persists durably across restart. +// +// Implementation notes: +// - We persist version = initialVersion - 1 to both the global metadata DB +// and every per-DB LocalMeta. Commit() does `version := committedVersion + 1`, +// so the next commit will return initialVersion. +// - Write order is "global first, per-DB second" so that any partial-write +// crash recovers as "fresh store" (loadGlobalMetadata lowers the global +// watermark to the minimum per-DB watermark; per-DB at 0 forces global +// back to 0). A retry with the same initialVersion is idempotent. +// - LtHashes stay at their zero values (lthash.New()) — a freshly seeded +// store has no data, so committed/working LtHashes remain the identity. +func (s *CommitStore) SetInitialVersion(initialVersion int64) error { + if s.readOnly { + return errReadOnly + } + if initialVersion <= 0 { + return fmt.Errorf("flatkv: initial version must be positive, got %d", initialVersion) + } + if s.committedVersion != 0 { + return fmt.Errorf("flatkv: SetInitialVersion can only be called on a fresh store; committedVersion=%d", + s.committedVersion) + } + if s.metadataDB == nil { + return fmt.Errorf("flatkv: SetInitialVersion called before LoadVersion") + } + + seededVersion := initialVersion - 1 + + if err := s.commitGlobalMetadata(seededVersion, s.committedLtHash); err != nil { + return fmt.Errorf("flatkv: SetInitialVersion: persist global metadata: %w", err) + } + + syncOpt := types.WriteOptions{Sync: s.config.Fsync} + for _, ndb := range s.namedDataDBs() { + ltHash := s.perDBWorkingLtHash[ndb.dir] + if ltHash == nil { + ltHash = lthash.New() + s.perDBWorkingLtHash[ndb.dir] = ltHash + } + batch := ndb.db.NewBatch() + if err := writeLocalMetaToBatch(batch, seededVersion, ltHash); err != nil { + _ = batch.Close() + return fmt.Errorf("flatkv: SetInitialVersion: prepare %s local meta: %w", ndb.dir, err) + } + if err := batch.Commit(syncOpt); err != nil { + _ = batch.Close() + return fmt.Errorf("flatkv: SetInitialVersion: commit %s local meta: %w", ndb.dir, err) + } + _ = batch.Close() + s.localMeta[ndb.dir] = &ktype.LocalMeta{ + CommittedVersion: seededVersion, + LtHash: ltHash.Clone(), + } + } + + s.committedVersion = seededVersion + logger.Info("FlatKV SetInitialVersion", "initialVersion", initialVersion, "seededVersion", seededVersion) + return nil +} + +// GetLatestVersion returns the latest committed version persisted under +// dir without holding an open *CommitStore. Mirrors memiavl.GetLatestVersion +// in role: a side-channel for callers that need the on-disk watermark +// before LoadVersion has run (e.g. the rootmulti sanity check at +// process startup). Returns 0 when the store has never been opened or +// has no commits yet. +// +// The truth source is MetaVersionKey in working/metadata. The working +// dir survives across restarts and is updated on every Commit, so this +// matches the precision of memiavl.GetLatestVersion (which reads the +// WAL tail). It must not be called concurrently with a running +// CommitStore on dir, because the underlying PebbleDB takes an +// exclusive file lock. +func GetLatestVersion(dir string) (int64, error) { + metaDir := filepath.Join(dir, workingDirName, metadataDir) + if _, err := os.Stat(metaDir); err != nil { + if os.IsNotExist(err) { + return 0, nil + } + return 0, fmt.Errorf("flatkv: stat working metadata dir %q: %w", metaDir, err) + } + + cfg := pebbledb.DefaultConfig() + cfg.DataDir = metaDir + cfg.EnableMetrics = false + db, err := pebbledb.Open(context.Background(), &cfg) + if err != nil { + return 0, fmt.Errorf("flatkv: open working metadata at %q: %w", cfg.DataDir, err) + } + defer func() { _ = db.Close() }() + + data, err := db.Get(ktype.MetaVersionKey) + if errorutils.IsNotFound(err) { + return 0, nil + } + if err != nil { + return 0, fmt.Errorf("flatkv: read MetaVersionKey: %w", err) + } + if len(data) != 8 { + return 0, fmt.Errorf("flatkv: invalid metadata version length: got %d, want 8", len(data)) + } + v := binary.BigEndian.Uint64(data) + if v > math.MaxInt64 { + return 0, fmt.Errorf("flatkv: metadata version overflow: %d exceeds max int64", v) + } + return int64(v), nil //nolint:gosec // overflow checked above +} + +// GetLatestVersion returns the latest committed version. When the store +// is open, the in-memory committed watermark is authoritative; before +// LoadVersion has run, it falls back to the free-standing on-disk +// helper. Either path returns 0 on a fresh store. +func (s *CommitStore) GetLatestVersion() (int64, error) { + if s.metadataDB != nil { + return s.committedVersion, nil + } + return GetLatestVersion(s.flatkvDir()) +} diff --git a/sei-db/state_db/sc/flatkv/store_meta_test.go b/sei-db/state_db/sc/flatkv/store_meta_test.go index f11bf368b7..ed9a7415c7 100644 --- a/sei-db/state_db/sc/flatkv/store_meta_test.go +++ b/sei-db/state_db/sc/flatkv/store_meta_test.go @@ -150,6 +150,127 @@ func TestStoreMetadataOperations(t *testing.T) { }) } +// ============================================================================= +// SetInitialVersion +// ============================================================================= + +func TestSetInitialVersion_HappyPath(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + require.NoError(t, s.SetInitialVersion(100)) + require.Equal(t, int64(99), s.committedVersion) + + addr := ktype.Address{0xAA} + slot := ktype.Slot{0xBB} + cs := makeChangeSet(evmStorageKey(addr, slot), padLeft32(0xCC), false) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + + v, err := s.Commit() + require.NoError(t, err) + require.Equal(t, int64(100), v, "first Commit after SetInitialVersion(100) must produce version 100") + require.Equal(t, int64(100), s.Version()) +} + +func TestSetInitialVersion_RejectsAfterCommit(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + addr := ktype.Address{0x01} + slot := ktype.Slot{0x02} + cs := makeChangeSet(evmStorageKey(addr, slot), padLeft32(0x03), false) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + _, err := s.Commit() + require.NoError(t, err) + + err = s.SetInitialVersion(50) + require.Error(t, err) + require.Contains(t, err.Error(), "fresh store") +} + +func TestSetInitialVersion_RejectsReadOnly(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + addr := ktype.Address{0x01} + slot := ktype.Slot{0x02} + cs := makeChangeSet(evmStorageKey(addr, slot), padLeft32(0x03), false) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + _, err := s.Commit() + require.NoError(t, err) + + roStore, err := s.LoadVersion(0, true) + require.NoError(t, err) + defer roStore.Close() + + err = roStore.SetInitialVersion(50) + require.Error(t, err) + require.ErrorIs(t, err, errReadOnly) +} + +func TestSetInitialVersion_RejectsNonPositive(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + require.Error(t, s.SetInitialVersion(0)) + require.Error(t, s.SetInitialVersion(-1)) + require.Equal(t, int64(0), s.committedVersion, "rejected calls must not mutate state") +} + +func TestSetInitialVersion_SurvivesReopen(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + cfg := config.DefaultConfig() + cfg.DataDir = dbDir + s, err := NewCommitStore(t.Context(), cfg) + require.NoError(t, err) + _, err = s.LoadVersion(0, false) + require.NoError(t, err) + + require.NoError(t, s.SetInitialVersion(100)) + require.NoError(t, s.Close()) + + cfg2 := config.DefaultConfig() + cfg2.DataDir = dbDir + s2, err := NewCommitStore(context.Background(), cfg2) + require.NoError(t, err) + _, err = s2.LoadVersion(0, false) + require.NoError(t, err) + defer s2.Close() + + require.Equal(t, int64(99), s2.committedVersion, + "persisted committedVersion must equal initialVersion-1 after reopen") + + addr := ktype.Address{0xDD} + slot := ktype.Slot{0xEE} + cs := makeChangeSet(evmStorageKey(addr, slot), padLeft32(0xFF), false) + require.NoError(t, s2.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + v, err := s2.Commit() + require.NoError(t, err) + require.Equal(t, int64(100), v, + "first Commit after reopen must produce initialVersion") +} + +func TestSetInitialVersion_RollbackBelowSeededVersionFails(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + require.NoError(t, s.SetInitialVersion(100)) + + addr := ktype.Address{0x77} + slot := ktype.Slot{0x88} + cs := makeChangeSet(evmStorageKey(addr, slot), padLeft32(0x01), false) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + _, err := s.Commit() + require.NoError(t, err) + require.Equal(t, int64(100), s.Version()) + + err = s.Rollback(50) + require.Error(t, err, + "rollback below initialVersion-1 must fail; nothing exists before the seeded baseline") +} + // ============================================================================= // Global Metadata Persistence After Commit + Reopen // ============================================================================= @@ -191,3 +312,99 @@ func TestGlobalMetadataPersistence(t *testing.T) { require.Equal(t, expectedHash, s2.committedLtHash.Checksum(), "global LtHash should survive reopen") } + +// ============================================================================= +// GetLatestVersion (free-standing helper + method) +// ============================================================================= + +func TestGetLatestVersionFreshDirReturnsZero(t *testing.T) { + dir := t.TempDir() + v, err := GetLatestVersion(filepath.Join(dir, flatkvRootDir)) + require.NoError(t, err) + require.Equal(t, int64(0), v, + "never-opened flatkv dir must report version 0, not an error") +} + +func TestGetLatestVersionAfterCommitsReadsWorkingMeta(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + cfg := config.DefaultConfig() + cfg.DataDir = dbDir + s, err := NewCommitStore(t.Context(), cfg) + require.NoError(t, err) + _, err = s.LoadVersion(0, false) + require.NoError(t, err) + + commitStorageEntry(t, s, ktype.Address{0x01}, ktype.Slot{0x01}, []byte{0xAA}) + commitStorageEntry(t, s, ktype.Address{0x02}, ktype.Slot{0x02}, []byte{0xBB}) + commitStorageEntry(t, s, ktype.Address{0x03}, ktype.Slot{0x03}, []byte{0xCC}) + + require.NoError(t, s.Close()) + + v, err := GetLatestVersion(dbDir) + require.NoError(t, err) + require.Equal(t, int64(3), v, + "helper must read MetaVersionKey from working/metadata after a clean close") +} + +func TestGetLatestVersionMissingKeyReturnsZero(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + cfg := config.DefaultConfig() + cfg.DataDir = dbDir + s, err := NewCommitStore(t.Context(), cfg) + require.NoError(t, err) + _, err = s.LoadVersion(0, false) + require.NoError(t, err) + require.NoError(t, s.Close()) + + v, err := GetLatestVersion(dbDir) + require.NoError(t, err) + require.Equal(t, int64(0), v, + "opened-then-closed-with-no-commits flatkv must report version 0") +} + +func TestCommitStoreGetLatestVersionReturnsInMemoryWhenLoaded(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + v, err := s.GetLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(0), v) + + commitStorageEntry(t, s, ktype.Address{0x01}, ktype.Slot{0x01}, []byte{0xAA}) + commitStorageEntry(t, s, ktype.Address{0x02}, ktype.Slot{0x02}, []byte{0xBB}) + + v, err = s.GetLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(2), v, + "method on an open store must return the in-memory committed version") +} + +func TestCommitStoreGetLatestVersionFallsBackToDiskWhenUnloaded(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + cfg := config.DefaultConfig() + cfg.DataDir = dbDir + s, err := NewCommitStore(t.Context(), cfg) + require.NoError(t, err) + _, err = s.LoadVersion(0, false) + require.NoError(t, err) + commitStorageEntry(t, s, ktype.Address{0x01}, ktype.Slot{0x01}, []byte{0xAA}) + commitStorageEntry(t, s, ktype.Address{0x02}, ktype.Slot{0x02}, []byte{0xBB}) + require.NoError(t, s.Close()) + + cfg2 := config.DefaultConfig() + cfg2.DataDir = dbDir + s2, err := NewCommitStore(context.Background(), cfg2) + require.NoError(t, err) + defer s2.Close() + + v, err := s2.GetLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(2), v, + "method on a not-yet-opened store must fall through to the on-disk helper") +} diff --git a/sei-db/state_db/sc/memiavl/store.go b/sei-db/state_db/sc/memiavl/store.go index 43201c67f1..dc9be9e2d3 100644 --- a/sei-db/state_db/sc/memiavl/store.go +++ b/sei-db/state_db/sc/memiavl/store.go @@ -36,8 +36,9 @@ func NewCommitStore(homeDir string, config Config) *CommitStore { return commitStore } -func (cs *CommitStore) Initialize(initialStores []string) { +func (cs *CommitStore) Initialize(initialStores []string) error { cs.opts.InitialStores = initialStores + return nil } func (cs *CommitStore) SetInitialVersion(initialVersion int64) error { @@ -127,14 +128,21 @@ func (cs *CommitStore) Version() int64 { return cs.db.Version() } +// GetLatestVersion returns the highest version durably written to the +// changelog WAL on disk. Note that with AsyncCommitBuffer > 0, +// wal.Write returns before the entry is durable (see sei-db/wal/wal.go, +// "Do not wait for the write to be durable"), so this value can lag the +// in-memory MultiTree by one or more commits while async writes drain. +// Callers that need the just-committed version should use cs.Version() +// or cs.LastCommitInfo().Version, both of which read the in-memory +// tree. The lag is harmless for current production callers +// (rootmulti.NewStore and rootmulti.LastCommitID), which only invoke +// GetLatestVersion before LoadVersion has opened the DB; in that +// pre-load state nothing is in memory anyway. func (cs *CommitStore) GetLatestVersion() (int64, error) { return GetLatestVersion(cs.opts.Dir) } -func (cs *CommitStore) GetEarliestVersion() (int64, error) { - return GetEarliestVersion(cs.opts.Dir) -} - func (cs *CommitStore) ApplyChangeSets(changesets []*proto.NamedChangeSet) error { if len(changesets) == 0 { return nil diff --git a/sei-db/state_db/sc/memiavl/store_test.go b/sei-db/state_db/sc/memiavl/store_test.go index c04303f44c..0b90df2cf2 100644 --- a/sei-db/state_db/sc/memiavl/store_test.go +++ b/sei-db/state_db/sc/memiavl/store_test.go @@ -886,7 +886,7 @@ func TestWALTruncationOnCommit(t *testing.T) { require.NoError(t, err) // Get earliest snapshot version - may not exist yet if snapshots are async - earliestSnapshot, err := cs.GetEarliestVersion() + earliestSnapshot, err := GetEarliestVersion(cs.opts.Dir) if err != nil { // No snapshots yet (async snapshot creation), that's okay for this test t.Logf("No snapshots created yet (async): %v", err) @@ -1273,7 +1273,7 @@ func TestWALTruncationDelta(t *testing.T) { require.NoError(t, err) // Get earliest snapshot version - may not exist yet if snapshots are async - earliestSnapshot, err := cs2.GetEarliestVersion() + earliestSnapshot, err := GetEarliestVersion(cs2.opts.Dir) if err != nil { t.Logf("No snapshots created yet: %v", err) require.NoError(t, cs2.Close()) diff --git a/sei-db/state_db/sc/migration/migration_manager.go b/sei-db/state_db/sc/migration/migration_manager.go index b98ec30407..08369625a3 100644 --- a/sei-db/state_db/sc/migration/migration_manager.go +++ b/sei-db/state_db/sc/migration/migration_manager.go @@ -103,25 +103,18 @@ func NewMigrationManager( startVersion, targetVersion) } - // Look up the version from the new DB first. If it's already at - // targetVersion the migration has completed on a prior boot; the - // caller should not be constructing a MigrationManager in that case. + // Look up the version from the new DB first. currentMigrationVersion, versionKnown, err := readVersionFromDB(newDBReader) if err != nil { return nil, fmt.Errorf("failed to read migration version from new DB: %w", err) } - if versionKnown { - if currentMigrationVersion == targetVersion { - return nil, fmt.Errorf( - "new DB already at targetVersion (%d); construct the next migration mode's router instead of a MigrationManager", - targetVersion) - } - if currentMigrationVersion != startVersion { - return nil, fmt.Errorf( - "unexpected migration version in new DB: expected %d (start) or %d (target), got %d", - startVersion, targetVersion, currentMigrationVersion) - } + atTargetVersion := versionKnown && currentMigrationVersion == targetVersion + + if versionKnown && !atTargetVersion && currentMigrationVersion != startVersion { + return nil, fmt.Errorf( + "unexpected migration version in new DB: expected %d (start) or %d (target), got %d", + startVersion, targetVersion, currentMigrationVersion) } if !versionKnown { @@ -136,9 +129,20 @@ func NewMigrationManager( } } - boundary, err := readMigrationBoundary(newDBReader) - if err != nil { - return nil, fmt.Errorf("failed to read migration boundary: %w", err) + var boundary MigrationBoundary + if atTargetVersion { + // The final block of the migration wrote MigrationVersionKey = + // targetVersion and deleted MigrationBoundaryKey atomically, so + // there is no boundary on disk to read. Come up in passthrough: + // every read routes to the new DB via IsMigrated, every write + // takes the post-completion early-return in ApplyChangeSets, + // and the iterator's Complete short-circuit keeps it inert. + boundary = MigrationBoundaryComplete + } else { + boundary, err = readMigrationBoundary(newDBReader) + if err != nil { + return nil, fmt.Errorf("failed to read migration boundary: %w", err) + } } iterator.SetBoundary(boundary) @@ -329,7 +333,6 @@ func (m *MigrationManager) ApplyChangeSets(changesets []*proto.NamedChangeSet) e }) } - // Write to the old DB first, then the new DB. if err := m.oldDBWriter(oldDBChangeSet); err != nil { return fmt.Errorf("failed to apply changes to old database: %w", err) } 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 8894c2cac8..8d310a2340 100644 --- a/sei-db/state_db/sc/migration/migration_manager_test.go +++ b/sei-db/state_db/sc/migration/migration_manager_test.go @@ -602,9 +602,13 @@ func TestApplyChangeSets_AfterMigrationComplete(t *testing.T) { ) require.NoError(t, err) - // Drive the manager into the post-completion state directly. The - // constructor no longer produces this state itself; the only way to - // reach it is through the boundary advancing during ApplyChangeSets. + // Drive the manager into the post-completion state directly so + // this test focuses on the ApplyChangeSets fast path in isolation. + // (The constructor will produce this state on its own when the new + // DB already reports targetVersion - see + // TestNewMigrationManager_AcceptsNewDBAtTargetVersion - but here we + // just want to exercise the post-completion branch without setting + // up that fixture.) mgr.boundary = MigrationBoundaryComplete changesets := []*proto.NamedChangeSet{ @@ -639,7 +643,9 @@ func TestApplyChangeSets_AfterMigrationCompleteNilChangesets(t *testing.T) { ) require.NoError(t, err) - // Drive the manager into the post-completion state directly. + // Drive the manager into the post-completion state directly so + // this test focuses on the nil-changeset post-completion fast + // path in isolation. mgr.boundary = MigrationBoundaryComplete err = mgr.ApplyChangeSets(nil) @@ -884,16 +890,23 @@ func TestNewMigrationManager_NilDependencies(t *testing.T) { } } -// TestNewMigrationManager_RejectsNewDBAtTargetVersion pins the contract -// that the constructor refuses to build a manager for a migration that -// is already over: when the new DB already reports targetVersion the -// caller is expected to construct the next migration mode's router -// (steady-state) instead. -func TestNewMigrationManager_RejectsNewDBAtTargetVersion(t *testing.T) { +// TestNewMigrationManager_AcceptsNewDBAtTargetVersion pins the contract +// that the constructor accepts a new DB whose version already equals +// targetVersion and produces a manager that comes up in passthrough +// mode: boundary = Complete, every read routes to the new DB, and +// ApplyChangeSets takes the post-completion fast path. This is what +// keeps a migration-mode WriteMode safe to leave configured +// indefinitely after the migration completes - operators don't need +// to flip a config setting on the first restart past the cutover. +func TestNewMigrationManager_AcceptsNewDBAtTargetVersion(t *testing.T) { oldDB := newMockDB() + oldDB.seed(map[string]map[string][]byte{ + "bank": {"k": []byte("from-old")}, + }) newDB := newMockDB() newDB.seed(map[string]map[string][]byte{ MigrationStore: {MigrationVersionKey: encodeVersion(testTargetVersion)}, + "bank": {"k": []byte("from-new")}, }) iter := NewMockMigrationIterator(nil, false) @@ -902,9 +915,34 @@ func TestNewMigrationManager_RejectsNewDBAtTargetVersion(t *testing.T) { newDB.reader(), newDB.writer(), iter, 10, ) - require.Error(t, err) - require.Nil(t, mgr) - require.Contains(t, err.Error(), "construct the next migration mode's router") + require.NoError(t, err) + require.NotNil(t, mgr) + require.True(t, mgr.boundary.Equals(MigrationBoundaryComplete), + "manager constructed at targetVersion must come up with boundary = Complete") + + // Reads must route to the new DB (the migrated side) for every + // store; if the manager were treating the boundary as NotStarted + // the read below would surface "from-old" from the old DB. + val, ok, err := mgr.Read("bank", []byte("k")) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, []byte("from-new"), val, + "passthrough manager must read migrated keys from the new DB") + + // Writes must take the post-completion fast path: forwarded + // verbatim to the new DB, old DB untouched, no migration + // bookkeeping injected. + cs := []*proto.NamedChangeSet{ + {Name: "bank", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k2"), Value: []byte("v2")}, + }}}, + } + require.NoError(t, mgr.ApplyChangeSets(cs)) + val, ok = newDB.get("bank", "k2") + require.True(t, ok) + require.Equal(t, []byte("v2"), val) + require.Empty(t, oldDB.writeLog, + "old DB must not be written when manager comes up post-completion") } // --- Issue 7: old-DB changeset grouping --- diff --git a/sei-db/state_db/sc/migration/migration_steady_state_test.go b/sei-db/state_db/sc/migration/migration_steady_state_test.go index f0b7bdd554..d096ca90e5 100644 --- a/sei-db/state_db/sc/migration/migration_steady_state_test.go +++ b/sei-db/state_db/sc/migration/migration_steady_state_test.go @@ -5,6 +5,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/keys" "github.com/sei-protocol/sei-chain/sei-db/common/testutil" + "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/stretchr/testify/require" ) @@ -92,7 +93,7 @@ func TestMemiavlOnly(t *testing.T) { inMemoryRouter := NewTestInMemoryRouter() keysInUse := newLiveKeySet() - memiavlOnlyRouter, err := BuildRouter(t.Context(), MemiavlOnly, memiavlDB, nil, 0) + memiavlOnlyRouter, err := BuildRouter(t.Context(), config.MemiavlOnly, memiavlDB, nil, 0) require.NoError(t, err) commit := func() { @@ -146,7 +147,7 @@ func TestEVMMigrated(t *testing.T) { inMemoryRouter := NewTestInMemoryRouter() keysInUse := newLiveKeySet() - evmMigratedRouter, err := BuildRouter(t.Context(), EVMMigrated, memiavlDB, flatKVDB, 0) + evmMigratedRouter, err := BuildRouter(t.Context(), config.EVMMigrated, memiavlDB, flatKVDB, 0) require.NoError(t, err) commitBoth := func() { @@ -231,7 +232,7 @@ func TestAllMigratedButBank(t *testing.T) { inMemoryRouter := NewTestInMemoryRouter() keysInUse := newLiveKeySet() - allMigratedButBankRouter, err := BuildRouter(t.Context(), AllMigratedButBank, memiavlDB, flatKVDB, 0) + allMigratedButBankRouter, err := BuildRouter(t.Context(), config.AllMigratedButBank, memiavlDB, flatKVDB, 0) require.NoError(t, err) commitBoth := func() { @@ -315,7 +316,7 @@ func TestFlatKVOnly(t *testing.T) { inMemoryRouter := NewTestInMemoryRouter() keysInUse := newLiveKeySet() - flatKVOnlyRouter, err := BuildRouter(t.Context(), FlatKVOnly, nil, flatKVDB, 0) + flatKVOnlyRouter, err := BuildRouter(t.Context(), config.FlatKVOnly, nil, flatKVDB, 0) require.NoError(t, err) commit := func() { @@ -384,7 +385,7 @@ func TestDualWrite(t *testing.T) { inMemoryRouter := NewTestInMemoryRouter() keysInUse := newLiveKeySet() - dualWriteRouter, err := BuildRouter(t.Context(), TestOnlyDualWrite, memiavlDB, flatKVDB, 0) + dualWriteRouter, err := BuildRouter(t.Context(), config.TestOnlyDualWrite, memiavlDB, flatKVDB, 0) require.NoError(t, err) commitBoth := func() { 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 77b8d7d846..697c74c88d 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 @@ -672,7 +672,7 @@ func NewTestFlatKVCommitStore(t *testing.T, dir string) *flatkv.CommitStore { func NewTestMemIAVLCommitStore(t *testing.T, dir string, storeNames []string) *memiavl.CommitStore { t.Helper() cs := memiavl.NewCommitStore(dir, memiavl.DefaultConfig()) - cs.Initialize(storeNames) + require.NoError(t, cs.Initialize(storeNames)) if _, err := cs.LoadVersion(0, false); err != nil { t.Fatalf("NewTestMemIAVLCommitStore: LoadVersion: %v", err) } diff --git a/sei-db/state_db/sc/migration/migration_transitions_test.go b/sei-db/state_db/sc/migration/migration_transitions_test.go index 16d46dc428..5a9f8aade8 100644 --- a/sei-db/state_db/sc/migration/migration_transitions_test.go +++ b/sei-db/state_db/sc/migration/migration_transitions_test.go @@ -5,6 +5,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/keys" "github.com/sei-protocol/sei-chain/sei-db/common/testutil" + "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/stretchr/testify/require" ) @@ -65,7 +66,7 @@ func TestMigrateEVM(t *testing.T) { ) // Build a migration router that will migrate the evm/ data to flatkv. - migrationRouter, err := BuildRouter(t.Context(), MigrateEVM, memiavlDB, flatKVDB, 100) + migrationRouter, err := BuildRouter(t.Context(), config.MigrateEVM, memiavlDB, flatKVDB, 100) require.NoError(t, err) // Phase 2: drive 2 blocks through the migration router. Phase 1 produced @@ -119,7 +120,7 @@ func TestMigrateEVM(t *testing.T) { // manager recovers its state from disk - either resuming from the // boundary, or coming up in passthrough if the version key already // records the target version. - migrationRouter, err = BuildRouter(t.Context(), MigrateEVM, memiavlDB, flatKVDB, 100) + migrationRouter, err = BuildRouter(t.Context(), config.MigrateEVM, memiavlDB, flatKVDB, 100) require.NoError(t, err, "rebuild migration router after restart") // Sanity check: all oracle data is still reachable through the rebuilt @@ -235,7 +236,7 @@ func TestMigrateAllButBank(t *testing.T) { // Lay down v1 state: evm/ in flatKV, everything else in memiavl. Drives // roughly equal load across all real modules so the non-evm-non-bank // stores accumulate enough keys to make the v1->v2 migration meaningful. - evmMigratedRouter, err := BuildRouter(t.Context(), EVMMigrated, memiavlDB, flatKVDB, 0) + evmMigratedRouter, err := BuildRouter(t.Context(), config.EVMMigrated, memiavlDB, flatKVDB, 0) require.NoError(t, err, "build EVMMigrated router") SimulateBlocks(t, NewTestMultiRouter(t, evmMigratedRouter, inMemoryRouter), @@ -258,7 +259,7 @@ func TestMigrateAllButBank(t *testing.T) { SeedMigrationVersionInFlatKV(t, flatKVDB, Version1_MigrateEVM) // --- Phase 2: partial MigrateAllButBank --- - migrationRouter, err := BuildRouter(t.Context(), MigrateAllButBank, memiavlDB, flatKVDB, 100) + migrationRouter, err := BuildRouter(t.Context(), config.MigrateAllButBank, memiavlDB, flatKVDB, 100) require.NoError(t, err, "build MigrateAllButBank router") // 50 blocks * 100 batch ≈ 5,000 keys migrated, well short of the ~9,000 @@ -307,7 +308,7 @@ func TestMigrateAllButBank(t *testing.T) { require.NoError(t, err, "flatKV commit") } - migrationRouter, err = BuildRouter(t.Context(), MigrateAllButBank, memiavlDB, flatKVDB, 100) + migrationRouter, err = BuildRouter(t.Context(), config.MigrateAllButBank, memiavlDB, flatKVDB, 100) require.NoError(t, err, "rebuild MigrateAllButBank router after restart") // Sanity check: all oracle data is still reachable through the rebuilt @@ -432,7 +433,7 @@ func TestMigrateBank(t *testing.T) { // Lay down v2 state: bank/ in memiavl, everything else in flatKV. Drives // roughly equal load across all real modules so bank/ accumulates enough // keys to make the v2->v3 migration meaningful. - allMigratedButBankRouter, err := BuildRouter(t.Context(), AllMigratedButBank, memiavlDB, flatKVDB, 0) + allMigratedButBankRouter, err := BuildRouter(t.Context(), config.AllMigratedButBank, memiavlDB, flatKVDB, 0) require.NoError(t, err, "build AllMigratedButBank router") SimulateBlocks(t, NewTestMultiRouter(t, allMigratedButBankRouter, inMemoryRouter), @@ -455,7 +456,7 @@ func TestMigrateBank(t *testing.T) { SeedMigrationVersionInFlatKV(t, flatKVDB, Version2_MigrateAllButBank) // --- Phase 2: MigrateBank --- - migrationRouter, err := BuildRouter(t.Context(), MigrateBank, memiavlDB, flatKVDB, 100) + migrationRouter, err := BuildRouter(t.Context(), config.MigrateBank, memiavlDB, flatKVDB, 100) require.NoError(t, err, "build MigrateBank router") // Drive 2 blocks through the migration router. Phase 1 produced ~500 @@ -502,7 +503,7 @@ func TestMigrateBank(t *testing.T) { require.NoError(t, err, "flatKV commit") } - migrationRouter, err = BuildRouter(t.Context(), MigrateBank, memiavlDB, flatKVDB, 100) + migrationRouter, err = BuildRouter(t.Context(), config.MigrateBank, memiavlDB, flatKVDB, 100) require.NoError(t, err, "rebuild MigrateBank router after restart") // Sanity check: all oracle data is still reachable through the rebuilt diff --git a/sei-db/state_db/sc/migration/migration_version_test.go b/sei-db/state_db/sc/migration/migration_version_test.go index aa43a212b5..96d1f77e1d 100644 --- a/sei-db/state_db/sc/migration/migration_version_test.go +++ b/sei-db/state_db/sc/migration/migration_version_test.go @@ -60,12 +60,14 @@ func TestIsAtVersion_ReaderErrorPropagates(t *testing.T) { // --- Constructor: at targetVersion --- -// TestMigrationManager_AtTargetVersion_RejectedByConstructor pins the -// post-R4 contract: the constructor refuses to build a manager when -// the new DB already reports targetVersion. The migration is over at -// that point and the caller is expected to construct the next migration -// mode's router (steady-state) instead. -func TestMigrationManager_AtTargetVersion_RejectedByConstructor(t *testing.T) { +// TestMigrationManager_AtTargetVersion_ComesUpInPassthrough pins the +// contract that the constructor accepts a new DB at targetVersion and +// produces a manager whose boundary is Complete. This is what allows +// the migration-mode WriteMode to remain configured after the +// migration completes without requiring an operator-driven config +// flip on the next restart - the on-disk version is the source of +// truth, and the manager adapts to it. +func TestMigrationManager_AtTargetVersion_ComesUpInPassthrough(t *testing.T) { oldDB := newMockDB() newDB := newMockDB() newDB.seed(map[string]map[string][]byte{ @@ -79,9 +81,10 @@ func TestMigrationManager_AtTargetVersion_RejectedByConstructor(t *testing.T) { NewMockMigrationIterator(nil, false), nil, ) - require.Error(t, err) - require.Nil(t, mgr) - require.Contains(t, err.Error(), "construct the next migration mode's router") + require.NoError(t, err) + require.NotNil(t, mgr) + require.True(t, mgr.boundary.Equals(MigrationBoundaryComplete), + "manager constructed at targetVersion must come up with boundary = Complete") } // TestMigrationManager_NilHandlesRejected pins the post-R4 unconditional @@ -316,12 +319,12 @@ func TestMigrationManager_UnexpectedVersionInNewDB_Errors(t *testing.T) { require.Contains(t, err.Error(), "10", "error should name the expected targetVersion") } -func TestMigrationManager_AtTargetVersion_RejectedRegardlessOfOldDB(t *testing.T) { +func TestMigrationManager_AtTargetVersion_OldDBVersionIgnored(t *testing.T) { // When the new DB already reports targetVersion the constructor - // rejects the configuration; the old DB's contents do not matter. - // The constructor no longer produces a manager directly in the - // "already complete" state — the caller is expected to detect that - // case via IsAtVersion and bypass MigrationManager entirely. + // trusts that signal and comes up in passthrough mode without + // consulting the old DB's version. We prove that by seeding the + // old DB with an otherwise-rejected version: if the constructor + // were to look at it, it would error out. oldDB := newMockDB() oldDB.seed(map[string]map[string][]byte{ MigrationStore: {MigrationVersionKey: encodeVersion(999)}, @@ -338,9 +341,9 @@ func TestMigrationManager_AtTargetVersion_RejectedRegardlessOfOldDB(t *testing.T NewMockMigrationIterator(nil, false), nil, ) - require.Error(t, err) - require.Nil(t, mgr) - require.Contains(t, err.Error(), "construct the next migration mode's router") + require.NoError(t, err, "new DB's targetVersion should be authoritative, old DB not re-checked") + require.NotNil(t, mgr) + require.True(t, mgr.boundary.Equals(MigrationBoundaryComplete)) } func TestMigrationManager_StartVersionMustBeLessThanTarget(t *testing.T) { diff --git a/sei-db/state_db/sc/migration/passthrough_router.go b/sei-db/state_db/sc/migration/passthrough_router.go new file mode 100644 index 0000000000..c59e153394 --- /dev/null +++ b/sei-db/state_db/sc/migration/passthrough_router.go @@ -0,0 +1,84 @@ +package migration + +import ( + "fmt" + + ics23 "github.com/confio/ics23/go" + "github.com/sei-protocol/sei-chain/sei-db/proto" + db "github.com/tendermint/tm-db" +) + +var _ Router = (*PassthroughRouter)(nil) + +// PassthroughRouter implements Router for single-backend modes where +// every operation goes to the same destination regardless of store +// name. Unlike ModuleRouter it holds no name -> backend map and +// performs no per-call name lookup: each method forwards its arguments +// straight to the supplied accessor. The wrapped accessors are +// themselves responsible for surfacing "unknown store" errors when the +// backend does not recognize the name. +// +// Used by MemiavlOnly mode where every store lives on memiavl and +// memiavl already reports unknown child stores from +// GetChildStoreByName. +type PassthroughRouter struct { + reader DBReader + writer DBWriter + iteratorBuilder DBIteratorBuilder + proofBuilder DBProofBuilder +} + +// NewPassthroughRouter builds a router that forwards every operation +// to the supplied accessors. The reader and writer are required. +// iteratorBuilder and proofBuilder are optional: when nil, the +// corresponding Router method returns an error describing the missing +// capability (e.g. flatkv has no proof builder). +func NewPassthroughRouter( + reader DBReader, + writer DBWriter, + iteratorBuilder DBIteratorBuilder, + proofBuilder DBProofBuilder, +) (*PassthroughRouter, error) { + if reader == nil { + return nil, fmt.Errorf("reader must not be nil") + } + if writer == nil { + return nil, fmt.Errorf("writer must not be nil") + } + return &PassthroughRouter{ + reader: reader, + writer: writer, + iteratorBuilder: iteratorBuilder, + proofBuilder: proofBuilder, + }, nil +} + +// Read forwards directly to the wrapped reader. +func (p *PassthroughRouter) Read(store string, key []byte) ([]byte, bool, error) { + return p.reader(store, key) +} + +// ApplyChangeSets forwards directly to the wrapped writer. The router +// performs no per-changeset name validation; the writer (and its +// backing store) is the sole authority on which names it accepts. +func (p *PassthroughRouter) ApplyChangeSets(changesets []*proto.NamedChangeSet) error { + return p.writer(changesets) +} + +// Iterator forwards to the wrapped iterator builder. If no iterator +// builder was supplied, returns an error describing the limitation. +func (p *PassthroughRouter) Iterator(store string, start []byte, end []byte, ascending bool) (db.Iterator, error) { + if p.iteratorBuilder == nil { + return nil, fmt.Errorf("iteration not supported by passthrough router (store=%q)", store) + } + return p.iteratorBuilder(store, start, end, ascending) +} + +// GetProof forwards to the wrapped proof builder. If no proof builder +// was supplied, returns an error describing the limitation. +func (p *PassthroughRouter) GetProof(store string, key []byte) (*ics23.CommitmentProof, error) { + if p.proofBuilder == nil { + return nil, fmt.Errorf("proofs not supported by passthrough router (store=%q)", store) + } + return p.proofBuilder(store, key) +} diff --git a/sei-db/state_db/sc/migration/passthrough_router_test.go b/sei-db/state_db/sc/migration/passthrough_router_test.go new file mode 100644 index 0000000000..f2bb71cd55 --- /dev/null +++ b/sei-db/state_db/sc/migration/passthrough_router_test.go @@ -0,0 +1,199 @@ +package migration + +import ( + "errors" + "testing" + + ics23 "github.com/confio/ics23/go" + "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" +) + +// newPassthroughRouterForTest wires the standard mockDB reader/writer +// into a PassthroughRouter with nil iterator/proof builders. Tests +// that need iteration or proofs construct the router directly. +func newPassthroughRouterForTest(t *testing.T) (*PassthroughRouter, *mockDB) { + t.Helper() + db := newMockDB() + r, err := NewPassthroughRouter(db.reader(), db.writer(), nil, nil) + require.NoError(t, err) + return r, db +} + +// TestPassthroughRouterRequiresReader verifies that NewPassthroughRouter +// rejects a nil reader. The router has no internal default and would +// nil-panic on the first Read call if we let it through. +func TestPassthroughRouterRequiresReader(t *testing.T) { + db := newMockDB() + r, err := NewPassthroughRouter(nil, db.writer(), nil, nil) + require.Error(t, err) + require.Nil(t, r) + require.Contains(t, err.Error(), "reader") +} + +// TestPassthroughRouterRequiresWriter verifies that NewPassthroughRouter +// rejects a nil writer. ApplyChangeSets has no fallback path. +func TestPassthroughRouterRequiresWriter(t *testing.T) { + db := newMockDB() + r, err := NewPassthroughRouter(db.reader(), nil, nil, nil) + require.Error(t, err) + require.Nil(t, r) + require.Contains(t, err.Error(), "writer") +} + +// TestPassthroughRouterReadForwardsAnyName is the core property test: +// the passthrough router never inspects the store name. Reads for +// names that are not in keys.MemIAVLStoreKeys (e.g. icahost) must +// still hit the backend. +func TestPassthroughRouterReadForwardsAnyName(t *testing.T) { + r, db := newPassthroughRouterForTest(t) + db.seed(map[string]map[string][]byte{ + "icahost": {"k": []byte("v")}, + }) + + got, ok, err := r.Read("icahost", []byte("k")) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, []byte("v"), got) +} + +// TestPassthroughRouterReadPropagatesReaderError verifies that a +// reader error is returned verbatim rather than masked by a routing +// error. ModuleRouter would have rejected unknown names before +// calling the reader; the passthrough router must not. +func TestPassthroughRouterReadPropagatesReaderError(t *testing.T) { + sentinel := errors.New("backend exploded") + r, err := NewPassthroughRouter(failReader(sentinel), newMockDB().writer(), nil, nil) + require.NoError(t, err) + + _, _, err = r.Read("anything", []byte("k")) + require.ErrorIs(t, err, sentinel) +} + +// TestPassthroughRouterApplyChangeSetsForwardsAnyName verifies that +// writes to names outside keys.MemIAVLStoreKeys are accepted and +// persisted. The mockDB writer records the raw batch so we can +// confirm the changesets reach it unmodified. +func TestPassthroughRouterApplyChangeSetsForwardsAnyName(t *testing.T) { + r, db := newPassthroughRouterForTest(t) + + batch := []*proto.NamedChangeSet{ + {Name: "icahost", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k1"), Value: []byte("v1")}, + }}}, + {Name: "icacontroller", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k2"), Value: []byte("v2")}, + }}}, + } + require.NoError(t, r.ApplyChangeSets(batch)) + + require.Len(t, db.writeLog, 1) + require.Equal(t, batch, db.writeLog[0]) + + v, ok := db.get("icahost", "k1") + require.True(t, ok) + require.Equal(t, []byte("v1"), v) + v, ok = db.get("icacontroller", "k2") + require.True(t, ok) + require.Equal(t, []byte("v2"), v) +} + +// TestPassthroughRouterApplyChangeSetsPropagatesWriterError verifies +// that the writer's error surfaces unwrapped. +func TestPassthroughRouterApplyChangeSetsPropagatesWriterError(t *testing.T) { + sentinel := errors.New("backend exploded") + r, err := NewPassthroughRouter(newMockDB().reader(), failWriter(sentinel), nil, nil) + require.NoError(t, err) + + err = r.ApplyChangeSets([]*proto.NamedChangeSet{{Name: "anything"}}) + require.ErrorIs(t, err, sentinel) +} + +// TestPassthroughRouterIteratorWithoutBuilder verifies that a router +// constructed without an iterator builder rejects Iterator() with a +// clean, descriptive error rather than nil-panicking. +func TestPassthroughRouterIteratorWithoutBuilder(t *testing.T) { + r, _ := newPassthroughRouterForTest(t) + + it, err := r.Iterator("icahost", nil, nil, true) + require.Error(t, err) + require.Nil(t, it) + require.Contains(t, err.Error(), "iteration not supported") + require.Contains(t, err.Error(), "icahost") +} + +// TestPassthroughRouterIteratorForwardsToBuilder verifies that when an +// iterator builder is supplied, calls forward with arguments intact +// and the builder's returned iterator/error are returned verbatim. +func TestPassthroughRouterIteratorForwardsToBuilder(t *testing.T) { + var captured struct { + store string + start []byte + end []byte + ascending bool + called bool + } + sentinelIter, err := dbm.NewMemDB().Iterator(nil, nil) + require.NoError(t, err) + t.Cleanup(func() { _ = sentinelIter.Close() }) + + builder := func(store string, start, end []byte, ascending bool) (dbm.Iterator, error) { + captured.store = store + captured.start = start + captured.end = end + captured.ascending = ascending + captured.called = true + return sentinelIter, nil + } + r, err2 := NewPassthroughRouter(newMockDB().reader(), newMockDB().writer(), builder, nil) + require.NoError(t, err2) + + got, err := r.Iterator("icahost", []byte("s"), []byte("e"), true) + require.NoError(t, err) + require.True(t, captured.called) + require.Equal(t, "icahost", captured.store) + require.Equal(t, []byte("s"), captured.start) + require.Equal(t, []byte("e"), captured.end) + require.True(t, captured.ascending) + require.Equal(t, sentinelIter, got) +} + +// TestPassthroughRouterGetProofWithoutBuilder verifies the proof path +// is symmetric with iterator: missing builder yields a clear error. +func TestPassthroughRouterGetProofWithoutBuilder(t *testing.T) { + r, _ := newPassthroughRouterForTest(t) + + p, err := r.GetProof("icahost", []byte("k")) + require.Error(t, err) + require.Nil(t, p) + require.Contains(t, err.Error(), "proofs not supported") + require.Contains(t, err.Error(), "icahost") +} + +// TestPassthroughRouterGetProofForwardsToBuilder verifies that when a +// proof builder is supplied, the call forwards with arguments intact +// and the builder's output is returned verbatim. +func TestPassthroughRouterGetProofForwardsToBuilder(t *testing.T) { + want := &ics23.CommitmentProof{} + var captured struct { + store string + key []byte + called bool + } + builder := func(store string, key []byte) (*ics23.CommitmentProof, error) { + captured.store = store + captured.key = key + captured.called = true + return want, nil + } + r, err := NewPassthroughRouter(newMockDB().reader(), newMockDB().writer(), nil, builder) + require.NoError(t, err) + + got, err := r.GetProof("icahost", []byte("k")) + require.NoError(t, err) + require.True(t, captured.called) + require.Equal(t, "icahost", captured.store) + require.Equal(t, []byte("k"), captured.key) + require.Same(t, want, got) +} diff --git a/sei-db/state_db/sc/migration/router_builder.go b/sei-db/state_db/sc/migration/router_builder.go index 7c06c30bfd..73261e8ccb 100644 --- a/sei-db/state_db/sc/migration/router_builder.go +++ b/sei-db/state_db/sc/migration/router_builder.go @@ -7,6 +7,7 @@ import ( ics23 "github.com/confio/ics23/go" "github.com/sei-protocol/sei-chain/sei-db/common/keys" + "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/memiavl" @@ -17,21 +18,21 @@ import ( // reads/writes between the memiavl and flatkv backends. func BuildRouter( ctx context.Context, - writeMode WriteMode, + writeMode config.WriteMode, memIAVL *memiavl.CommitStore, - flatKV *flatkv.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, ) (Router, error) { switch writeMode { - case MemiavlOnly: + case config.MemiavlOnly: router, err := buildMemiavlOnlyRouter(memIAVL) if err != nil { return nil, fmt.Errorf("buildMemiavlOnlyRouter: %w", err) } return router, nil - case MigrateEVM: + case config.MigrateEVM: router, err := buildMigrateEVMRouter(ctx, memIAVL, flatKV, migrationBatchSize) if err != nil { return nil, fmt.Errorf("buildMigrateEVMRouter: %w", err) @@ -41,7 +42,7 @@ func BuildRouter( return nil, fmt.Errorf("NewThreadSafeRouter: %w", err) } return threadSafe, nil - case EVMMigrated: + case config.EVMMigrated: router, err := buildEVMMigratedRouter(memIAVL, flatKV) if err != nil { return nil, fmt.Errorf("buildEVMMigratedRouter: %w", err) @@ -51,7 +52,7 @@ func BuildRouter( return nil, fmt.Errorf("NewThreadSafeRouter: %w", err) } return threadSafe, nil - case MigrateAllButBank: + case config.MigrateAllButBank: router, err := buildMigrateAllButBankRouter(ctx, memIAVL, flatKV, migrationBatchSize) if err != nil { return nil, fmt.Errorf("buildMigrateAllButBankRouter: %w", err) @@ -61,7 +62,7 @@ func BuildRouter( return nil, fmt.Errorf("NewThreadSafeRouter: %w", err) } return threadSafe, nil - case AllMigratedButBank: + case config.AllMigratedButBank: router, err := buildAllMigratedButBankRouter(memIAVL, flatKV) if err != nil { return nil, fmt.Errorf("buildAllMigratedButBankRouter: %w", err) @@ -71,7 +72,7 @@ func BuildRouter( return nil, fmt.Errorf("NewThreadSafeRouter: %w", err) } return threadSafe, nil - case MigrateBank: + case config.MigrateBank: router, err := buildMigrateBankRouter(ctx, memIAVL, flatKV, migrationBatchSize) if err != nil { return nil, fmt.Errorf("buildMigrateBankRouter: %w", err) @@ -81,13 +82,13 @@ func BuildRouter( return nil, fmt.Errorf("NewThreadSafeRouter: %w", err) } return threadSafe, nil - case FlatKVOnly: + case config.FlatKVOnly: router, err := buildFlatKVOnlyRouter(flatKV) if err != nil { return nil, fmt.Errorf("buildFlatKVOnlyRouter: %w", err) } return router, nil - case TestOnlyDualWrite: + case config.TestOnlyDualWrite: router, err := buildTestOnlyDualWriteRouter(memIAVL, flatKV) if err != nil { return nil, fmt.Errorf("buildTestOnlyDualWriteRouter: %w", err) @@ -104,9 +105,9 @@ func BuildRouter( /* Data flow: MemiavlOnly (0) - ┌──────────────┐ ┌─────────┐ -──all-modules────────▶ │ moduleRouter │ ──────────all-modules──────────▶ │ memIAVL │ - └──────────────┘ └─────────┘ + ┌─────────────┐ ┌─────────┐ +──all-modules────────▶ │ passthrough │ ──────────all-modules──────────▶ │ memIAVL │ + └─────────────┘ └─────────┘ */ // Build a router for handling write mode MemiavlOnly. Operates on a schema at migration version 0. @@ -117,14 +118,14 @@ func buildMemiavlOnlyRouter( return nil, fmt.Errorf("memIAVL is nil") } - route, err := routeToMemIAVL(memIAVL, keys.MemIAVLStoreKeys...) - if err != nil { - return nil, fmt.Errorf("routeToMemIAVL: %w", err) - } - - router, err := NewModuleRouter(route) + router, err := NewPassthroughRouter( + buildMemIAVLReader(memIAVL), + buildMemIAVLWriter(memIAVL), + buildMemIAVLIteratorBuilder(memIAVL), + buildMemIAVLProofBuilder(memIAVL), + ) if err != nil { - return nil, fmt.Errorf("NewModuleRouter: %w", err) + return nil, fmt.Errorf("NewPassthroughRouter: %w", err) } return router, nil @@ -149,7 +150,7 @@ func buildMemiavlOnlyRouter( func buildMigrateEVMRouter( ctx context.Context, memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, migrationBatchSize int, ) (Router, error) { @@ -219,7 +220,7 @@ func buildMigrateEVMRouter( // Build a router for handling write mode EVMMigrated. Operates on a schema at migration version 1. func buildEVMMigratedRouter( memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, ) (Router, error) { if memIAVL == nil { @@ -273,7 +274,7 @@ func buildEVMMigratedRouter( func buildMigrateAllButBankRouter( ctx context.Context, memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, migrationBatchSize int, ) (Router, error) { @@ -349,7 +350,7 @@ func buildMigrateAllButBankRouter( // Build a router for handling write mode AllMigratedButBank. Operates on a schema at migration version 2. func buildAllMigratedButBankRouter( memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, ) (Router, error) { if memIAVL == nil { @@ -402,7 +403,7 @@ func buildAllMigratedButBankRouter( func buildMigrateBankRouter( ctx context.Context, memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, migrationBatchSize int, ) (Router, error) { @@ -459,27 +460,27 @@ func buildMigrateBankRouter( /* Data flow: FlatKVOnly (3) - ┌──────────────┐ ┌────────┐ -──all-modules────────▶ │ moduleRouter │ ──────────all-modules──────────▶ │ flatKV │ - └──────────────┘ └────────┘ + ┌─────────────┐ ┌────────┐ +──all-modules────────▶ │ passthrough │ ──────────all-modules──────────▶ │ flatKV │ + └─────────────┘ └────────┘ */ // Build a router for handling write mode FlatKVOnly. Operates on a schema at migration version 3. func buildFlatKVOnlyRouter( - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, ) (Router, error) { if flatKV == nil { return nil, fmt.Errorf("flatKV is nil") } - route, err := routeToFlatKV(flatKV, keys.MemIAVLStoreKeys...) - if err != nil { - return nil, fmt.Errorf("routeToFlatKV: %w", err) - } - - router, err := NewModuleRouter(route) + router, err := NewPassthroughRouter( + buildFlatKVReader(flatKV), + buildFlatKVWriter(flatKV), + nil, // iteration not supported by flatkv + nil, // proof building not supported by flatkv + ) if err != nil { - return nil, fmt.Errorf("NewModuleRouter: %w", err) + return nil, fmt.Errorf("NewPassthroughRouter: %w", err) } return router, nil @@ -505,7 +506,7 @@ func buildFlatKVOnlyRouter( // CRITICAL: this is a test-only router and should never be deployed to production machines. func buildTestOnlyDualWriteRouter( memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, ) (Router, error) { if memIAVL == nil { return nil, fmt.Errorf("memIAVL is nil") @@ -597,7 +598,7 @@ func buildMemIAVLProofBuilder(memIAVL *memiavl.CommitStore) DBProofBuilder { } // Build a function capable of reading data from flatkv. -func buildFlatKVReader(flatKV *flatkv.CommitStore) DBReader { +func buildFlatKVReader(flatKV flatkv.Store) DBReader { return func(store string, key []byte) ([]byte, bool, error) { value, found := flatKV.Get(store, key) return value, found, nil @@ -605,7 +606,7 @@ func buildFlatKVReader(flatKV *flatkv.CommitStore) DBReader { } // Build a function capable of writing data to flatkv. -func buildFlatKVWriter(flatKV *flatkv.CommitStore) DBWriter { +func buildFlatKVWriter(flatKV flatkv.Store) DBWriter { return func(changesets []*proto.NamedChangeSet) error { err := flatKV.ApplyChangeSets(changesets) if err != nil { @@ -627,7 +628,7 @@ func routeToMemIAVL(memIAVL *memiavl.CommitStore, moduleNames ...string) (*Route } // Build a route to a flatkv store for the given module names. -func routeToFlatKV(flatKV *flatkv.CommitStore, moduleNames ...string) (*Route, error) { +func routeToFlatKV(flatKV flatkv.Store, moduleNames ...string) (*Route, error) { return NewRoute( buildFlatKVReader(flatKV), buildFlatKVWriter(flatKV), diff --git a/sei-db/state_db/sc/migration/write_mode.go b/sei-db/state_db/sc/migration/write_mode.go deleted file mode 100644 index d5a9775c9b..0000000000 --- a/sei-db/state_db/sc/migration/write_mode.go +++ /dev/null @@ -1,78 +0,0 @@ -package migration - -import "fmt" - -// WriteMode is the migration-package-local enumeration of the migrate-and-steady-state write -// strategies that the migration router supports. The pure single-backend layouts (memiavl-only -// and flatkv-only) are reached by NOT calling BuildRouter at all and instead writing through the -// underlying store handles directly. Eventually will be merged into config.WriteMode. -type WriteMode string - -const ( - - // MemiavlOnly writes all data to memiavl only. - // - // Migration version 0. - MemiavlOnly WriteMode = "memiavl_only" - - // MigrateEVM migrates the evm/ module from memiavl to flatkv. - // - // Handles the transition from migration version 0 to 1, - // and continues to function once we reach migration version 1. - MigrateEVM WriteMode = "migrate_evm" - - // EVMMigrated is the steady state after the evm/ module has been migrated, but before we - // are ready to do the next migration. - // - // Migration version 1. - EVMMigrated WriteMode = "evm_migrated" - - // MigrateAllButBank migrates all but the bank module from memiavl to flatkv. - // - // Handles the transition from migration version 1 to 2, - // and continues to function once we reach migration version 2. - MigrateAllButBank WriteMode = "migrate_all_but_bank" - - // AllMigratedButBank is the steady state after all but the bank module has been migrated, - // but before we are ready to do the next migration. - // - // Migration version 2. - AllMigratedButBank WriteMode = "all_migrated_but_bank" - - // MigrateBank migrates the bank module from memiavl to flatkv. - // - // Handles the transition from migration version 2 to 3, - // and continues to function once we reach migration version 3. - MigrateBank WriteMode = "migrate_bank" - - // All data is written to FlatKV. - // - // Migration version 3. - FlatKVOnly WriteMode = "flatkv_only" - - // TestOnlyDualWrite is a test-only dual-write router. EVM traffic is written to both memiavl and flatkv, - // but all other traffic is written to memiavl only. - // - // CRITICAL: this is a test-only router and should never be deployed to production machines. - TestOnlyDualWrite WriteMode = "test_only_dual_write" -) - -// IsValid returns true if the migration write mode is a recognized value. -func (m WriteMode) IsValid() bool { - switch m { - case MemiavlOnly, MigrateEVM, EVMMigrated, - MigrateAllButBank, AllMigratedButBank, MigrateBank, FlatKVOnly, TestOnlyDualWrite: - return true - default: - return false - } -} - -// ParseWriteMode converts a string to a migration WriteMode, returning an error if invalid. -func ParseWriteMode(s string) (WriteMode, error) { - m := WriteMode(s) - if !m.IsValid() { - return "", fmt.Errorf("invalid migration write mode: %s", s) - } - return m, nil -} diff --git a/sei-db/state_db/sc/types/types.go b/sei-db/state_db/sc/types/types.go index 9bf63f6113..ad469d46c5 100644 --- a/sei-db/state_db/sc/types/types.go +++ b/sei-db/state_db/sc/types/types.go @@ -21,7 +21,8 @@ type Committer interface { // Initialize records the set of child store (tree) names that should be // created when the database is first opened with no prior state. It has // no effect on a non-empty database. Must be called before LoadVersion. - Initialize(initialStores []string) + // Rejects store names not in keys.MemIAVLStoreKeys. + Initialize(initialStores []string) error // Commit persists the current working state and returns the version // number assigned to the new commit. After a successful Commit the @@ -37,17 +38,17 @@ type Committer interface { // been opened at an older height. GetLatestVersion() (int64, error) - // GetEarliestVersion returns the lowest version still retained on disk. - // Versions below this have been pruned and are no longer queryable. - GetEarliestVersion() (int64, error) - - // Get returns the value for a key in a given store. + // Get returns the value for a key in a given store. Returns an error + // if store is not routable (i.e. not a member of + // keys.MemIAVLStoreKeys). // // Get(store, key) is a replacement for GetChildStoreByName(store).Get(key). The // GetChildStoreByName(store).Get(key) pathway is deprecated. Get(store string, key []byte) (value []byte, ok bool, err error) - // Has returns true if a key exists in a given store. + // Has returns true if a key exists in a given store. Returns an error + // if store is not routable (i.e. not a member of + // keys.MemIAVLStoreKeys). // // Has(store, key) is a replacement for GetChildStoreByName(store).Has(key). The // GetChildStoreByName(store).Has(key) pathway is deprecated. @@ -60,20 +61,18 @@ type Committer interface { ApplyChangeSets(cs []*proto.NamedChangeSet) error // Iterator returns an iterator over a range of keys in a given store. - // - // Note that this method may not be supported for some stores. For example, after evm/ data is migrated to flatKV, - // iteration is not supported for the evm/ store. If this method is called for an unsupported store, it will return - // an error. + // Returns an error if store is not routable (i.e. not a member of + // keys.MemIAVLStoreKeys), or if the routed backend does not support + // iteration for that store (e.g. evm/ once migrated to flatKV). // // Iterator(store, start, end, ascending) is a replacement for // GetChildStoreByName(store).Iterator(start, end, ascending), which is a deprecated pathway. Iterator(store string, start []byte, end []byte, ascending bool) (dbm.Iterator, error) // GetProof returns a proof of the value for a key in a given store. - // - // Note that this method may not be supported for some stores. For example, after evm/ data is migrated to flatKV, - // proofs are not supported for the evm/ store. If this method is called for an unsupported store, it will return - // an error. + // Returns an error if store is not routable (i.e. not a member of + // keys.MemIAVLStoreKeys), or if the routed backend does not support + // proofs for that store (e.g. evm/ once migrated to flatKV). // // GetProof(store, key) is a replacement for GetChildStoreByName(store).GetProof(key), which is a deprecated // pathway. @@ -119,8 +118,9 @@ type Committer interface { SetInitialVersion(initialVersion int64) error // GetChildStoreByName returns the CommitKVStore for the named child - // store, or nil if no such store exists. The returned store shares - // state with the Committer and must not be used after Close. + // store. Method calls on the returned store panic if name is not + // routable. The returned store shares state with the Committer and + // must not be used after Close. GetChildStoreByName(name string) CommitKVStore // Copy returns an in-memory snapshot of the current committer state. diff --git a/x/evm/keeper/trace_snapshot_test.go b/x/evm/keeper/trace_snapshot_test.go index 831dc76609..4ee9520798 100644 --- a/x/evm/keeper/trace_snapshot_test.go +++ b/x/evm/keeper/trace_snapshot_test.go @@ -11,6 +11,8 @@ import ( dbm "github.com/tendermint/tm-db" ) +var _ sctypes.Committer = (*fakeCommitter)(nil) + // fakeCommitter is a minimal Committer stub that records Close so we can // assert lifecycle. Methods the snapshot store doesn't touch panic to make // surprise calls visible. @@ -23,10 +25,9 @@ type fakeCommitter struct { func (f *fakeCommitter) Close() error { atomic.StoreInt32(&f.closed, 1); return nil } func (f *fakeCommitter) IsClosed() bool { return atomic.LoadInt32(&f.closed) == 1 } func (f *fakeCommitter) Version() int64 { return f.id } -func (f *fakeCommitter) Initialize(_ []string) { panic("unused") } +func (f *fakeCommitter) Initialize(_ []string) error { panic("unused") } func (f *fakeCommitter) Commit() (int64, error) { panic("unused") } func (f *fakeCommitter) GetLatestVersion() (int64, error) { panic("unused") } -func (f *fakeCommitter) GetEarliestVersion() (int64, error) { panic("unused") } func (f *fakeCommitter) Get(string, []byte) ([]byte, bool, error) { panic("unused") } func (f *fakeCommitter) Has(string, []byte) (bool, error) { panic("unused") } func (f *fakeCommitter) Iterator(string, []byte, []byte, bool) (dbm.Iterator, error) {