From 343f9419de1d3b478fefe4435bdb41034dad32f7 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Sun, 22 Feb 2026 18:47:27 -0800 Subject: [PATCH 1/4] Add RocksDB backend support for EVM sub-databases Introduce an EVMDBEngine interface and backend registry (matching the existing pattern in ss/pebbledb_init.go and ss/rocksdb_init.go) so that EVM sub-databases can use either PebbleDB or RocksDB. New files: - evm/engine.go: EVMDBEngine interface + RegisterEVMBackend registry - evm/db_rocksdb.go: RocksDB implementation using user-defined timestamps and column families (mirrors the rocksdb/mvcc pattern), includes timestamp comparator and backend registration Modified files: - evm/db.go: registers PebbleDB EVM backend at init - evm/store.go: EVMStateStore uses EVMDBEngine interface; NewEVMStateStore accepts a backend name parameter - composite/store.go: NewCompositeStateStore accepts a pre-created cosmos store (removing the hardcoded pebbledb/mvcc import) and passes the backend name through to NewEVMStateStore - ss/store.go: NewStateStore uses the backend registry to create the cosmos store, then passes it to the composite store Both Cosmos_SS and EVM_SS now respect ssConfig.Backend for engine selection. Co-authored-by: Cursor --- sei-db/state_db/ss/composite/recovery_test.go | 6 +- sei-db/state_db/ss/composite/store.go | 22 +- sei-db/state_db/ss/composite/store_test.go | 56 +-- sei-db/state_db/ss/evm/db.go | 6 + sei-db/state_db/ss/evm/db_rocksdb.go | 340 ++++++++++++++++++ sei-db/state_db/ss/evm/db_test.go | 6 +- sei-db/state_db/ss/evm/engine.go | 37 ++ sei-db/state_db/ss/evm/store.go | 41 ++- sei-db/state_db/ss/store.go | 14 +- 9 files changed, 473 insertions(+), 55 deletions(-) create mode 100644 sei-db/state_db/ss/evm/db_rocksdb.go create mode 100644 sei-db/state_db/ss/evm/engine.go diff --git a/sei-db/state_db/ss/composite/recovery_test.go b/sei-db/state_db/ss/composite/recovery_test.go index 78ccaa97c2..cdedf6d675 100644 --- a/sei-db/state_db/ss/composite/recovery_test.go +++ b/sei-db/state_db/ss/composite/recovery_test.go @@ -55,7 +55,7 @@ func TestRecoverCompositeStateStore(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - evmStore, err := evm.NewEVMStateStore(ssConfig.EVMDBDirectory, log) + evmStore, err := evm.NewEVMStateStore(ssConfig.EVMDBDirectory, "pebbledb", log) require.NoError(t, err) defer evmStore.Close() @@ -185,7 +185,7 @@ func TestSyncEVMStoreBehind(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - evmStore, err := evm.NewEVMStateStore(ssConfig.EVMDBDirectory, log) + evmStore, err := evm.NewEVMStateStore(ssConfig.EVMDBDirectory, "pebbledb", log) require.NoError(t, err) // Create composite store using test helper - EVM store starts at version 0 @@ -315,7 +315,7 @@ func TestConstructorRecoversStalEVM(t *testing.T) { // Step 3: Open via NewCompositeStateStore -- EVM_SS starts at v0, Cosmos at v5. // The constructor must detect this and replay WAL to catch EVM up. - compositeStore, err := NewCompositeStateStore(ssConfig, dir, log) + compositeStore, err := testNewCompositeStateStore(ssConfig, dir, log) require.NoError(t, err) defer compositeStore.Close() diff --git a/sei-db/state_db/ss/composite/store.go b/sei-db/state_db/ss/composite/store.go index e12eba5ae3..ce77c3b6a7 100644 --- a/sei-db/state_db/ss/composite/store.go +++ b/sei-db/state_db/ss/composite/store.go @@ -9,7 +9,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/config" - "github.com/sei-protocol/sei-chain/sei-db/db_engine/pebbledb/mvcc" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/evm" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/pruning" @@ -24,8 +23,8 @@ import ( // Always created by NewStateStore; when WriteMode==CosmosOnlyWrite && ReadMode==CosmosOnlyRead, // evmStore is nil and the composite store behaves identically to a plain state store. type CompositeStateStore struct { - cosmosStore types.StateStore // Main MVCC PebbleDB for all modules - evmStore *evm.EVMStateStore // Separate EVM DBs with default comparer (nil if disabled) + cosmosStore types.StateStore // Main MVCC store for all modules (PebbleDB or RocksDB) + evmStore *evm.EVMStateStore // Separate EVM DBs (nil if disabled) pruningManager *pruning.Manager // Pruning lifecycle manager (nil if pruning disabled) config config.StateStoreConfig logger logger.Logger @@ -33,23 +32,21 @@ type CompositeStateStore struct { closeErr error } -// NewCompositeStateStore creates a new composite state store that manages both Cosmos_SS and EVM_SS. -// It initializes both stores internally and starts pruning on the composite store. +// NewCompositeStateStore wraps a pre-created Cosmos store with optional EVM stores. +// The cosmosStore is created by the caller (ss.NewStateStore) using the backend registry, +// so both PebbleDB and RocksDB are supported without a direct import here. // EVM stores are opened when ssConfig.EVMEnabled() returns true (derived from WriteMode/ReadMode). +// The same backend (ssConfig.Backend) is used for EVM sub-databases. func NewCompositeStateStore( + cosmosStore types.StateStore, ssConfig config.StateStoreConfig, homeDir string, log logger.Logger, ) (*CompositeStateStore, error) { - // Initialize Cosmos store (without pruning - we start pruning on composite) dbHome := utils.GetStateStorePath(homeDir, ssConfig.Backend) if ssConfig.DBDirectory != "" { dbHome = ssConfig.DBDirectory } - cosmosStore, err := mvcc.OpenDB(dbHome, ssConfig) - if err != nil { - return nil, fmt.Errorf("failed to create cosmos store: %w", err) - } cs := &CompositeStateStore{ cosmosStore: cosmosStore, @@ -64,13 +61,14 @@ func NewCompositeStateStore( evmDir = filepath.Join(homeDir, "data", "evm_ss") } - evmStore, err := evm.NewEVMStateStore(evmDir, log) + evmStore, err := evm.NewEVMStateStore(evmDir, ssConfig.Backend, log) if err != nil { _ = cosmosStore.Close() return nil, fmt.Errorf("failed to create EVM store: %w", err) } cs.evmStore = evmStore - log.Info("EVM state store enabled", "dir", evmDir, "writeMode", ssConfig.WriteMode, "readMode", ssConfig.ReadMode) + log.Info("EVM state store enabled", "dir", evmDir, "backend", ssConfig.Backend, + "writeMode", ssConfig.WriteMode, "readMode", ssConfig.ReadMode) } // Recover from WAL if needed (handles EVM_SS being behind Cosmos_SS) diff --git a/sei-db/state_db/ss/composite/store_test.go b/sei-db/state_db/ss/composite/store_test.go index 438d40f2bc..114e833255 100644 --- a/sei-db/state_db/ss/composite/store_test.go +++ b/sei-db/state_db/ss/composite/store_test.go @@ -9,13 +9,29 @@ import ( commonevm "github.com/sei-protocol/sei-chain/sei-db/common/evm" "github.com/sei-protocol/sei-chain/sei-db/common/logger" + "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/config" + "github.com/sei-protocol/sei-chain/sei-db/db_engine/pebbledb/mvcc" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/evm" iavl "github.com/sei-protocol/sei-chain/sei-iavl" "github.com/stretchr/testify/require" ) +// testNewCompositeStateStore opens a PebbleDB cosmos backend and creates a +// CompositeStateStore. Test-only helper to avoid repeating the two-step creation. +func testNewCompositeStateStore(ssConfig config.StateStoreConfig, homeDir string, log logger.Logger) (*CompositeStateStore, error) { + dbHome := utils.GetStateStorePath(homeDir, ssConfig.Backend) + if ssConfig.DBDirectory != "" { + dbHome = ssConfig.DBDirectory + } + cosmosStore, err := mvcc.OpenDB(dbHome, ssConfig) + if err != nil { + return nil, err + } + return NewCompositeStateStore(cosmosStore, ssConfig, homeDir, log) +} + func setupTestStores(t *testing.T) (*CompositeStateStore, string, func()) { dir, err := os.MkdirTemp("", "composite_store_test") require.NoError(t, err) @@ -29,7 +45,7 @@ func setupTestStores(t *testing.T) (*CompositeStateStore, string, func()) { EVMDBDirectory: filepath.Join(dir, "evm_ss"), } - compositeStore, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + compositeStore, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) cleanup := func() { @@ -182,7 +198,7 @@ func TestCompositeStateStoreWithoutEVM(t *testing.T) { } // Create composite store with EVM disabled (default cosmos_only modes) - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -436,7 +452,7 @@ func TestBug1Fix_WriteModeControlsEVMWrites(t *testing.T) { ReadMode: config.CosmosOnlyRead, } - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -477,7 +493,7 @@ func TestBug1Fix_WriteModeControlsEVMWrites(t *testing.T) { EVMDBDirectory: filepath.Join(dir, "evm_ss"), } - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -533,7 +549,7 @@ func TestBug1Fix_ReadModeControlsEVMReads(t *testing.T) { EVMDBDirectory: filepath.Join(dir, "evm_ss"), } - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -578,7 +594,7 @@ func TestBug1Fix_ReadModeControlsEVMReads(t *testing.T) { EVMDBDirectory: filepath.Join(dir, "evm_ss"), } - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -784,7 +800,7 @@ func TestCompositeStateStorePrunesBothStores(t *testing.T) { EVMDBDirectory: filepath.Join(dir, "evm_ss"), } - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -852,7 +868,7 @@ func TestE2E_AllEVMDBsReadableViaComposite(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -979,7 +995,7 @@ func TestE2E_MVCCConsistencyAcrossBothStores(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1057,7 +1073,7 @@ func TestE2E_NonEVMModulesUnaffectedByDualWrite(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1155,7 +1171,7 @@ func TestE2E_VersionConsistencyAfterSetLatestVersion(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1200,7 +1216,7 @@ func TestE2E_DeleteTombstonePropagatedToBothStores(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1302,7 +1318,7 @@ func TestE2E_FactoryMethodCreatesCorrectStoreType(t *testing.T) { EVMDBDirectory: filepath.Join(dir, "evm_ss"), } - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1329,7 +1345,7 @@ func TestE2E_FactoryMethodCreatesCorrectStoreType(t *testing.T) { } // Default WriteMode=CosmosOnlyWrite, ReadMode=CosmosOnlyRead → no EVM stores - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1358,7 +1374,7 @@ func TestFix1_SplitWriteStripsEVMFromCosmos(t *testing.T) { ssConfig.ReadMode = config.SplitRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1424,7 +1440,7 @@ func TestFix1_SplitWriteAsyncAlsoStrips(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1478,7 +1494,7 @@ func TestFix2_SplitReadNoCosmFallback(t *testing.T) { ssConfig.ReadMode = config.SplitRead // But reads from EVM only, no fallback ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1565,7 +1581,7 @@ func TestFix3_SetLatestVersionRespectsWriteMode(t *testing.T) { ReadMode: config.CosmosOnlyRead, } - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1607,7 +1623,7 @@ func TestFix3_SetLatestVersionRespectsWriteMode(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -1649,7 +1665,7 @@ func TestE2E_LargeChangesetParallelWrite(t *testing.T) { ssConfig.ReadMode = config.EVMFirstRead ssConfig.EVMDBDirectory = filepath.Join(dir, "evm_ss") - store, err := NewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) + store, err := testNewCompositeStateStore(ssConfig, dir, logger.NewNopLogger()) require.NoError(t, err) defer store.Close() diff --git a/sei-db/state_db/ss/evm/db.go b/sei-db/state_db/ss/evm/db.go index 1fd2554e9d..7c2a8715fd 100644 --- a/sei-db/state_db/ss/evm/db.go +++ b/sei-db/state_db/ss/evm/db.go @@ -443,3 +443,9 @@ func (it *EVMIterator) Next() { func (it *EVMIterator) Close() error { return nil } + +func init() { + RegisterEVMBackend("pebbledb", func(dir string, storeType EVMStoreType) (EVMDBEngine, error) { + return OpenDB(dir, storeType) + }) +} diff --git a/sei-db/state_db/ss/evm/db_rocksdb.go b/sei-db/state_db/ss/evm/db_rocksdb.go new file mode 100644 index 0000000000..67655bb419 --- /dev/null +++ b/sei-db/state_db/ss/evm/db_rocksdb.go @@ -0,0 +1,340 @@ +//go:build rocksdbBackend +// +build rocksdbBackend + +package evm + +import ( + "bytes" + "encoding/binary" + "fmt" + "path/filepath" + "runtime" + "sync/atomic" + + "github.com/linxGnu/grocksdb" + iavl "github.com/sei-protocol/sei-chain/sei-iavl" +) + +func init() { + RegisterEVMBackend("rocksdb", func(dir string, storeType EVMStoreType) (EVMDBEngine, error) { + return OpenRocksDB(dir, storeType) + }) +} + +const ( + rocksTimestampSize = 8 + + rocksCFDefault = "default" + rocksCFStateStorage = "state_storage" + + rocksLatestVersionKey = "s/latest" + rocksEarliestVersionKey = "s/earliest" +) + +var ( + rocksDefaultWriteOpts = grocksdb.NewDefaultWriteOptions() + rocksDefaultReadOpts = grocksdb.NewDefaultReadOptions() +) + +// EVMDatabaseRocksDB is a RocksDB-backed versioned KV store for a single EVM data type. +// Uses user-defined timestamps for MVCC versioning and a column family for state data. +type EVMDatabaseRocksDB struct { + storeType EVMStoreType + storage *grocksdb.DB + cfHandle *grocksdb.ColumnFamilyHandle + + tsLow int64 + latestVersion atomic.Int64 + earliestVersion atomic.Int64 +} + +// OpenRocksDB opens a RocksDB instance for one EVM sub-database. +func OpenRocksDB(dir string, storeType EVMStoreType) (*EVMDatabaseRocksDB, error) { + dbPath := filepath.Join(dir, StoreTypeName(storeType)) + + defaultOpts := grocksdb.NewDefaultOptions() + defaultOpts.SetCreateIfMissing(true) + defaultOpts.SetCreateIfMissingColumnFamilies(true) + + cfOpts := newRocksDBEVMOpts() + + db, cfHandles, err := grocksdb.OpenDbColumnFamilies( + defaultOpts, + dbPath, + []string{rocksCFDefault, rocksCFStateStorage}, + []*grocksdb.Options{defaultOpts, cfOpts}, + ) + if err != nil { + return nil, fmt.Errorf("failed to open EVM RocksDB %s: %w", StoreTypeName(storeType), err) + } + cfHandle := cfHandles[1] + + slice, err := db.GetFullHistoryTsLow(cfHandle) + if err != nil { + return nil, fmt.Errorf("failed to get full_history_ts_low: %w", err) + } + var tsLow int64 + tsLowBz := rocksCloneSlice(slice) + if len(tsLowBz) > 0 { + tsLow = int64(binary.LittleEndian.Uint64(tsLowBz)) + } + + earliest, err := rocksRetrieveVersion(db, rocksEarliestVersionKey) + if err != nil { + return nil, err + } + latest, err := rocksRetrieveVersion(db, rocksLatestVersionKey) + if err != nil { + return nil, err + } + + evmDB := &EVMDatabaseRocksDB{ + storeType: storeType, + storage: db, + cfHandle: cfHandle, + tsLow: tsLow, + } + evmDB.latestVersion.Store(latest) + evmDB.earliestVersion.Store(earliest) + + return evmDB, nil +} + +func newRocksDBEVMOpts() *grocksdb.Options { + opts := grocksdb.NewDefaultOptions() + opts.SetCreateIfMissing(true) + opts.SetComparator(createTimestampComparator()) + opts.IncreaseParallelism(runtime.NumCPU()) + opts.OptimizeLevelStyleCompaction(512 * 1024 * 1024) + opts.SetTargetFileSizeMultiplier(2) + opts.SetLevelCompactionDynamicLevelBytes(true) + + bbto := grocksdb.NewDefaultBlockBasedTableOptions() + bbto.SetBlockSize(32 * 1024) + bbto.SetBlockCache(grocksdb.NewLRUCache(256 << 20)) // 256 MB per sub-DB + bbto.SetFilterPolicy(grocksdb.NewRibbonHybridFilterPolicy(9.9, 1)) + bbto.SetIndexType(grocksdb.KBinarySearchWithFirstKey) + bbto.SetOptimizeFiltersForMemory(true) + opts.SetBlockBasedTableFactory(bbto) + + opts.SetCompressionOptionsParallelThreads(4) + opts.SetBottommostCompression(grocksdb.ZSTDCompression) + compressOpts := grocksdb.NewDefaultCompressionOptions() + compressOpts.MaxDictBytes = 112640 + compressOpts.Level = 12 + opts.SetBottommostCompressionOptions(compressOpts, true) + opts.SetBottommostCompressionOptionsZstdMaxTrainBytes(compressOpts.MaxDictBytes*100, true) + + return opts +} + +func (db *EVMDatabaseRocksDB) Get(key []byte, version int64) ([]byte, error) { + if version < db.earliestVersion.Load() { + return nil, nil + } + slice, err := db.storage.GetCF(rocksNewTSReadOpts(version), db.cfHandle, key) + if err != nil { + return nil, fmt.Errorf("rocksdb evm get: %w", err) + } + return rocksCloneSlice(slice), nil +} + +func (db *EVMDatabaseRocksDB) Has(key []byte, version int64) (bool, error) { + if version < db.earliestVersion.Load() { + return false, nil + } + slice, err := db.storage.GetCF(rocksNewTSReadOpts(version), db.cfHandle, key) + if err != nil { + return false, err + } + exists := slice.Exists() + slice.Free() + return exists, nil +} + +func (db *EVMDatabaseRocksDB) Set(key, value []byte, version int64) error { + var ts [rocksTimestampSize]byte + binary.LittleEndian.PutUint64(ts[:], uint64(version)) + + batch := grocksdb.NewWriteBatch() + batch.PutCFWithTS(db.cfHandle, key, ts[:], value) + batch.Put([]byte(rocksLatestVersionKey), ts[:]) + defer batch.Destroy() + + if err := db.storage.Write(rocksDefaultWriteOpts, batch); err != nil { + return err + } + db.latestVersion.Store(version) + return nil +} + +func (db *EVMDatabaseRocksDB) Delete(key []byte, version int64) error { + var ts [rocksTimestampSize]byte + binary.LittleEndian.PutUint64(ts[:], uint64(version)) + + batch := grocksdb.NewWriteBatch() + batch.DeleteCFWithTS(db.cfHandle, key, ts[:]) + defer batch.Destroy() + + if err := db.storage.Write(rocksDefaultWriteOpts, batch); err != nil { + return err + } + return nil +} + +func (db *EVMDatabaseRocksDB) ApplyBatch(pairs []*iavl.KVPair, version int64) error { + if len(pairs) == 0 { + return nil + } + + var ts [rocksTimestampSize]byte + binary.LittleEndian.PutUint64(ts[:], uint64(version)) + + batch := grocksdb.NewWriteBatch() + batch.Put([]byte(rocksLatestVersionKey), ts[:]) + for _, pair := range pairs { + if pair.Value == nil || pair.Delete { + batch.DeleteCFWithTS(db.cfHandle, pair.Key, ts[:]) + } else { + batch.PutCFWithTS(db.cfHandle, pair.Key, ts[:], pair.Value) + } + } + defer batch.Destroy() + + if err := db.storage.Write(rocksDefaultWriteOpts, batch); err != nil { + return err + } + db.latestVersion.Store(version) + return nil +} + +func (db *EVMDatabaseRocksDB) GetLatestVersion() int64 { + return db.latestVersion.Load() +} + +func (db *EVMDatabaseRocksDB) SetLatestVersion(version int64) error { + var ts [rocksTimestampSize]byte + binary.LittleEndian.PutUint64(ts[:], uint64(version)) + if err := db.storage.Put(rocksDefaultWriteOpts, []byte(rocksLatestVersionKey), ts[:]); err != nil { + return err + } + db.latestVersion.Store(version) + return nil +} + +func (db *EVMDatabaseRocksDB) GetEarliestVersion() int64 { + return db.earliestVersion.Load() +} + +func (db *EVMDatabaseRocksDB) SetEarliestVersion(version int64) error { + if version <= db.earliestVersion.Load() { + return nil + } + var ts [rocksTimestampSize]byte + binary.LittleEndian.PutUint64(ts[:], uint64(version)) + if err := db.storage.Put(rocksDefaultWriteOpts, []byte(rocksEarliestVersionKey), ts[:]); err != nil { + return err + } + db.earliestVersion.Store(version) + return nil +} + +// Prune leverages RocksDB's IncreaseFullHistoryTsLow for lazy pruning. +// Old versions are dropped during subsequent compactions. +func (db *EVMDatabaseRocksDB) Prune(version int64) error { + if db.storage == nil { + return fmt.Errorf("rocksdb: database is closed") + } + tsLow := version + 1 + var ts [rocksTimestampSize]byte + binary.LittleEndian.PutUint64(ts[:], uint64(tsLow)) + + if err := db.storage.IncreaseFullHistoryTsLow(db.cfHandle, ts[:]); err != nil { + return fmt.Errorf("failed to update full_history_ts_low: %w", err) + } + db.tsLow = tsLow + return db.SetEarliestVersion(tsLow) +} + +func (db *EVMDatabaseRocksDB) Close() error { + if db.storage == nil { + return nil + } + db.cfHandle = nil + db.storage.Close() + db.storage = nil + return nil +} + +// --- helpers --- + +func rocksNewTSReadOpts(version int64) *grocksdb.ReadOptions { + var ts [rocksTimestampSize]byte + binary.LittleEndian.PutUint64(ts[:], uint64(version)) + ro := grocksdb.NewDefaultReadOptions() + ro.SetTimestamp(ts[:]) + return ro +} + +func rocksRetrieveVersion(db *grocksdb.DB, key string) (int64, error) { + bz, err := db.GetBytes(rocksDefaultReadOpts, []byte(key)) + if err != nil || len(bz) == 0 { + return 0, err + } + return int64(binary.LittleEndian.Uint64(bz)), nil +} + +func rocksCloneSlice(s *grocksdb.Slice) []byte { + defer s.Free() + if !s.Exists() { + return nil + } + out := make([]byte, len(s.Data())) + copy(out, s.Data()) + return out +} + +// --- timestamp comparator --- + +// createTimestampComparator returns a comparator identical to the RocksDB builtin +// "leveldb.BytewiseComparator.u64ts" so that ldb/sst_dump can work with these DBs. +func createTimestampComparator() *grocksdb.Comparator { + return grocksdb.NewComparatorWithTimestamp( + "leveldb.BytewiseComparator.u64ts", + rocksTimestampSize, + rocksCompare, + rocksCompareTS, + rocksCompareWithoutTS, + ) +} + +func rocksCompareTS(a, b []byte) int { + ts1 := binary.LittleEndian.Uint64(a) + ts2 := binary.LittleEndian.Uint64(b) + switch { + case ts1 < ts2: + return -1 + case ts1 > ts2: + return 1 + default: + return 0 + } +} + +func rocksCompare(a, b []byte) int { + ret := rocksCompareWithoutTS(a, true, b, true) + if ret != 0 { + return ret + } + return -rocksCompareTS(a[len(a)-rocksTimestampSize:], b[len(b)-rocksTimestampSize:]) +} + +func rocksCompareWithoutTS(a []byte, aHasTS bool, b []byte, bHasTS bool) int { + if aHasTS { + a = a[:len(a)-rocksTimestampSize] + } + if bHasTS { + b = b[:len(b)-rocksTimestampSize] + } + return bytes.Compare(a, b) +} diff --git a/sei-db/state_db/ss/evm/db_test.go b/sei-db/state_db/ss/evm/db_test.go index 1b8274a0b6..cb1ff2a0cb 100644 --- a/sei-db/state_db/ss/evm/db_test.go +++ b/sei-db/state_db/ss/evm/db_test.go @@ -169,7 +169,7 @@ func TestEVMStateStore(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(dir) - store, err := NewEVMStateStore(dir, logger.NewNopLogger()) + store, err := NewEVMStateStore(dir, "pebbledb", logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -221,7 +221,7 @@ func TestEVMStateStoreParallel(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(dir) - store, err := NewEVMStateStore(dir, logger.NewNopLogger()) + store, err := NewEVMStateStore(dir, "pebbledb", logger.NewNopLogger()) require.NoError(t, err) defer store.Close() @@ -487,7 +487,7 @@ func TestCodeSizeGoesToLegacyDB(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(dir) - store, err := NewEVMStateStore(dir, logger.NewNopLogger()) + store, err := NewEVMStateStore(dir, "pebbledb", logger.NewNopLogger()) require.NoError(t, err) defer store.Close() diff --git a/sei-db/state_db/ss/evm/engine.go b/sei-db/state_db/ss/evm/engine.go new file mode 100644 index 0000000000..43c9f9b167 --- /dev/null +++ b/sei-db/state_db/ss/evm/engine.go @@ -0,0 +1,37 @@ +package evm + +import iavl "github.com/sei-protocol/sei-chain/sei-iavl" + +// EVMDBEngine abstracts a single versioned KV store for one EVM data type. +// Implementations exist for PebbleDB (default) and RocksDB (build tag: rocksdbBackend). +type EVMDBEngine interface { + Get(key []byte, version int64) ([]byte, error) + Has(key []byte, version int64) (bool, error) + Set(key, value []byte, version int64) error + Delete(key []byte, version int64) error + ApplyBatch(pairs []*iavl.KVPair, version int64) error + + GetLatestVersion() int64 + SetLatestVersion(version int64) error + GetEarliestVersion() int64 + SetEarliestVersion(version int64) error + + Prune(version int64) error + Close() error +} + +// EVMDBOpener is a factory function that opens an EVMDBEngine for a given store type. +type EVMDBOpener func(dir string, storeType EVMStoreType) (EVMDBEngine, error) + +var evmBackends = map[string]EVMDBOpener{} + +// RegisterEVMBackend registers a named backend factory for EVM sub-databases. +func RegisterEVMBackend(name string, opener EVMDBOpener) { + evmBackends[name] = opener +} + +// GetEVMBackend returns the registered opener for the given backend name. +func GetEVMBackend(name string) (EVMDBOpener, bool) { + opener, ok := evmBackends[name] + return opener, ok +} diff --git a/sei-db/state_db/ss/evm/store.go b/sei-db/state_db/ss/evm/store.go index ac9d56fbb8..bf4f41b716 100644 --- a/sei-db/state_db/ss/evm/store.go +++ b/sei-db/state_db/ss/evm/store.go @@ -18,11 +18,12 @@ type dbWrite struct { pairs []*iavl.KVPair } -// EVMStateStore manages multiple EVMDatabase instances, one per EVM data type. +// EVMStateStore manages multiple EVMDBEngine instances, one per EVM data type. // Each database has an optional background goroutine for async writes. type EVMStateStore struct { - databases map[EVMStoreType]*EVMDatabase + databases map[EVMStoreType]EVMDBEngine dir string + backend string logger logger.Logger // Per-DB async write channels and worker goroutines @@ -30,50 +31,58 @@ type EVMStateStore struct { asyncWg sync.WaitGroup } -// NewEVMStateStore creates a new EVM state store with all sub-databases. +// NewEVMStateStore creates a new EVM state store with all sub-databases +// using the specified backend ("pebbledb" or "rocksdb"). // Each database gets a background worker goroutine for async writes. -func NewEVMStateStore(dir string, log logger.Logger) (*EVMStateStore, error) { +func NewEVMStateStore(dir string, backend string, log logger.Logger) (*EVMStateStore, error) { + if backend == "" { + backend = "pebbledb" + } + + opener, ok := GetEVMBackend(backend) + if !ok { + return nil, fmt.Errorf("unsupported EVM backend: %s", backend) + } + store := &EVMStateStore{ - databases: make(map[EVMStoreType]*EVMDatabase), + databases: make(map[EVMStoreType]EVMDBEngine), asyncChs: make(map[EVMStoreType]chan dbWrite), dir: dir, + backend: backend, logger: log, } - // Open a database for each EVM store type for _, storeType := range AllEVMStoreTypes() { - db, err := OpenDB(dir, storeType) + db, err := opener(dir, storeType) if err != nil { - // Close any already opened DBs _ = store.Close() return nil, fmt.Errorf("failed to open EVM DB for %s: %w", StoreTypeName(storeType), err) } store.databases[storeType] = db - // Start per-DB background worker ch := make(chan dbWrite, asyncBufferSize) store.asyncChs[storeType] = ch store.asyncWg.Add(1) - go store.asyncWorker(db, ch) + go store.asyncWorker(storeType, db, ch) } return store, nil } // asyncWorker processes writes from a per-DB channel until it's closed. -func (s *EVMStateStore) asyncWorker(db *EVMDatabase, ch <-chan dbWrite) { +func (s *EVMStateStore) asyncWorker(storeType EVMStoreType, db EVMDBEngine, ch <-chan dbWrite) { defer s.asyncWg.Done() for w := range ch { if err := db.ApplyBatch(w.pairs, w.version); err != nil { - s.logger.Error("async EVM write failed", "storeType", StoreTypeName(db.storeType), "version", w.version, "error", err) + s.logger.Error("async EVM write failed", "storeType", StoreTypeName(storeType), "version", w.version, "error", err) continue } _ = db.SetLatestVersion(w.version) } } -// GetDB returns the database for a specific store type -func (s *EVMStateStore) GetDB(storeType EVMStoreType) *EVMDatabase { +// GetDB returns the database engine for a specific store type. +func (s *EVMStateStore) GetDB(storeType EVMStoreType) EVMDBEngine { return s.databases[storeType] } @@ -142,7 +151,7 @@ func (s *EVMStateStore) ApplyChangesetParallel(version int64, changes map[EVMSto } wg.Add(1) - go func(db *EVMDatabase, pairs []*iavl.KVPair) { + go func(db EVMDBEngine, pairs []*iavl.KVPair) { defer wg.Done() if err := db.ApplyBatch(pairs, version); err != nil { errCh <- err @@ -239,7 +248,7 @@ func (s *EVMStateStore) Prune(version int64) error { for _, db := range s.databases { wg.Add(1) - go func(db *EVMDatabase) { + go func(db EVMDBEngine) { defer wg.Done() if err := db.Prune(version); err != nil { errCh <- err diff --git a/sei-db/state_db/ss/store.go b/sei-db/state_db/ss/store.go index ac42da6e4e..83faf555bd 100644 --- a/sei-db/state_db/ss/store.go +++ b/sei-db/state_db/ss/store.go @@ -30,10 +30,22 @@ func RegisterBackend(backendType BackendType, initializer BackendInitializer) { } // NewStateStore creates a CompositeStateStore which handles both Cosmos and EVM data. +// The Cosmos backend (pebbledb or rocksdb) is selected via ssConfig.Backend using the +// registered backend initializers. The same backend is used for EVM sub-databases. // When WriteMode/ReadMode are both cosmos_only (the default), the EVM stores are not // opened and the composite store behaves identically to a plain state store. func NewStateStore(logger logger.Logger, homeDir string, ssConfig config.StateStoreConfig) (types.StateStore, error) { - return composite.NewCompositeStateStore(ssConfig, homeDir, logger) + initializer, ok := backends[BackendType(ssConfig.Backend)] + if !ok { + return nil, fmt.Errorf("unsupported backend: %s", ssConfig.Backend) + } + + cosmosStore, err := initializer(homeDir, ssConfig) + if err != nil { + return nil, fmt.Errorf("failed to create cosmos store: %w", err) + } + + return composite.NewCompositeStateStore(cosmosStore, ssConfig, homeDir, logger) } // RecoverStateStore replays WAL entries that the state store hasn't applied yet. From 54d5e057c2a794a81c7907737e9bc479bfbf184b Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Tue, 24 Feb 2026 14:23:13 -0800 Subject: [PATCH 2/4] Fix ReadOptions CGo leak in EVM RocksDB Get/Has Apply the same fix from #2971 to the EVM RocksDB backend: assign ReadOptions to a local, defer Destroy(), and use heap-allocated timestamp slices so the backing array outlives the stack frame. Co-authored-by: Cursor --- sei-db/state_db/ss/evm/db_rocksdb.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sei-db/state_db/ss/evm/db_rocksdb.go b/sei-db/state_db/ss/evm/db_rocksdb.go index 67655bb419..5709a197bf 100644 --- a/sei-db/state_db/ss/evm/db_rocksdb.go +++ b/sei-db/state_db/ss/evm/db_rocksdb.go @@ -132,7 +132,10 @@ func (db *EVMDatabaseRocksDB) Get(key []byte, version int64) ([]byte, error) { if version < db.earliestVersion.Load() { return nil, nil } - slice, err := db.storage.GetCF(rocksNewTSReadOpts(version), db.cfHandle, key) + readOpts := rocksNewTSReadOpts(version) + defer readOpts.Destroy() + + slice, err := db.storage.GetCF(readOpts, db.cfHandle, key) if err != nil { return nil, fmt.Errorf("rocksdb evm get: %w", err) } @@ -143,7 +146,10 @@ func (db *EVMDatabaseRocksDB) Has(key []byte, version int64) (bool, error) { if version < db.earliestVersion.Load() { return false, nil } - slice, err := db.storage.GetCF(rocksNewTSReadOpts(version), db.cfHandle, key) + readOpts := rocksNewTSReadOpts(version) + defer readOpts.Destroy() + + slice, err := db.storage.GetCF(readOpts, db.cfHandle, key) if err != nil { return false, err } @@ -269,10 +275,10 @@ func (db *EVMDatabaseRocksDB) Close() error { // --- helpers --- func rocksNewTSReadOpts(version int64) *grocksdb.ReadOptions { - var ts [rocksTimestampSize]byte - binary.LittleEndian.PutUint64(ts[:], uint64(version)) + ts := make([]byte, rocksTimestampSize) + binary.LittleEndian.PutUint64(ts, uint64(version)) ro := grocksdb.NewDefaultReadOptions() - ro.SetTimestamp(ts[:]) + ro.SetTimestamp(ts) return ro } From 609bbb63c1c5479d1238e371cb7b730837c48a77 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Tue, 24 Feb 2026 14:39:45 -0800 Subject: [PATCH 3/4] Fix RocksDB EVM resource leaks and missing version update in Delete - Destroy C-allocated Options (defaultOpts, cfOpts) after OpenDbColumnFamilies - Destroy ColumnFamilyHandle before DB.Close() to prevent C memory leak - Update latestVersion in Delete to match Set/ApplyBatch and PebbleDB behavior Co-authored-by: Cursor --- sei-db/state_db/ss/evm/db_rocksdb.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sei-db/state_db/ss/evm/db_rocksdb.go b/sei-db/state_db/ss/evm/db_rocksdb.go index 5709a197bf..fd7dcbff98 100644 --- a/sei-db/state_db/ss/evm/db_rocksdb.go +++ b/sei-db/state_db/ss/evm/db_rocksdb.go @@ -55,8 +55,10 @@ func OpenRocksDB(dir string, storeType EVMStoreType) (*EVMDatabaseRocksDB, error defaultOpts := grocksdb.NewDefaultOptions() defaultOpts.SetCreateIfMissing(true) defaultOpts.SetCreateIfMissingColumnFamilies(true) + defer defaultOpts.Destroy() cfOpts := newRocksDBEVMOpts() + defer cfOpts.Destroy() db, cfHandles, err := grocksdb.OpenDbColumnFamilies( defaultOpts, @@ -180,11 +182,13 @@ func (db *EVMDatabaseRocksDB) Delete(key []byte, version int64) error { batch := grocksdb.NewWriteBatch() batch.DeleteCFWithTS(db.cfHandle, key, ts[:]) + batch.Put([]byte(rocksLatestVersionKey), ts[:]) defer batch.Destroy() if err := db.storage.Write(rocksDefaultWriteOpts, batch); err != nil { return err } + db.latestVersion.Store(version) return nil } @@ -266,7 +270,10 @@ func (db *EVMDatabaseRocksDB) Close() error { if db.storage == nil { return nil } - db.cfHandle = nil + if db.cfHandle != nil { + db.cfHandle.Destroy() + db.cfHandle = nil + } db.storage.Close() db.storage = nil return nil From b85b8ecd111a76bd2a7fba1518daac51be5cf711 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Wed, 25 Feb 2026 07:29:30 -0800 Subject: [PATCH 4/4] Remove unused storeType and backend fields from EVM structs Co-authored-by: Cursor --- sei-db/state_db/ss/evm/db.go | 6 ++---- sei-db/state_db/ss/evm/db_rocksdb.go | 12 +++++------- sei-db/state_db/ss/evm/store.go | 2 -- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/sei-db/state_db/ss/evm/db.go b/sei-db/state_db/ss/evm/db.go index 7c2a8715fd..4c5c80f7b4 100644 --- a/sei-db/state_db/ss/evm/db.go +++ b/sei-db/state_db/ss/evm/db.go @@ -23,8 +23,7 @@ var defaultWriteOpts = &pebble.WriteOptions{Sync: false} // EVMDatabase represents a single PebbleDB instance for a specific EVM data type. // Uses pebble.DefaultComparer (lexicographic byte ordering) instead of MVCCComparer. type EVMDatabase struct { - storeType EVMStoreType - storage *pebble.DB + storage *pebble.DB // Version tracking (atomic for concurrent access) latestVersion atomic.Int64 @@ -58,8 +57,7 @@ func OpenDB(dir string, storeType EVMStoreType) (*EVMDatabase, error) { } evmDB := &EVMDatabase{ - storeType: storeType, - storage: db, + storage: db, } // latestVersion and earliestVersion are zero-initialized by default diff --git a/sei-db/state_db/ss/evm/db_rocksdb.go b/sei-db/state_db/ss/evm/db_rocksdb.go index fd7dcbff98..89608dc531 100644 --- a/sei-db/state_db/ss/evm/db_rocksdb.go +++ b/sei-db/state_db/ss/evm/db_rocksdb.go @@ -39,9 +39,8 @@ var ( // EVMDatabaseRocksDB is a RocksDB-backed versioned KV store for a single EVM data type. // Uses user-defined timestamps for MVCC versioning and a column family for state data. type EVMDatabaseRocksDB struct { - storeType EVMStoreType - storage *grocksdb.DB - cfHandle *grocksdb.ColumnFamilyHandle + storage *grocksdb.DB + cfHandle *grocksdb.ColumnFamilyHandle tsLow int64 latestVersion atomic.Int64 @@ -91,10 +90,9 @@ func OpenRocksDB(dir string, storeType EVMStoreType) (*EVMDatabaseRocksDB, error } evmDB := &EVMDatabaseRocksDB{ - storeType: storeType, - storage: db, - cfHandle: cfHandle, - tsLow: tsLow, + storage: db, + cfHandle: cfHandle, + tsLow: tsLow, } evmDB.latestVersion.Store(latest) evmDB.earliestVersion.Store(earliest) diff --git a/sei-db/state_db/ss/evm/store.go b/sei-db/state_db/ss/evm/store.go index bf4f41b716..7bab63e04e 100644 --- a/sei-db/state_db/ss/evm/store.go +++ b/sei-db/state_db/ss/evm/store.go @@ -23,7 +23,6 @@ type dbWrite struct { type EVMStateStore struct { databases map[EVMStoreType]EVMDBEngine dir string - backend string logger logger.Logger // Per-DB async write channels and worker goroutines @@ -48,7 +47,6 @@ func NewEVMStateStore(dir string, backend string, log logger.Logger) (*EVMStateS databases: make(map[EVMStoreType]EVMDBEngine), asyncChs: make(map[EVMStoreType]chan dbWrite), dir: dir, - backend: backend, logger: log, }