From 5baaa02313f505b1fddd220c6594e7036f1fa8fa Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Wed, 27 May 2026 15:03:56 -0500 Subject: [PATCH 01/16] simplify iterator interfaces --- .../db_engine/dbcache/cached_key_value_db.go | 2 +- sei-db/db_engine/pebbledb/db.go | 4 +- sei-db/db_engine/pebbledb/db_test.go | 26 ++-- sei-db/db_engine/pebbledb/iterator.go | 68 +++++--- sei-db/db_engine/types/types.go | 30 +--- sei-db/ledger_db/receipt/tx_hash_index.go | 2 +- sei-db/state_db/sc/composite/store_test.go | 4 +- sei-db/state_db/sc/flatkv/api.go | 39 +---- sei-db/state_db/sc/flatkv/exporter.go | 8 +- .../sc/flatkv/lthash_correctness_test.go | 2 +- .../state_db/sc/flatkv/perdb_lthash_test.go | 2 +- sei-db/state_db/sc/flatkv/raw_iterator.go | 130 ++++++++++++++++ sei-db/state_db/sc/flatkv/store_iterator.go | 146 ------------------ sei-db/state_db/sc/flatkv/store_read.go | 10 ++ sei-db/state_db/sc/flatkv/store_read_test.go | 10 +- sei-db/state_db/sc/flatkv/store_write_test.go | 2 +- sei-db/state_db/sc/flatkv/testutil_test.go | 2 +- sei-db/state_db/sc/flatkv/verify.go | 2 +- sei-db/state_db/sc/migration/OPERATIONS.md | 4 +- .../migration_test_framework_test.go | 2 +- .../tools/cmd/seidb/operations/dump_flatkv.go | 19 +-- .../cmd/seidb/operations/flatkv_state_size.go | 11 +- 22 files changed, 251 insertions(+), 274 deletions(-) create mode 100644 sei-db/state_db/sc/flatkv/raw_iterator.go delete mode 100644 sei-db/state_db/sc/flatkv/store_iterator.go diff --git a/sei-db/db_engine/dbcache/cached_key_value_db.go b/sei-db/db_engine/dbcache/cached_key_value_db.go index cd5d661512..93de41e822 100644 --- a/sei-db/db_engine/dbcache/cached_key_value_db.go +++ b/sei-db/db_engine/dbcache/cached_key_value_db.go @@ -87,7 +87,7 @@ func (c *cachedKeyValueDB) Delete(key []byte, opts types.WriteOptions) error { return nil } -func (c *cachedKeyValueDB) NewIter(opts *types.IterOptions) (types.KeyValueDBIterator, error) { +func (c *cachedKeyValueDB) NewIter(opts *types.IterOptions) (types.DBIterator, error) { return c.db.NewIter(opts) } diff --git a/sei-db/db_engine/pebbledb/db.go b/sei-db/db_engine/pebbledb/db.go index 623abcceb6..9e1fb642c2 100644 --- a/sei-db/db_engine/pebbledb/db.go +++ b/sei-db/db_engine/pebbledb/db.go @@ -163,7 +163,7 @@ func (p *pebbleDB) Delete(key []byte, opts types.WriteOptions) error { return nil } -func (p *pebbleDB) NewIter(opts *types.IterOptions) (types.KeyValueDBIterator, error) { +func (p *pebbleDB) NewIter(opts *types.IterOptions) (types.DBIterator, error) { var iopts *pebble.IterOptions if opts != nil { iopts = &pebble.IterOptions{ @@ -175,7 +175,7 @@ func (p *pebbleDB) NewIter(opts *types.IterOptions) (types.KeyValueDBIterator, e if err != nil { return nil, err } - return &pebbleIterator{it: it}, nil + return newPebbleIterator(it, opts), nil } func (p *pebbleDB) Flush() error { diff --git a/sei-db/db_engine/pebbledb/db_test.go b/sei-db/db_engine/pebbledb/db_test.go index 17c13899b5..2b876c34e3 100644 --- a/sei-db/db_engine/pebbledb/db_test.go +++ b/sei-db/db_engine/pebbledb/db_test.go @@ -40,6 +40,16 @@ func openDB(t *testing.T, cfg *PebbleDBConfig, cacheCfg *dbcache.CacheConfig) ty return db } +func openUncachedPebbleDB(t *testing.T, cfg *PebbleDBConfig) *pebbleDB { + t.Helper() + db, err := Open(t.Context(), cfg) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, db.Close()) }) + pdb, ok := db.(*pebbleDB) + require.True(t, ok, "Open must return *pebbleDB") + return pdb +} + // --------------------------------------------------------------------------- // Cache-sensitive tests — run in both cached and uncached modes // --------------------------------------------------------------------------- @@ -172,7 +182,7 @@ func TestIteratorBounds(t *testing.T) { t.Cleanup(func() { require.NoError(t, itr.Close()) }) var keys []string - for ok := itr.First(); ok && itr.Valid(); ok = itr.Next() { + for ; itr.Valid(); itr.Next() { keys = append(keys, string(itr.Key())) } require.NoError(t, itr.Error()) @@ -181,14 +191,13 @@ func TestIteratorBounds(t *testing.T) { func TestIteratorPrev(t *testing.T) { cfg := DefaultTestConfig(t) - cacheCfg := DefaultTestCacheConfig() - db := openDB(t, &cfg, &cacheCfg) + pdb := openUncachedPebbleDB(t, &cfg) for _, k := range []string{"a", "b", "c"} { - require.NoError(t, db.Set([]byte(k), []byte("x"), types.WriteOptions{Sync: false})) + require.NoError(t, pdb.Set([]byte(k), []byte("x"), types.WriteOptions{Sync: false})) } - itr, err := db.NewIter(nil) + itr, err := pdb.db.NewIter(nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, itr.Close()) }) @@ -203,18 +212,17 @@ func TestIteratorPrev(t *testing.T) { func TestIteratorSeekLTAndValue(t *testing.T) { cfg := DefaultTestConfig(t) - cacheCfg := DefaultTestCacheConfig() - db := openDB(t, &cfg, &cacheCfg) + pdb := openUncachedPebbleDB(t, &cfg) for _, kv := range []struct{ k, v string }{ {"a", "val-a"}, {"b", "val-b"}, {"c", "val-c"}, } { - require.NoError(t, db.Set([]byte(kv.k), []byte(kv.v), types.WriteOptions{Sync: false})) + require.NoError(t, pdb.Set([]byte(kv.k), []byte(kv.v), types.WriteOptions{Sync: false})) } - itr, err := db.NewIter(nil) + itr, err := pdb.db.NewIter(nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, itr.Close()) }) diff --git a/sei-db/db_engine/pebbledb/iterator.go b/sei-db/db_engine/pebbledb/iterator.go index f7fe59fea8..04d17657a9 100644 --- a/sei-db/db_engine/pebbledb/iterator.go +++ b/sei-db/db_engine/pebbledb/iterator.go @@ -5,23 +5,53 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" ) -// pebbleIterator implements db_engine.Iterator using PebbleDB. -// Key/Value follow Pebble's zero-copy semantics; see db_engine.Iterator contract. +var _ types.DBIterator = (*pebbleIterator)(nil) + +// pebbleIterator implements types.DBIterator over a Pebble iterator. +// Key/Value follow Pebble's zero-copy semantics; copy before modifying. type pebbleIterator struct { - it *pebble.Iterator -} - -var _ types.KeyValueDBIterator = (*pebbleIterator)(nil) - -func (pi *pebbleIterator) First() bool { return pi.it.First() } -func (pi *pebbleIterator) Last() bool { return pi.it.Last() } -func (pi *pebbleIterator) Valid() bool { return pi.it.Valid() } -func (pi *pebbleIterator) SeekGE(k []byte) bool { return pi.it.SeekGE(k) } -func (pi *pebbleIterator) SeekLT(k []byte) bool { return pi.it.SeekLT(k) } -func (pi *pebbleIterator) Next() bool { return pi.it.Next() } -func (pi *pebbleIterator) NextPrefix() bool { return pi.it.NextPrefix() } -func (pi *pebbleIterator) Prev() bool { return pi.it.Prev() } -func (pi *pebbleIterator) Key() []byte { return pi.it.Key() } -func (pi *pebbleIterator) Value() []byte { return pi.it.Value() } -func (pi *pebbleIterator) Error() error { return pi.it.Error() } -func (pi *pebbleIterator) Close() error { return pi.it.Close() } + it *pebble.Iterator + lowerBound []byte + upperBound []byte +} + +func newPebbleIterator(it *pebble.Iterator, opts *types.IterOptions) *pebbleIterator { + pi := &pebbleIterator{it: it} + if opts != nil { + pi.lowerBound = opts.LowerBound + pi.upperBound = opts.UpperBound + } + pi.it.First() + return pi +} + +func (pi *pebbleIterator) Domain() ([]byte, []byte) { + return pi.lowerBound, pi.upperBound +} + +func (pi *pebbleIterator) Valid() bool { + return pi.it.Valid() +} + +func (pi *pebbleIterator) Next() { + if !pi.Valid() { + return + } + pi.it.Next() +} + +func (pi *pebbleIterator) Key() []byte { + return pi.it.Key() +} + +func (pi *pebbleIterator) Value() []byte { + return pi.it.Value() +} + +func (pi *pebbleIterator) Error() error { + return pi.it.Error() +} + +func (pi *pebbleIterator) Close() error { + return pi.it.Close() +} diff --git a/sei-db/db_engine/types/types.go b/sei-db/db_engine/types/types.go index 161017381d..c8e7975c84 100644 --- a/sei-db/db_engine/types/types.go +++ b/sei-db/db_engine/types/types.go @@ -66,8 +66,9 @@ type KeyValueDB interface { // Delete deletes the value for the given key. Delete(key []byte, opts WriteOptions) error - // NewIter returns a new iterator over the key-value store. - NewIter(opts *IterOptions) (KeyValueDBIterator, error) + // NewIter returns a positioned forward iterator over the key-value store. + // Keys and values are read-only; copy before modifying. + NewIter(opts *IterOptions) (DBIterator, error) // NewBatch returns a new batch for atomic writes. NewBatch() Batch @@ -105,31 +106,6 @@ type Checkpointable interface { Checkpoint(destDir string) error } -// KeyValueDBIterator provides ordered iteration over keyspace with seek primitives. -// -// Zero-copy contract: -// - Key/Value may return views into internal buffers and are only valid until the -// next iterator positioning call (Next/Prev/Seek*/First/Last) or Close. -// -// TODO: Merge with DBIterator -type KeyValueDBIterator interface { - First() bool - Last() bool - Valid() bool - - SeekGE(key []byte) bool - SeekLT(key []byte) bool - - Next() bool - NextPrefix() bool - Prev() bool - - Key() []byte - Value() []byte - Error() error - io.Closer -} - // --------------------------------------------------------------------------- // SS DB layer // --------------------------------------------------------------------------- diff --git a/sei-db/ledger_db/receipt/tx_hash_index.go b/sei-db/ledger_db/receipt/tx_hash_index.go index f17e7585c8..9149af6d5a 100644 --- a/sei-db/ledger_db/receipt/tx_hash_index.go +++ b/sei-db/ledger_db/receipt/tx_hash_index.go @@ -168,7 +168,7 @@ func (idx *PebbleTxHashIndex) PruneBefore(_ context.Context, blockNumber uint64) const maxBatchSize = 10000 count := 0 - for ok := iter.First(); ok; ok = iter.Next() { + for ; iter.Valid(); iter.Next() { key := bytes.Clone(iter.Key()) if len(key) == 1+blockNumLen+txHashLen && key[0] == blockPrefix { var txHash common.Hash diff --git a/sei-db/state_db/sc/composite/store_test.go b/sei-db/state_db/sc/composite/store_test.go index c74eaa2477..53622c81da 100644 --- a/sei-db/state_db/sc/composite/store_test.go +++ b/sei-db/state_db/sc/composite/store_test.go @@ -13,6 +13,8 @@ import ( "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/proto" + dbm "github.com/tendermint/tm-db" + "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" @@ -35,7 +37,7 @@ func (f *failingEVMStore) GetBlockHeightModified(string, []byte) (int64, bool, e return -1, false, nil } func (f *failingEVMStore) Has(string, []byte) bool { return false } -func (f *failingEVMStore) RawGlobalIterator() flatkv.Iterator { return nil } +func (f *failingEVMStore) RawGlobalIterator() dbm.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 } diff --git a/sei-db/state_db/sc/flatkv/api.go b/sei-db/state_db/sc/flatkv/api.go index 56e5e5b6c3..dc6c91a38f 100644 --- a/sei-db/state_db/sc/flatkv/api.go +++ b/sei-db/state_db/sc/flatkv/api.go @@ -3,6 +3,8 @@ package flatkv import ( "io" + dbm "github.com/tendermint/tm-db" + "github.com/sei-protocol/sei-chain/sei-db/common/metrics" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" @@ -55,8 +57,12 @@ type Store interface { // Has reports whether the key exists within the given module. Has(moduleName string, key []byte) bool - // RawGlobalIterator returns an iterator for all keys across all underlying DBs - RawGlobalIterator() Iterator + // RawGlobalIterator returns a positioned forward iterator over all committed + // keys across underlying data DBs (account → code → storage → legacy). + // Keys are physical format: "evm/" + type_prefix_byte + stripped_key. + // Pending writes are not visible. Keys and values are read-only; copy + // before modifying. Caller must Close when done. + RawGlobalIterator() dbm.Iterator // RootHash returns the 32-byte checksum of the working LtHash. // Note: This is the Blake3-256 digest of the underlying 2048-byte @@ -99,32 +105,3 @@ type Store interface { io.Closer } - -// Iterator provides ordered iteration over EVM keys. -// Follows PebbleDB semantics: not positioned on creation. -// -// Keys are returned in physical format ("evm/" + type_prefix_byte + stripped_key). -// SeekGE/SeekLT accept both physical keys and memiavl keys (prefix_byte + stripped_key). -// -// EXPERIMENTAL: not used in production. Interface may change when -// Exporter/state-sync is implemented. -type Iterator interface { - Domain() (start, end []byte) - Valid() bool - Error() error - Close() error - - First() bool - Last() bool - SeekGE(key []byte) bool - SeekLT(key []byte) bool - Next() bool - Prev() bool - - // Key returns the current key in physical format (valid until next move). - // Physical format: "evm/" + type_prefix_byte + stripped_key. - Key() []byte - - // Value returns the current value (valid until next move). - Value() []byte -} diff --git a/sei-db/state_db/sc/flatkv/exporter.go b/sei-db/state_db/sc/flatkv/exporter.go index d827b07c84..7cd040a92b 100644 --- a/sei-db/state_db/sc/flatkv/exporter.go +++ b/sei-db/state_db/sc/flatkv/exporter.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" + dbm "github.com/tendermint/tm-db" + errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" ) @@ -21,7 +23,7 @@ var _ types.Exporter = (*KVExporter)(nil) type KVExporter struct { store *CommitStore version int64 - iter Iterator + iter dbm.Iterator } func NewKVExporter(store *CommitStore, version int64) *KVExporter { @@ -34,9 +36,9 @@ func NewKVExporter(store *CommitStore, version int64) *KVExporter { func (e *KVExporter) Next() (interface{}, error) { if e.iter == nil { e.iter = e.store.RawGlobalIterator() - if !e.iter.First() { + if !e.iter.Valid() { if err := e.iter.Error(); err != nil { - return nil, fmt.Errorf("iterator seek error: %w", err) + return nil, fmt.Errorf("iterator error: %w", err) } return nil, errorutils.ErrorExportDone } diff --git a/sei-db/state_db/sc/flatkv/lthash_correctness_test.go b/sei-db/state_db/sc/flatkv/lthash_correctness_test.go index eb02758ece..0aa2a98aaf 100644 --- a/sei-db/state_db/sc/flatkv/lthash_correctness_test.go +++ b/sei-db/state_db/sc/flatkv/lthash_correctness_test.go @@ -31,7 +31,7 @@ func fullScanLtHash(t *testing.T, s *CommitStore) *lthash.LtHash { iter, err := db.NewIter(&types.IterOptions{}) require.NoError(t, err) defer iter.Close() - for iter.First(); iter.Valid(); iter.Next() { + for ; iter.Valid(); iter.Next() { if ktype.IsMetaKey(iter.Key()) { continue } diff --git a/sei-db/state_db/sc/flatkv/perdb_lthash_test.go b/sei-db/state_db/sc/flatkv/perdb_lthash_test.go index 79c557e2b8..6abc768019 100644 --- a/sei-db/state_db/sc/flatkv/perdb_lthash_test.go +++ b/sei-db/state_db/sc/flatkv/perdb_lthash_test.go @@ -27,7 +27,7 @@ func testFullScanDBLtHash(t *testing.T, db types.KeyValueDB) *lthash.LtHash { defer iter.Close() var pairs []lthash.KVPairWithLastValue - for iter.First(); iter.Valid(); iter.Next() { + for ; iter.Valid(); iter.Next() { if ktype.IsMetaKey(iter.Key()) { continue } diff --git a/sei-db/state_db/sc/flatkv/raw_iterator.go b/sei-db/state_db/sc/flatkv/raw_iterator.go new file mode 100644 index 0000000000..fd519210f0 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/raw_iterator.go @@ -0,0 +1,130 @@ +package flatkv + +import ( + dbm "github.com/tendermint/tm-db" + + seidbtypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/ktype" +) + +var _ dbm.Iterator = (*rawIterator)(nil) + +// Iteratates over the raw key-value pairs in a set of pebbleDB databases. Intended for use by the import/export +// workflow. +type rawIterator struct { + dbs []seidbtypes.KeyValueDB + // index into dbs for the current DB + dbIdx int + iter seidbtypes.DBIterator + err error +} + +// newRawIterator returns an iterator positioned on the first non-meta key across +// dbs (account → code → storage → legacy), or invalid if empty. +func newRawIterator(dbs []seidbtypes.KeyValueDB) *rawIterator { + s := &rawIterator{dbs: dbs} + if !s.openCurrent() { + return s + } + skipMeta(s.iter) + if !s.iter.Valid() { + s.advanceDB() + } + return s +} + +// openCurrent opens an iterator on dbs[dbIdx]. Returns false if no more DBs. +func (s *rawIterator) openCurrent() bool { + if s.dbIdx >= len(s.dbs) { + return false + } + it, err := s.dbs[s.dbIdx].NewIter(nil) + if err != nil { + s.err = err + return false + } + s.iter = it + return true +} + +// advanceDB closes the current iterator and moves to the next DB, +// positioning at the first non-meta key. Returns true if positioned. +// If the current iterator has an error, it is captured and iteration stops. +func (s *rawIterator) advanceDB() bool { + for { + if s.iter != nil { + if err := s.iter.Error(); err != nil { + s.err = err + _ = s.iter.Close() + s.iter = nil + return false + } + _ = s.iter.Close() + s.iter = nil + } + s.dbIdx++ + if !s.openCurrent() { + return false + } + skipMeta(s.iter) + if s.iter.Valid() { + return true + } + } +} + +func skipMeta(it seidbtypes.DBIterator) { + for it.Valid() && ktype.IsMetaKey(it.Key()) { + it.Next() + } +} + +func (s *rawIterator) Domain() ([]byte, []byte) { return nil, nil } + +func (s *rawIterator) Valid() bool { + return s.iter != nil && s.iter.Valid() +} + +func (s *rawIterator) Error() error { + if s.err != nil { + return s.err + } + if s.iter != nil { + return s.iter.Error() + } + return nil +} + +func (s *rawIterator) Close() error { + if s.iter != nil { + _ = s.iter.Close() + s.iter = nil + } + return nil +} + +func (s *rawIterator) Next() { + if !s.Valid() { + return + } + s.iter.Next() + skipMeta(s.iter) + if s.iter.Valid() { + return + } + s.advanceDB() +} + +func (s *rawIterator) Key() []byte { + if !s.Valid() { + return nil + } + return s.iter.Key() +} + +func (s *rawIterator) Value() []byte { + if !s.Valid() { + return nil + } + return s.iter.Value() +} diff --git a/sei-db/state_db/sc/flatkv/store_iterator.go b/sei-db/state_db/sc/flatkv/store_iterator.go deleted file mode 100644 index bbf3e8e996..0000000000 --- a/sei-db/state_db/sc/flatkv/store_iterator.go +++ /dev/null @@ -1,146 +0,0 @@ -package flatkv - -import ( - seidbtypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" - "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/ktype" -) - -var _ Iterator = (*sequentialIterator)(nil) - -// sequentialIterator iterates through a slice of DBs one at a time. -// It fully drains the current DB before moving to the next. -type sequentialIterator struct { - dbs []seidbtypes.KeyValueDB - dbIdx int // index into dbs for the current DB - iter seidbtypes.KeyValueDBIterator - err error -} - -// openCurrent opens an iterator on dbs[dbIdx]. Returns false if no more DBs. -func (s *sequentialIterator) openCurrent() bool { - if s.dbIdx >= len(s.dbs) { - return false - } - it, err := s.dbs[s.dbIdx].NewIter(nil) - if err != nil { - s.err = err - return false - } - s.iter = it - return true -} - -// advanceDB closes the current iterator and moves to the next DB, -// positioning at the first non-meta key. Returns true if positioned. -// If the current iterator has an error, it is captured and iteration stops. -func (s *sequentialIterator) advanceDB() bool { - for { - if s.iter != nil { - if err := s.iter.Error(); err != nil { - s.err = err - _ = s.iter.Close() - s.iter = nil - return false - } - _ = s.iter.Close() - s.iter = nil - } - s.dbIdx++ - if !s.openCurrent() { - return false - } - s.iter.First() - skipMeta(s.iter) - if s.iter.Valid() { - return true - } - } -} - -func skipMeta(it seidbtypes.KeyValueDBIterator) { - for it.Valid() && ktype.IsMetaKey(it.Key()) { - it.Next() - } -} - -func (s *sequentialIterator) Domain() ([]byte, []byte) { return nil, nil } - -func (s *sequentialIterator) Valid() bool { - return s.iter != nil && s.iter.Valid() -} - -func (s *sequentialIterator) Error() error { - if s.err != nil { - return s.err - } - if s.iter != nil { - return s.iter.Error() - } - return nil -} - -func (s *sequentialIterator) Close() error { - if s.iter != nil { - _ = s.iter.Close() - s.iter = nil - } - return nil -} - -func (s *sequentialIterator) First() bool { - if s.iter != nil { - _ = s.iter.Close() - s.iter = nil - } - s.dbIdx = 0 - if !s.openCurrent() { - return false - } - s.iter.First() - skipMeta(s.iter) - if s.iter.Valid() { - return true - } - return s.advanceDB() -} - -func (s *sequentialIterator) Next() bool { - if !s.Valid() { - return false - } - s.iter.Next() - skipMeta(s.iter) - if s.iter.Valid() { - return true - } - return s.advanceDB() -} - -func (s *sequentialIterator) Key() []byte { - if !s.Valid() { - return nil - } - return s.iter.Key() -} - -func (s *sequentialIterator) Value() []byte { - if !s.Valid() { - return nil - } - return s.iter.Value() -} - -// Unsupported positioning methods — not needed for forward-only scanning. - -func (s *sequentialIterator) Last() bool { return false } -func (s *sequentialIterator) SeekGE([]byte) bool { return false } -func (s *sequentialIterator) SeekLT([]byte) bool { return false } -func (s *sequentialIterator) Prev() bool { return false } - -// RawGlobalIterator returns an iterator that walks each data DB sequentially -// in fixed order (account → code → storage → legacy). Within each DB the -// keys are returned in PebbleDB's natural order. Per-DB _meta/* keys are -// skipped. Pending writes are not visible. metadataDB is not included. -func (s *CommitStore) RawGlobalIterator() Iterator { - return &sequentialIterator{dbs: s.dataDBs()} -} diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index 630955ad3f..115439aaf9 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -4,6 +4,8 @@ import ( "encoding/binary" "fmt" + dbm "github.com/tendermint/tm-db" + errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/keys" seidbtypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" @@ -215,3 +217,11 @@ func (s *CommitStore) getLegacyValue(moduleName string, key []byte) ([]byte, err } return ld.GetValue(), nil } + +// RawGlobalIterator returns an iterator that walks each data DB sequentially +// in fixed order (account → code → storage → legacy). Within each DB the +// keys are returned in PebbleDB's natural order. Per-DB _meta/* keys are +// skipped. Pending writes are not visible. metadataDB is not included. +func (s *CommitStore) RawGlobalIterator() dbm.Iterator { + return newRawIterator(s.dataDBs()) +} diff --git a/sei-db/state_db/sc/flatkv/store_read_test.go b/sei-db/state_db/sc/flatkv/store_read_test.go index 47bfb4faaf..f54ec08016 100644 --- a/sei-db/state_db/sc/flatkv/store_read_test.go +++ b/sei-db/state_db/sc/flatkv/store_read_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" "github.com/sei-protocol/sei-chain/sei-db/common/keys" "github.com/sei-protocol/sei-chain/sei-db/proto" @@ -655,7 +656,7 @@ func TestIteratorDoesNotSeePendingWrites(t *testing.T) { // Before commit: iterator should not see the pending write iter := s.RawGlobalIterator() - require.False(t, iter.First(), "iterator should not see pending writes") + require.False(t, iter.Valid(), "iterator should not see pending writes") require.NoError(t, iter.Close()) commitAndCheck(t, s) @@ -663,8 +664,7 @@ func TestIteratorDoesNotSeePendingWrites(t *testing.T) { // After commit: iterator should see it iter = s.RawGlobalIterator() defer iter.Close() - require.True(t, iter.First(), "iterator should see committed entry") - require.True(t, iter.Valid()) + require.True(t, iter.Valid(), "iterator should see committed entry") require.Equal(t, storagePhysKey(addr, slot), iter.Key()) } @@ -704,11 +704,11 @@ func TestIteratorDoesNotSeePendingDeletes(t *testing.T) { // Helpers // ============================================================================= -func iterCount(t *testing.T, iter Iterator) int { +func iterCount(t *testing.T, iter dbm.Iterator) int { t.Helper() defer iter.Close() count := 0 - for iter.First(); iter.Valid(); iter.Next() { + for ; iter.Valid(); iter.Next() { count++ } require.NoError(t, iter.Error()) diff --git a/sei-db/state_db/sc/flatkv/store_write_test.go b/sei-db/state_db/sc/flatkv/store_write_test.go index 4e164f5796..78b8719ef4 100644 --- a/sei-db/state_db/sc/flatkv/store_write_test.go +++ b/sei-db/state_db/sc/flatkv/store_write_test.go @@ -1474,7 +1474,7 @@ func countLiveEntries(t *testing.T, db types.KeyValueDB) int { defer iter.Close() count := 0 - for iter.First(); iter.Valid(); iter.Next() { + for ; iter.Valid(); iter.Next() { if ktype.IsMetaKey(iter.Key()) { continue } diff --git a/sei-db/state_db/sc/flatkv/testutil_test.go b/sei-db/state_db/sc/flatkv/testutil_test.go index f7604e2517..d865a60898 100644 --- a/sei-db/state_db/sc/flatkv/testutil_test.go +++ b/sei-db/state_db/sc/flatkv/testutil_test.go @@ -192,7 +192,7 @@ func CountKeys(s *CommitStore) (int64, error) { iter := s.RawGlobalIterator() defer func() { _ = iter.Close() }() var count int64 - for ok := iter.First(); ok; ok = iter.Next() { + for ; iter.Valid(); iter.Next() { count++ } if err := iter.Error(); err != nil { diff --git a/sei-db/state_db/sc/flatkv/verify.go b/sei-db/state_db/sc/flatkv/verify.go index aed40a1c99..164d4b3d82 100644 --- a/sei-db/state_db/sc/flatkv/verify.go +++ b/sei-db/state_db/sc/flatkv/verify.go @@ -48,7 +48,7 @@ func verifyLtHashInternal(cs *CommitStore) error { if err != nil { return fmt.Errorf("VerifyLtHash: open iterator: %w", err) } - for iter.First(); iter.Valid(); iter.Next() { + for ; iter.Valid(); iter.Next() { if ktype.IsMetaKey(iter.Key()) { continue } diff --git a/sei-db/state_db/sc/migration/OPERATIONS.md b/sei-db/state_db/sc/migration/OPERATIONS.md index 63d82cdee9..e9c4a418a3 100644 --- a/sei-db/state_db/sc/migration/OPERATIONS.md +++ b/sei-db/state_db/sc/migration/OPERATIONS.md @@ -238,7 +238,7 @@ Exit code 0 if successfully read; non-zero if either DB cannot be opened. - for each key in flatkv evm: assert logical_k <= boundary - require: no key appears in both - `--mode=ground-truth` -- intended for `C3` extended audit, or post-DR verification: - - compute LtHash over current `(physical_k, encoded_v)` set (using `RawGlobalIterator` from [flatkv/store_iterator.go:144](../flatkv/store_iterator.go) for flatkv; using `MultiTreeExporter` + `ImportTranslator` for memiavl evm if any remaining) + - compute LtHash over current `(physical_k, encoded_v)` set (using `RawGlobalIterator` from [flatkv/store_read.go](../flatkv/store_read.go) for flatkv; using `MultiTreeExporter` + `ImportTranslator` for memiavl evm if any remaining) - compare to `--ground-truth-digest` Exit 0 on pass, non-zero on any failure. On non-zero, print up to `N` divergent samples plus a one-line summary. @@ -254,7 +254,7 @@ Exit 0 on pass, non-zero on any failure. On non-zero, print up to `N` divergent **Library dependencies**: - T1's package-private migration helpers (boundary read) -- `flatkv.RawGlobalIterator` +- `flatkv.RawGlobalIterator` (returns a positioned `dbm.Iterator`; use `for ; iter.Valid(); iter.Next()`) - `memiavl.NewMultiTreeExporter` for the memiavl-evm walk - `flatkv.NewImportTranslator` for the memiavl -> physical-key mapping (`ground-truth` mode only) - `flatkv/lthash` for the digest 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 f684e141b8..705a6f9108 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 @@ -338,7 +338,7 @@ func GetFlatKVKeyCount(t *testing.T, flatKV *flatkv.CommitStore) int64 { iter := flatKV.RawGlobalIterator() defer func() { _ = iter.Close() }() var count int64 - for ok := iter.First(); ok; ok = iter.Next() { + for ; iter.Valid(); iter.Next() { count++ } require.NoError(t, iter.Error()) diff --git a/sei-db/tools/cmd/seidb/operations/dump_flatkv.go b/sei-db/tools/cmd/seidb/operations/dump_flatkv.go index 5702c422e4..1c804d6d71 100644 --- a/sei-db/tools/cmd/seidb/operations/dump_flatkv.go +++ b/sei-db/tools/cmd/seidb/operations/dump_flatkv.go @@ -127,18 +127,15 @@ func dumpFlatKVFromStore(store *flatkv.CommitStore, outputDir string, version in defer func() { _ = iter.Close() }() counts := make(map[string]uint64, len(flatkvBucketOrder)) - if iter.First() { - for iter.Valid() { - key := iter.Key() - val := iter.Value() - bucketName := classifyFlatKVPhysicalKey(key) - if w := writers[bucketName]; w != nil { - if _, werr := fmt.Fprintf(w, "Key: %X, Value: %X\n", key, val); werr != nil { - return fmt.Errorf("write %s: %w", bucketName, werr) - } - counts[bucketName]++ + for ; iter.Valid(); iter.Next() { + key := iter.Key() + val := iter.Value() + bucketName := classifyFlatKVPhysicalKey(key) + if w := writers[bucketName]; w != nil { + if _, werr := fmt.Fprintf(w, "Key: %X, Value: %X\n", key, val); werr != nil { + return fmt.Errorf("write %s: %w", bucketName, werr) } - iter.Next() + counts[bucketName]++ } } if err := iter.Error(); err != nil { diff --git a/sei-db/tools/cmd/seidb/operations/flatkv_state_size.go b/sei-db/tools/cmd/seidb/operations/flatkv_state_size.go index 25187356ae..5a416e6d4e 100644 --- a/sei-db/tools/cmd/seidb/operations/flatkv_state_size.go +++ b/sei-db/tools/cmd/seidb/operations/flatkv_state_size.go @@ -48,14 +48,7 @@ func collectFlatKVStateSize(store *flatkv.CommitStore) (*FlatKVStateSizeResult, iter := store.RawGlobalIterator() defer func() { _ = iter.Close() }() - if !iter.First() { - if err := iter.Error(); err != nil { - return nil, fmt.Errorf("iterate flatkv: %w", err) - } - return result, nil - } - - for iter.Valid() { + for ; iter.Valid(); iter.Next() { key := iter.Key() value := iter.Value() keySize := uint64(len(key)) @@ -92,8 +85,6 @@ func collectFlatKVStateSize(store *flatkv.CommitStore) (*FlatKVStateSizeResult, if result.Total.NumKeys%10000000 == 0 { fmt.Printf(" scanned %d flatkv keys...\n", result.Total.NumKeys) } - - iter.Next() } if err := iter.Error(); err != nil { return nil, fmt.Errorf("iterate flatkv: %w", err) From e96bd9359404dfc8ebcfc09cd435789a1380ddfe Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Wed, 27 May 2026 15:21:51 -0500 Subject: [PATCH 02/16] implemented merging iterator --- sei-db/common/iterators/merging_iterator.go | 202 ++++++++++++++++++ .../common/iterators/merging_iterator_test.go | 174 +++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 sei-db/common/iterators/merging_iterator.go create mode 100644 sei-db/common/iterators/merging_iterator_test.go diff --git a/sei-db/common/iterators/merging_iterator.go b/sei-db/common/iterators/merging_iterator.go new file mode 100644 index 0000000000..73f4c210f0 --- /dev/null +++ b/sei-db/common/iterators/merging_iterator.go @@ -0,0 +1,202 @@ +package iterators + +import ( + "bytes" + "fmt" + + dbm "github.com/tendermint/tm-db" +) + +var _ dbm.Iterator = (*mergingIterator)(nil) + +// mergingIterator merges multiple iterators into a single iterator. Output is +// emitted in lexicographic order so long as each input iterator is already in +// lexicographic order. +type mergingIterator struct { + // the nested iterators to combine + iterators []dbm.Iterator + + // union of child start domains, fixed at construction + start []byte + + // union of child end domains, fixed at construction + end []byte + + // the index of the iterator that should next emit a value + nextIteratorIndex int + + // the error encountered by the iterator, if any + err error +} + +// NewMergingIterator combines iterators into a single iterator. Output is +// emitted in lexicographic order so long as each input iterator is already in +// lexicographic order. +// +// Intended for a small number of iterators (on the order of half a dozen). May not be performant for +// combining large numbers of iterators. +func NewMergingIterator(iterators ...dbm.Iterator) (dbm.Iterator, error) { + m := &mergingIterator{ + iterators: make([]dbm.Iterator, len(iterators)), + nextIteratorIndex: -1, + } + copy(m.iterators, iterators) + + for i, child := range m.iterators { + if child == nil { + return nil, fmt.Errorf("nil iterator at index %d", i) + } + if err := child.Error(); err != nil { + return nil, fmt.Errorf("error in iterator at index %d: %w", i, err) + } + } + + m.start, m.end = mergeDomain(m.iterators) + m.findMin() + return m, nil +} + +// findMin sets nextIteratorIndex to the index of the valid child with the +// smallest current key, or -1 if no child is valid. Child errors are checked +// here and cached on the merge iterator via fail. +func (m *mergingIterator) findMin() { + if m.err != nil { + return + } + m.nextIteratorIndex = -1 + var smallestKey []byte + for i, child := range m.iterators { + if child == nil { + continue + } + if err := child.Error(); err != nil { + m.fail(err) + return + } + if !child.Valid() { + continue + } + childKey := child.Key() + if m.nextIteratorIndex < 0 || bytes.Compare(childKey, smallestKey) < 0 { + m.nextIteratorIndex = i + smallestKey = childKey + } + } +} + +// fail records the first error, closes all children, and clears iterators so no +// further child methods are invoked. +func (m *mergingIterator) fail(err error) { + if m.err != nil { + return + } + m.err = err + m.nextIteratorIndex = -1 + for _, child := range m.iterators { + if child != nil { + _ = child.Close() + } + } + m.iterators = nil +} + +func (m *mergingIterator) Close() error { + if m.iterators == nil { + return nil + } + var firstErr error + for _, child := range m.iterators { + if child == nil { + continue + } + if err := child.Close(); err != nil && firstErr == nil { + firstErr = err + } + } + m.iterators = nil + m.nextIteratorIndex = -1 + return firstErr +} + +func (m *mergingIterator) Domain() ([]byte, []byte) { + return m.start, m.end +} + +// mergeDomain returns the union of child iterator domains: the smallest lower +// bound and the largest upper bound. +func mergeDomain(iters []dbm.Iterator) (start, end []byte) { + first := true + for _, child := range iters { + if child == nil { + continue + } + childStart, childEnd := child.Domain() + if first { + start, end = childStart, childEnd + first = false + continue + } + start = minStart(start, childStart) + end = maxEnd(end, childEnd) + } + return start, end +} + +// minStart returns the smaller of two exclusive-lower iterator bounds. A nil +// bound means unbounded and wins over any non-nil bound. +func minStart(left, right []byte) []byte { + if left == nil || right == nil { + return nil + } + if bytes.Compare(left, right) <= 0 { + return left + } + return right +} + +// maxEnd returns the larger of two exclusive-upper iterator bounds. A nil bound +// means unbounded and wins over any non-nil bound. +func maxEnd(left, right []byte) []byte { + if left == nil || right == nil { + return nil + } + if bytes.Compare(left, right) >= 0 { + return left + } + return right +} + +func (m *mergingIterator) Error() error { + return m.err +} + +func (m *mergingIterator) Key() []byte { + if !m.Valid() { + return nil + } + return m.iterators[m.nextIteratorIndex].Key() +} + +func (m *mergingIterator) Next() { + if !m.Valid() { + return + } + + m.iterators[m.nextIteratorIndex].Next() + if err := m.iterators[m.nextIteratorIndex].Error(); err != nil { + m.fail(err) + return + } + m.findMin() +} + +func (m *mergingIterator) Valid() bool { + return m.nextIteratorIndex >= 0 && m.err == nil +} + +func (m *mergingIterator) Value() []byte { + if !m.Valid() { + return nil + } + return m.iterators[m.nextIteratorIndex].Value() +} diff --git a/sei-db/common/iterators/merging_iterator_test.go b/sei-db/common/iterators/merging_iterator_test.go new file mode 100644 index 0000000000..40c006e2cf --- /dev/null +++ b/sei-db/common/iterators/merging_iterator_test.go @@ -0,0 +1,174 @@ +package iterators_test + +import ( + "errors" + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/common/iterators" + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" +) + +var errChild = errors.New("child failed") + +func memIter(t *testing.T, keys ...[]byte) dbm.Iterator { + t.Helper() + db := dbm.NewMemDB() + for i, k := range keys { + require.NoError(t, db.Set(k, []byte{byte('a' + i)})) + } + it, err := db.Iterator(nil, nil) + require.NoError(t, err) + return it +} + +func collect(t *testing.T, it dbm.Iterator) [][2][]byte { + t.Helper() + var out [][2][]byte + for ; it.Valid(); it.Next() { + out = append(out, [2][]byte{it.Key(), it.Value()}) + } + require.NoError(t, it.Error()) + return out +} + +func TestNewMergingIterator_NilIterator(t *testing.T) { + _, err := iterators.NewMergingIterator(memIter(t, []byte("a")), nil) + require.Error(t, err) +} + +func TestNewMergingIterator_Empty(t *testing.T) { + it, err := iterators.NewMergingIterator() + require.NoError(t, err) + require.False(t, it.Valid()) + require.Nil(t, it.Key()) + require.Nil(t, it.Value()) + require.NoError(t, it.Close()) +} + +func TestMergingIterator_Single(t *testing.T) { + child := memIter(t, []byte("b"), []byte("c")) + it, err := iterators.NewMergingIterator(child) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("b"), []byte("a")}, + {[]byte("c"), []byte("b")}, + }, got) +} + +func TestMergingIterator_LexOrder(t *testing.T) { + a := memIter(t, []byte("a"), []byte("d")) + b := memIter(t, []byte("b"), []byte("c"), []byte("e")) + it, err := iterators.NewMergingIterator(a, b) + require.NoError(t, err) + defer it.Close() + + keys := make([][]byte, 0, 5) + for ; it.Valid(); it.Next() { + keys = append(keys, it.Key()) + } + require.Equal(t, [][]byte{ + []byte("a"), []byte("b"), []byte("c"), []byte("d"), []byte("e"), + }, keys) +} + +func TestMergingIterator_DuplicateKeys(t *testing.T) { + a := memIter(t, []byte("k"), []byte("z")) + b := memIter(t, []byte("k"), []byte("m")) + it, err := iterators.NewMergingIterator(a, b) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("k"), []byte("a")}, + {[]byte("k"), []byte("a")}, + {[]byte("m"), []byte("b")}, + {[]byte("z"), []byte("b")}, + }, got) +} + +func TestMergingIterator_Domain(t *testing.T) { + db := dbm.NewMemDB() + it1, err := db.Iterator([]byte("b"), []byte("f")) + require.NoError(t, err) + it2, err := db.Iterator([]byte("a"), nil) + require.NoError(t, err) + + merged, err := iterators.NewMergingIterator(it1, it2) + require.NoError(t, err) + defer merged.Close() + + start, end := merged.Domain() + require.Equal(t, []byte("a"), start) + require.Nil(t, end) +} + +type closeTrackingIterator struct { + dbm.Iterator + closed bool +} + +func (c *closeTrackingIterator) Close() error { + c.closed = true + return c.Iterator.Close() +} + +type errOnSecondNextIterator struct { + dbm.Iterator + nextCount int + closed bool +} + +func (child *errOnSecondNextIterator) Next() { + child.nextCount++ + child.Iterator.Next() +} + +func (child *errOnSecondNextIterator) Error() error { + if child.nextCount >= 2 { + return errChild + } + return child.Iterator.Error() +} + +func (child *errOnSecondNextIterator) Close() error { + child.closed = true + return child.Iterator.Close() +} + +func TestMergingIterator_CachesChildError(t *testing.T) { + ok := memIter(t, []byte("a"), []byte("c")) + bad := &errOnSecondNextIterator{Iterator: memIter(t, []byte("b"), []byte("d"))} + merged, err := iterators.NewMergingIterator(ok, bad) + require.NoError(t, err) + + require.True(t, merged.Valid()) + merged.Next() // emit "a" + require.True(t, merged.Valid()) + merged.Next() // emit "b", advances bad once + require.True(t, merged.Valid()) + merged.Next() // emit "c", advances ok + require.True(t, merged.Valid()) + merged.Next() // emit "d", advances bad again -> error + + require.False(t, merged.Valid()) + require.ErrorIs(t, merged.Error(), errChild) + require.Nil(t, merged.Key()) + require.Nil(t, merged.Value()) + merged.Next() // no-op after failure + + require.True(t, bad.closed) + require.NoError(t, merged.Close()) +} + +func TestMergingIterator_ClosesChildren(t *testing.T) { + child := &closeTrackingIterator{Iterator: memIter(t, []byte("x"))} + it, err := iterators.NewMergingIterator(child) + require.NoError(t, err) + require.NoError(t, it.Close()) + require.True(t, child.closed) +} From 99ef64ccb6963fafe9576863614bdca2571a1d64 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Wed, 27 May 2026 15:57:14 -0500 Subject: [PATCH 03/16] simplify types, add iterator utilities --- evmrpc/watermark_manager_test.go | 5 +- sei-db/common/iterators/invalid_iterator.go | 31 ++++ sei-db/common/iterators/mapping_iterator.go | 161 ++++++++++++++++++ .../common/iterators/mapping_iterator_test.go | 72 ++++++++ .../db_engine/dbcache/cached_key_value_db.go | 4 +- sei-db/db_engine/pebbledb/db.go | 4 +- sei-db/db_engine/pebbledb/iterator.go | 6 +- sei-db/db_engine/pebbledb/mvcc/db.go | 10 +- .../db_engine/pebbledb/mvcc/db_ascending.go | 7 +- sei-db/db_engine/pebbledb/mvcc/iterator.go | 4 +- .../pebbledb/mvcc/iterator_ascending.go | 4 +- sei-db/db_engine/rocksdb/mvcc/db.go | 6 +- sei-db/db_engine/rocksdb/mvcc/iterator.go | 3 +- sei-db/db_engine/types/types.go | 18 +- .../state_db/bench/wrappers/wrappers_test.go | 5 +- sei-db/state_db/sc/flatkv/api.go | 2 +- sei-db/state_db/sc/flatkv/exporter.go | 2 +- sei-db/state_db/sc/flatkv/raw_iterator.go | 130 -------------- sei-db/state_db/sc/flatkv/store_read.go | 37 +++- sei-db/state_db/sc/flatkv/store_read_test.go | 72 ++++++++ sei-db/state_db/sc/migration/OPERATIONS.md | 2 +- sei-db/state_db/ss/composite/store.go | 6 +- sei-db/state_db/ss/composite/store_test.go | 6 +- sei-db/state_db/ss/cosmos/store.go | 6 +- sei-db/state_db/ss/evm/store.go | 6 +- sei-db/state_db/ss/pruning/manager_test.go | 6 +- .../tools/cmd/seidb/operations/dump_flatkv.go | 10 +- 27 files changed, 436 insertions(+), 189 deletions(-) create mode 100644 sei-db/common/iterators/invalid_iterator.go create mode 100644 sei-db/common/iterators/mapping_iterator.go create mode 100644 sei-db/common/iterators/mapping_iterator_test.go delete mode 100644 sei-db/state_db/sc/flatkv/raw_iterator.go diff --git a/evmrpc/watermark_manager_test.go b/evmrpc/watermark_manager_test.go index 09dcbf57ec..810e915081 100644 --- a/evmrpc/watermark_manager_test.go +++ b/evmrpc/watermark_manager_test.go @@ -16,6 +16,7 @@ import ( storetypes "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + dbm "github.com/tendermint/tm-db" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" "github.com/sei-protocol/sei-chain/sei-db/proto" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -222,10 +223,10 @@ type fakeStateStore struct { func (f *fakeStateStore) Get(string, int64, []byte) ([]byte, error) { return nil, nil } func (f *fakeStateStore) Has(string, int64, []byte) (bool, error) { return false, nil } -func (f *fakeStateStore) Iterator(string, int64, []byte, []byte) (types.DBIterator, error) { +func (f *fakeStateStore) Iterator(string, int64, []byte, []byte) (dbm.Iterator, error) { return nil, nil } -func (f *fakeStateStore) ReverseIterator(string, int64, []byte, []byte) (types.DBIterator, error) { +func (f *fakeStateStore) ReverseIterator(string, int64, []byte, []byte) (dbm.Iterator, error) { return nil, nil } func (f *fakeStateStore) RawIterate(string, func([]byte, []byte, int64) bool) (bool, error) { diff --git a/sei-db/common/iterators/invalid_iterator.go b/sei-db/common/iterators/invalid_iterator.go new file mode 100644 index 0000000000..72abe0e04d --- /dev/null +++ b/sei-db/common/iterators/invalid_iterator.go @@ -0,0 +1,31 @@ +package iterators + +import dbm "github.com/tendermint/tm-db" + +var _ dbm.Iterator = (*invalidIterator)(nil) + +// invalidIterator is always invalid and reports a fixed construction error. +type invalidIterator struct { + err error +} + +// NewInvalidIterator returns an iterator that is never valid and reports err +// from Error(). Used when iterator construction fails but the API cannot +// return an error (e.g. RawGlobalIterator). +func NewInvalidIterator(err error) dbm.Iterator { + return &invalidIterator{err: err} +} + +func (m *invalidIterator) Close() error { return nil } + +func (m *invalidIterator) Domain() ([]byte, []byte) { return nil, nil } + +func (m *invalidIterator) Error() error { return m.err } + +func (m *invalidIterator) Key() []byte { return nil } + +func (m *invalidIterator) Next() {} + +func (m *invalidIterator) Valid() bool { return false } + +func (m *invalidIterator) Value() []byte { return nil } diff --git a/sei-db/common/iterators/mapping_iterator.go b/sei-db/common/iterators/mapping_iterator.go new file mode 100644 index 0000000000..8aafac77e4 --- /dev/null +++ b/sei-db/common/iterators/mapping_iterator.go @@ -0,0 +1,161 @@ +package iterators + +import ( + "fmt" + + dbm "github.com/tendermint/tm-db" +) + +var _ dbm.Iterator = (*mappingIterator)(nil) + +// A function used to remap key/value pairs returned by an iterator. +type IteratorRemapper func( + // The input key. + inputKey []byte, + // The input value. + inputValue []byte, +) ( + // The resulting key to emit. + outputKey []byte, + // The resulting value to emit. + outputValue []byte, + // Whether to skip the current key/value pair. If true, the iterator will + // not emit this key/value pair. + skip bool, + // An error to return if the remapping fails (e.g. parsing failure) + err error, +) + +// mappingIterator applies a remapper to each key/value pair from a parent +// iterator, optionally skipping entries. +type mappingIterator struct { + // The parent iterator to remap. + parent dbm.Iterator + // The function used to remap key/value pairs. + remapper IteratorRemapper + // The next key/value pair to emit. + key []byte + // The next value to emit. + value []byte + // The error encountered by the iterator, if any. + err error +} + +// NewMappingIterator returns an iterator that emits remapped key/value pairs +// from parent, skipping pairs for which remapper returns skip=true. +func NewMappingIterator(parent dbm.Iterator, remapper IteratorRemapper) *mappingIterator { + m := &mappingIterator{ + parent: parent, + remapper: remapper, + } + if parent == nil { + m.err = fmt.Errorf("nil parent iterator") + return m + } + if remapper == nil { + m.err = fmt.Errorf("nil remapper") + return m + } + if err := parent.Error(); err != nil { + m.fail(err) + return m + } + m.advance() + return m +} + +// advance moves to the next non-skipped parent entry, or clears the position if +// none remain. +func (m *mappingIterator) advance() { + m.key = nil + m.value = nil + if m.parent == nil { + return + } + for m.parent.Valid() { + if err := m.parent.Error(); err != nil { + m.fail(err) + return + } + inputKey := m.parent.Key() + inputValue := m.parent.Value() + outputKey, outputValue, skip, err := m.remapper(inputKey, inputValue) + if err != nil { + m.fail(err) + return + } + if !skip { + m.key = outputKey + m.value = outputValue + return + } + m.parent.Next() + } +} + +// fail records the first error, closes the parent, and clears it so no further +// parent methods are invoked. +func (m *mappingIterator) fail(err error) { + if m.err != nil { + return + } + m.err = err + m.key = nil + m.value = nil + if m.parent != nil { + _ = m.parent.Close() + m.parent = nil + } +} + +func (m *mappingIterator) Close() error { + if m.parent == nil { + return nil + } + err := m.parent.Close() + m.parent = nil + m.key = nil + m.value = nil + return err +} + +func (m *mappingIterator) Domain() ([]byte, []byte) { + if m.parent == nil { + return nil, nil + } + return m.parent.Domain() +} + +func (m *mappingIterator) Error() error { + return m.err +} + +func (m *mappingIterator) Key() []byte { + if !m.Valid() { + return nil + } + return m.key +} + +func (m *mappingIterator) Next() { + if !m.Valid() { + return + } + m.parent.Next() + if err := m.parent.Error(); err != nil { + m.fail(err) + return + } + m.advance() +} + +func (m *mappingIterator) Valid() bool { + return m.err == nil && m.key != nil +} + +func (m *mappingIterator) Value() []byte { + if !m.Valid() { + return nil + } + return m.value +} diff --git a/sei-db/common/iterators/mapping_iterator_test.go b/sei-db/common/iterators/mapping_iterator_test.go new file mode 100644 index 0000000000..a61f0512e0 --- /dev/null +++ b/sei-db/common/iterators/mapping_iterator_test.go @@ -0,0 +1,72 @@ +package iterators_test + +import ( + "errors" + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/common/iterators" + "github.com/stretchr/testify/require" +) + +var errRemap = errors.New("remap failed") + +func TestMappingIterator_SkipsKeys(t *testing.T) { + parent := memIter(t, []byte("a"), []byte("b"), []byte("c")) + mapIter := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + if key[0] == 'b' { + return nil, nil, true, nil + } + return key, value, false, nil + }) + + got := collect(t, mapIter) + require.Equal(t, [][2][]byte{ + {[]byte("a"), []byte("a")}, + {[]byte("c"), []byte("c")}, + }, got) +} + +func TestMappingIterator_RemapsKeyValue(t *testing.T) { + parent := memIter(t, []byte("k")) + mapIter := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + return append([]byte("x"), key...), append([]byte("y"), value...), false, nil + }) + + require.True(t, mapIter.Valid()) + require.Equal(t, []byte("xk"), mapIter.Key()) + require.Equal(t, []byte("ya"), mapIter.Value()) + require.NoError(t, mapIter.Close()) +} + +func TestMappingIterator_RemapperError(t *testing.T) { + parent := memIter(t, []byte("a"), []byte("b")) + mapIter := iterators.NewMappingIterator(parent, func(key, _ []byte) ([]byte, []byte, bool, error) { + if key[0] == 'b' { + return nil, nil, false, errRemap + } + return key, key, false, nil + }) + + require.True(t, mapIter.Valid()) + require.Equal(t, []byte("a"), mapIter.Key()) + mapIter.Next() + require.False(t, mapIter.Valid()) + require.ErrorIs(t, mapIter.Error(), errRemap) +} + +func TestMappingIterator_EmptyParent(t *testing.T) { + parent := memIter(t) + mapIter := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + return key, value, false, nil + }) + require.False(t, mapIter.Valid()) + require.NoError(t, mapIter.Error()) +} + +func TestInvalidIterator(t *testing.T) { + errConstruction := errors.New("open failed") + it := iterators.NewInvalidIterator(errConstruction) + require.False(t, it.Valid()) + require.ErrorIs(t, it.Error(), errConstruction) + require.NoError(t, it.Close()) +} diff --git a/sei-db/db_engine/dbcache/cached_key_value_db.go b/sei-db/db_engine/dbcache/cached_key_value_db.go index 93de41e822..2234f4267f 100644 --- a/sei-db/db_engine/dbcache/cached_key_value_db.go +++ b/sei-db/db_engine/dbcache/cached_key_value_db.go @@ -3,6 +3,8 @@ package dbcache import ( "fmt" + dbm "github.com/tendermint/tm-db" + errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" ) @@ -87,7 +89,7 @@ func (c *cachedKeyValueDB) Delete(key []byte, opts types.WriteOptions) error { return nil } -func (c *cachedKeyValueDB) NewIter(opts *types.IterOptions) (types.DBIterator, error) { +func (c *cachedKeyValueDB) NewIter(opts *types.IterOptions) (dbm.Iterator, error) { return c.db.NewIter(opts) } diff --git a/sei-db/db_engine/pebbledb/db.go b/sei-db/db_engine/pebbledb/db.go index 9e1fb642c2..c22dece944 100644 --- a/sei-db/db_engine/pebbledb/db.go +++ b/sei-db/db_engine/pebbledb/db.go @@ -11,6 +11,8 @@ import ( "github.com/cockroachdb/pebble/v2/bloom" "github.com/cockroachdb/pebble/v2/sstable" + dbm "github.com/tendermint/tm-db" + errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/threading" "github.com/sei-protocol/sei-chain/sei-db/common/unit" @@ -163,7 +165,7 @@ func (p *pebbleDB) Delete(key []byte, opts types.WriteOptions) error { return nil } -func (p *pebbleDB) NewIter(opts *types.IterOptions) (types.DBIterator, error) { +func (p *pebbleDB) NewIter(opts *types.IterOptions) (dbm.Iterator, error) { var iopts *pebble.IterOptions if opts != nil { iopts = &pebble.IterOptions{ diff --git a/sei-db/db_engine/pebbledb/iterator.go b/sei-db/db_engine/pebbledb/iterator.go index 04d17657a9..5f2687896f 100644 --- a/sei-db/db_engine/pebbledb/iterator.go +++ b/sei-db/db_engine/pebbledb/iterator.go @@ -2,12 +2,14 @@ package pebbledb import ( "github.com/cockroachdb/pebble/v2" + dbm "github.com/tendermint/tm-db" + "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" ) -var _ types.DBIterator = (*pebbleIterator)(nil) +var _ dbm.Iterator = (*pebbleIterator)(nil) -// pebbleIterator implements types.DBIterator over a Pebble iterator. +// pebbleIterator implements dbm.Iterator over a Pebble iterator. // Key/Value follow Pebble's zero-copy semantics; copy before modifying. type pebbleIterator struct { it *pebble.Iterator diff --git a/sei-db/db_engine/pebbledb/mvcc/db.go b/sei-db/db_engine/pebbledb/mvcc/db.go index b34d09037c..95d0ebb1b2 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db.go +++ b/sei-db/db_engine/pebbledb/mvcc/db.go @@ -19,6 +19,8 @@ import ( "go.opentelemetry.io/otel/metric" "golang.org/x/exp/slices" + dbm "github.com/tendermint/tm-db" + errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/config" @@ -489,7 +491,7 @@ func (db *Database) Prune(version int64) error { // Iterator dispatches between descending- and ascending-mode implementations // depending on the on-disk encoding detected at open time. -func (db *Database) Iterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (db *Database) Iterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if db.descending { return db.iteratorDescending(storeKey, version, start, end) } @@ -498,7 +500,7 @@ func (db *Database) Iterator(storeKey string, version int64, start, end []byte) // ReverseIterator dispatches between descending- and ascending-mode // implementations depending on the on-disk encoding detected at open time. -func (db *Database) ReverseIterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (db *Database) ReverseIterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if db.descending { return db.reverseIteratorDescending(storeKey, version, start, end) } @@ -685,7 +687,7 @@ func (db *Database) pruneDescending(version int64) (_err error) { return db.SetEarliestVersion(earliestVersion, false) } -func (db *Database) iteratorDescending(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (db *Database) iteratorDescending(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { return nil, errorutils.ErrKeyEmpty } @@ -711,7 +713,7 @@ func (db *Database) iteratorDescending(storeKey string, version int64, start, en return newPebbleDBIterator(itr, storePrefix(storeKey), start, end, version, db.GetEarliestVersion(), false, db.config.UseDefaultComparer, storeKey), nil } -func (db *Database) reverseIteratorDescending(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (db *Database) reverseIteratorDescending(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { return nil, errorutils.ErrKeyEmpty } diff --git a/sei-db/db_engine/pebbledb/mvcc/db_ascending.go b/sei-db/db_engine/pebbledb/mvcc/db_ascending.go index ee116d3192..b632c4dd00 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db_ascending.go +++ b/sei-db/db_engine/pebbledb/mvcc/db_ascending.go @@ -13,8 +13,9 @@ import ( "go.opentelemetry.io/otel/metric" "golang.org/x/exp/slices" + dbm "github.com/tendermint/tm-db" + errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" - "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" ) // This file contains the ascending-version MVCC implementation used to read @@ -209,7 +210,7 @@ func (db *Database) pruneAscending(version int64) (_err error) { return db.SetEarliestVersion(earliestVersion, false) } -func (db *Database) iteratorAscending(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (db *Database) iteratorAscending(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { return nil, errorutils.ErrKeyEmpty } @@ -233,7 +234,7 @@ func (db *Database) iteratorAscending(storeKey string, version int64, start, end return newAscendingIterator(itr, storePrefix(storeKey), start, end, version, db.GetEarliestVersion(), false, storeKey), nil } -func (db *Database) reverseIteratorAscending(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (db *Database) reverseIteratorAscending(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { return nil, errorutils.ErrKeyEmpty } diff --git a/sei-db/db_engine/pebbledb/mvcc/iterator.go b/sei-db/db_engine/pebbledb/mvcc/iterator.go index 9243f554e2..d79eb60174 100644 --- a/sei-db/db_engine/pebbledb/mvcc/iterator.go +++ b/sei-db/db_engine/pebbledb/mvcc/iterator.go @@ -12,10 +12,10 @@ import ( "go.opentelemetry.io/otel/metric" "golang.org/x/exp/slices" - "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + dbm "github.com/tendermint/tm-db" ) -var _ types.DBIterator = (*iterator)(nil) +var _ dbm.Iterator = (*iterator)(nil) // iterator implements the Iterator interface. It wraps a PebbleDB iterator // with added MVCC key handling logic. The iterator will iterate over the key space diff --git a/sei-db/db_engine/pebbledb/mvcc/iterator_ascending.go b/sei-db/db_engine/pebbledb/mvcc/iterator_ascending.go index 49bd1dcfa6..550c537231 100644 --- a/sei-db/db_engine/pebbledb/mvcc/iterator_ascending.go +++ b/sei-db/db_engine/pebbledb/mvcc/iterator_ascending.go @@ -11,7 +11,7 @@ import ( "go.opentelemetry.io/otel/metric" "golang.org/x/exp/slices" - "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + dbm "github.com/tendermint/tm-db" ) // This file contains the ascending-version MVCC iterator used for legacy DBs @@ -22,7 +22,7 @@ import ( // // Archive nodes that cannot migrate will continue to use this path. -var _ types.DBIterator = (*ascendingIterator)(nil) +var _ dbm.Iterator = (*ascendingIterator)(nil) // ascendingIterator is the legacy iterator. Versions of a logical key sort // oldest-first on disk, so finding the visible version for a target height diff --git a/sei-db/db_engine/rocksdb/mvcc/db.go b/sei-db/db_engine/rocksdb/mvcc/db.go index 44d9683398..1b8c45b590 100644 --- a/sei-db/db_engine/rocksdb/mvcc/db.go +++ b/sei-db/db_engine/rocksdb/mvcc/db.go @@ -15,6 +15,8 @@ import ( "github.com/linxGnu/grocksdb" "golang.org/x/exp/slices" + dbm "github.com/tendermint/tm-db" + "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/config" @@ -313,7 +315,7 @@ func (db *Database) Prune(version int64) error { return db.SetEarliestVersion(tsLow, false) } -func (db *Database) Iterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (db *Database) Iterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { return nil, errors.ErrKeyEmpty } @@ -330,7 +332,7 @@ func (db *Database) Iterator(storeKey string, version int64, start, end []byte) return NewRocksDBIterator(itr, readOpts, prefix, start, end, version, db.earliestVersion, false), nil } -func (db *Database) ReverseIterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (db *Database) ReverseIterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { return nil, errors.ErrKeyEmpty } diff --git a/sei-db/db_engine/rocksdb/mvcc/iterator.go b/sei-db/db_engine/rocksdb/mvcc/iterator.go index 27d6006556..b8266d24d9 100644 --- a/sei-db/db_engine/rocksdb/mvcc/iterator.go +++ b/sei-db/db_engine/rocksdb/mvcc/iterator.go @@ -8,11 +8,12 @@ import ( "sync" "github.com/linxGnu/grocksdb" + dbm "github.com/tendermint/tm-db" "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" ) -var _ types.DBIterator = (*iterator)(nil) +var _ dbm.Iterator = (*iterator)(nil) type iterator struct { source *grocksdb.Iterator diff --git a/sei-db/db_engine/types/types.go b/sei-db/db_engine/types/types.go index c8e7975c84..23cacb31f8 100644 --- a/sei-db/db_engine/types/types.go +++ b/sei-db/db_engine/types/types.go @@ -4,6 +4,7 @@ import ( "io" "github.com/sei-protocol/sei-chain/sei-db/proto" + dbm "github.com/tendermint/tm-db" ) // WriteOptions controls durability for write operations. @@ -68,7 +69,7 @@ type KeyValueDB interface { // NewIter returns a positioned forward iterator over the key-value store. // Keys and values are read-only; copy before modifying. - NewIter(opts *IterOptions) (DBIterator, error) + NewIter(opts *IterOptions) (dbm.Iterator, error) // NewBatch returns a new batch for atomic writes. NewBatch() Batch @@ -116,8 +117,8 @@ type Checkpointable interface { type StateStore interface { Get(storeKey string, version int64, key []byte) ([]byte, error) Has(storeKey string, version int64, key []byte) (bool, error) - Iterator(storeKey string, version int64, start, end []byte) (DBIterator, error) - ReverseIterator(storeKey string, version int64, start, end []byte) (DBIterator, error) + Iterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) + ReverseIterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) RawIterate(storeKey string, fn func([]byte, []byte, int64) bool) (bool, error) GetLatestVersion() int64 SetLatestVersion(version int64) error @@ -130,17 +131,6 @@ type StateStore interface { io.Closer } -// DBIterator iterates over versioned key-value pairs. -type DBIterator interface { - Domain() (start []byte, end []byte) - Valid() bool - Next() - Key() (key []byte) - Value() (value []byte) - Error() error - Close() error -} - type SnapshotNode struct { StoreKey string Key []byte diff --git a/sei-db/state_db/bench/wrappers/wrappers_test.go b/sei-db/state_db/bench/wrappers/wrappers_test.go index a3d873918f..c1b7d88303 100644 --- a/sei-db/state_db/bench/wrappers/wrappers_test.go +++ b/sei-db/state_db/bench/wrappers/wrappers_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" "github.com/sei-protocol/sei-chain/sei-db/common/metrics" dbTypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" @@ -69,11 +70,11 @@ func (m *mockStateStore) Has(_ string, _ int64, _ []byte) (bool, error) { return false, nil } -func (m *mockStateStore) Iterator(_ string, _ int64, _, _ []byte) (dbTypes.DBIterator, error) { +func (m *mockStateStore) Iterator(_ string, _ int64, _, _ []byte) (dbm.Iterator, error) { return nil, nil } -func (m *mockStateStore) ReverseIterator(_ string, _ int64, _, _ []byte) (dbTypes.DBIterator, error) { +func (m *mockStateStore) ReverseIterator(_ string, _ int64, _, _ []byte) (dbm.Iterator, error) { return nil, nil } diff --git a/sei-db/state_db/sc/flatkv/api.go b/sei-db/state_db/sc/flatkv/api.go index dc6c91a38f..31b4117ca8 100644 --- a/sei-db/state_db/sc/flatkv/api.go +++ b/sei-db/state_db/sc/flatkv/api.go @@ -58,7 +58,7 @@ type Store interface { Has(moduleName string, key []byte) bool // RawGlobalIterator returns a positioned forward iterator over all committed - // keys across underlying data DBs (account → code → storage → legacy). + // keys across underlying data DBs, merged in global lexicographic order. // Keys are physical format: "evm/" + type_prefix_byte + stripped_key. // Pending writes are not visible. Keys and values are read-only; copy // before modifying. Caller must Close when done. diff --git a/sei-db/state_db/sc/flatkv/exporter.go b/sei-db/state_db/sc/flatkv/exporter.go index 7cd040a92b..86317908b1 100644 --- a/sei-db/state_db/sc/flatkv/exporter.go +++ b/sei-db/state_db/sc/flatkv/exporter.go @@ -14,7 +14,7 @@ var _ types.Exporter = (*KVExporter)(nil) // KVExporter exports all committed data from a read-only FlatKV store as raw // physical key/value pairs. It uses RawGlobalIterator to walk every data DB -// (account → code → storage → legacy) and emits each row as a single +// in global lexicographic order and emits each row as a single // SnapshotNode without any parsing or conversion. // // All emitted SnapshotNodes carry the export version and Height=0 (leaf). diff --git a/sei-db/state_db/sc/flatkv/raw_iterator.go b/sei-db/state_db/sc/flatkv/raw_iterator.go deleted file mode 100644 index fd519210f0..0000000000 --- a/sei-db/state_db/sc/flatkv/raw_iterator.go +++ /dev/null @@ -1,130 +0,0 @@ -package flatkv - -import ( - dbm "github.com/tendermint/tm-db" - - seidbtypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" - "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/ktype" -) - -var _ dbm.Iterator = (*rawIterator)(nil) - -// Iteratates over the raw key-value pairs in a set of pebbleDB databases. Intended for use by the import/export -// workflow. -type rawIterator struct { - dbs []seidbtypes.KeyValueDB - // index into dbs for the current DB - dbIdx int - iter seidbtypes.DBIterator - err error -} - -// newRawIterator returns an iterator positioned on the first non-meta key across -// dbs (account → code → storage → legacy), or invalid if empty. -func newRawIterator(dbs []seidbtypes.KeyValueDB) *rawIterator { - s := &rawIterator{dbs: dbs} - if !s.openCurrent() { - return s - } - skipMeta(s.iter) - if !s.iter.Valid() { - s.advanceDB() - } - return s -} - -// openCurrent opens an iterator on dbs[dbIdx]. Returns false if no more DBs. -func (s *rawIterator) openCurrent() bool { - if s.dbIdx >= len(s.dbs) { - return false - } - it, err := s.dbs[s.dbIdx].NewIter(nil) - if err != nil { - s.err = err - return false - } - s.iter = it - return true -} - -// advanceDB closes the current iterator and moves to the next DB, -// positioning at the first non-meta key. Returns true if positioned. -// If the current iterator has an error, it is captured and iteration stops. -func (s *rawIterator) advanceDB() bool { - for { - if s.iter != nil { - if err := s.iter.Error(); err != nil { - s.err = err - _ = s.iter.Close() - s.iter = nil - return false - } - _ = s.iter.Close() - s.iter = nil - } - s.dbIdx++ - if !s.openCurrent() { - return false - } - skipMeta(s.iter) - if s.iter.Valid() { - return true - } - } -} - -func skipMeta(it seidbtypes.DBIterator) { - for it.Valid() && ktype.IsMetaKey(it.Key()) { - it.Next() - } -} - -func (s *rawIterator) Domain() ([]byte, []byte) { return nil, nil } - -func (s *rawIterator) Valid() bool { - return s.iter != nil && s.iter.Valid() -} - -func (s *rawIterator) Error() error { - if s.err != nil { - return s.err - } - if s.iter != nil { - return s.iter.Error() - } - return nil -} - -func (s *rawIterator) Close() error { - if s.iter != nil { - _ = s.iter.Close() - s.iter = nil - } - return nil -} - -func (s *rawIterator) Next() { - if !s.Valid() { - return - } - s.iter.Next() - skipMeta(s.iter) - if s.iter.Valid() { - return - } - s.advanceDB() -} - -func (s *rawIterator) Key() []byte { - if !s.Valid() { - return nil - } - return s.iter.Key() -} - -func (s *rawIterator) Value() []byte { - if !s.Valid() { - return nil - } - return s.iter.Value() -} diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index 115439aaf9..2844fc8ff8 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -7,6 +7,7 @@ import ( dbm "github.com/tendermint/tm-db" errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" + "github.com/sei-protocol/sei-chain/sei-db/common/iterators" "github.com/sei-protocol/sei-chain/sei-db/common/keys" seidbtypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/ktype" @@ -218,10 +219,38 @@ func (s *CommitStore) getLegacyValue(moduleName string, key []byte) ([]byte, err return ld.GetValue(), nil } -// RawGlobalIterator returns an iterator that walks each data DB sequentially -// in fixed order (account → code → storage → legacy). Within each DB the -// keys are returned in PebbleDB's natural order. Per-DB _meta/* keys are +// RawGlobalIterator returns an iterator over all committed keys across the +// data DBs (account, code, storage, legacy), merged in global lexicographic +// order. Within each DB, keys are in Pebble order. Per-DB _meta/* keys are // skipped. Pending writes are not visible. metadataDB is not included. func (s *CommitStore) RawGlobalIterator() dbm.Iterator { - return newRawIterator(s.dataDBs()) + dbs := s.dataDBs() + children := make([]dbm.Iterator, 0, len(dbs)) + for _, db := range dbs { + pebbleIter, err := db.NewIter(nil) + if err != nil { + closeIterators(children) + return iterators.NewInvalidIterator(fmt.Errorf("open data DB iterator: %w", err)) + } + children = append(children, iterators.NewMappingIterator(pebbleIter, skipMetaKeys)) + } + merged, err := iterators.NewMergingIterator(children...) + if err != nil { + closeIterators(children) + return iterators.NewInvalidIterator(err) + } + return merged +} + +// Used to cause the raw global iterator to skip _meta/* keys. +func skipMetaKeys(key, value []byte) ([]byte, []byte, bool, error) { + return key, value, ktype.IsMetaKey(key), nil +} + +func closeIterators(iters []dbm.Iterator) { + for _, it := range iters { + if it != nil { + _ = it.Close() + } + } } diff --git a/sei-db/state_db/sc/flatkv/store_read_test.go b/sei-db/state_db/sc/flatkv/store_read_test.go index f54ec08016..5c5de0ddaa 100644 --- a/sei-db/state_db/sc/flatkv/store_read_test.go +++ b/sei-db/state_db/sc/flatkv/store_read_test.go @@ -1,6 +1,7 @@ package flatkv import ( + "bytes" "encoding/binary" "testing" @@ -639,6 +640,66 @@ func TestGetAfterReopenAllKeyTypes(t *testing.T) { require.Equal(t, []byte{0x77}, got) } +// ============================================================================= +// RawGlobalIterator +// ============================================================================= + +func TestRawGlobalIterator_LexOrderAcrossDBs(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + addr := addrN(0x42) + slot := slotN(0x01) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ + namedCS( + storagePair(addr, slot, padLeft32(0x01)), + &proto.KVPair{Key: keys.BuildEVMKey(keys.EVMKeyCode, addr[:]), Value: []byte{0x60}}, + noncePair(addr, 1), + ), + })) + commitAndCheck(t, s) + + storageKey := storagePhysKey(addr, slot) + codeKey := ktype.EVMPhysicalKey(keys.EVMKeyCode, addr[:]) + accountKey := accountPhysKey(addr) + + keys := collectIterKeys(t, s.RawGlobalIterator()) + + storageIdx, codeIdx, accountIdx := -1, -1, -1 + for i, key := range keys { + switch { + case bytes.Equal(key, storageKey): + storageIdx = i + case bytes.Equal(key, codeKey): + codeIdx = i + case bytes.Equal(key, accountKey): + accountIdx = i + } + } + require.NotEqual(t, -1, storageIdx) + require.NotEqual(t, -1, codeIdx) + require.NotEqual(t, -1, accountIdx) + require.Less(t, storageIdx, codeIdx, "storage prefix 0x03 sorts before code 0x07") + require.Less(t, codeIdx, accountIdx, "code prefix 0x07 sorts before account 0x0a") +} + +func TestRawGlobalIterator_SkipsMetaKeys(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ + namedCS(storagePair(addrN(0x01), slotN(0x02), padLeft32(0x03))), + })) + commitAndCheck(t, s) + + iter := s.RawGlobalIterator() + defer iter.Close() + for ; iter.Valid(); iter.Next() { + require.False(t, ktype.IsMetaKey(iter.Key()), "iterator must skip _meta/* keys: %x", iter.Key()) + } + require.NoError(t, iter.Error()) +} + // ============================================================================= // R-12, R-13: Iterator Pending Write Visibility // ============================================================================= @@ -715,6 +776,17 @@ func iterCount(t *testing.T, iter dbm.Iterator) int { return count } +func collectIterKeys(t *testing.T, iter dbm.Iterator) [][]byte { + t.Helper() + defer iter.Close() + var keys [][]byte + for ; iter.Valid(); iter.Next() { + keys = append(keys, bytes.Clone(iter.Key())) + } + require.NoError(t, iter.Error()) + return keys +} + func TestGetNilKey(t *testing.T) { s := setupTestStore(t) defer s.Close() diff --git a/sei-db/state_db/sc/migration/OPERATIONS.md b/sei-db/state_db/sc/migration/OPERATIONS.md index e9c4a418a3..d6319ce901 100644 --- a/sei-db/state_db/sc/migration/OPERATIONS.md +++ b/sei-db/state_db/sc/migration/OPERATIONS.md @@ -254,7 +254,7 @@ Exit 0 on pass, non-zero on any failure. On non-zero, print up to `N` divergent **Library dependencies**: - T1's package-private migration helpers (boundary read) -- `flatkv.RawGlobalIterator` (returns a positioned `dbm.Iterator`; use `for ; iter.Valid(); iter.Next()`) +- `flatkv.RawGlobalIterator` (returns a positioned `dbm.Iterator` over all data DBs in global lex order; use `for ; iter.Valid(); iter.Next()`) - `memiavl.NewMultiTreeExporter` for the memiavl-evm walk - `flatkv.NewImportTranslator` for the memiavl -> physical-key mapping (`ground-truth` mode only) - `flatkv/lthash` for the digest diff --git a/sei-db/state_db/ss/composite/store.go b/sei-db/state_db/ss/composite/store.go index 06cbd03ead..2d4102a486 100644 --- a/sei-db/state_db/ss/composite/store.go +++ b/sei-db/state_db/ss/composite/store.go @@ -6,6 +6,8 @@ import ( "os" "sync" + dbm "github.com/tendermint/tm-db" + "github.com/sei-protocol/sei-chain/sei-db/common/keys" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/config" @@ -191,14 +193,14 @@ func (s *CompositeStateStore) Has(storeKey string, version int64, key []byte) (b return s.cosmosStore.Has(storeKey, version, key) } -func (s *CompositeStateStore) Iterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (s *CompositeStateStore) Iterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if s.evmRouted(storeKey) { return s.evmStore.Iterator(storeKey, version, start, end) } return s.cosmosStore.Iterator(storeKey, version, start, end) } -func (s *CompositeStateStore) ReverseIterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (s *CompositeStateStore) ReverseIterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { if s.evmRouted(storeKey) { return s.evmStore.ReverseIterator(storeKey, version, start, end) } diff --git a/sei-db/state_db/ss/composite/store_test.go b/sei-db/state_db/ss/composite/store_test.go index 163e1fea53..ad83c860f7 100644 --- a/sei-db/state_db/ss/composite/store_test.go +++ b/sei-db/state_db/ss/composite/store_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + dbm "github.com/tendermint/tm-db" + commonevm "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/db_engine/types" @@ -31,11 +33,11 @@ func (m *mockImportStateStore) Has(storeKey string, version int64, key []byte) ( return false, nil } -func (m *mockImportStateStore) Iterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (m *mockImportStateStore) Iterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { return nil, nil } -func (m *mockImportStateStore) ReverseIterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (m *mockImportStateStore) ReverseIterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { return nil, nil } diff --git a/sei-db/state_db/ss/cosmos/store.go b/sei-db/state_db/ss/cosmos/store.go index b68538b7fa..5b02d8ed15 100644 --- a/sei-db/state_db/ss/cosmos/store.go +++ b/sei-db/state_db/ss/cosmos/store.go @@ -1,6 +1,8 @@ package cosmos import ( + dbm "github.com/tendermint/tm-db" + "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" "github.com/sei-protocol/sei-chain/sei-db/proto" ) @@ -27,11 +29,11 @@ func (s *CosmosStateStore) Has(storeKey string, version int64, key []byte) (bool return s.db.Has(storeKey, version, key) } -func (s *CosmosStateStore) Iterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (s *CosmosStateStore) Iterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { return s.db.Iterator(storeKey, version, start, end) } -func (s *CosmosStateStore) ReverseIterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (s *CosmosStateStore) ReverseIterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { return s.db.ReverseIterator(storeKey, version, start, end) } diff --git a/sei-db/state_db/ss/evm/store.go b/sei-db/state_db/ss/evm/store.go index a13f52eaf9..01337940f3 100644 --- a/sei-db/state_db/ss/evm/store.go +++ b/sei-db/state_db/ss/evm/store.go @@ -5,6 +5,8 @@ import ( "path/filepath" "sync" + dbm "github.com/tendermint/tm-db" + commonevm "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/db_engine/types" @@ -101,7 +103,7 @@ func (s *EVMStateStore) Has(_ string, version int64, key []byte) (bool, error) { return db.Has(EVMStoreKey, version, key) } -func (s *EVMStateStore) Iterator(_ string, version int64, start, end []byte) (types.DBIterator, error) { +func (s *EVMStateStore) Iterator(_ string, version int64, start, end []byte) (dbm.Iterator, error) { if !s.separateDBs { return s.primaryDB().Iterator(EVMStoreKey, version, start, end) } @@ -112,7 +114,7 @@ func (s *EVMStateStore) Iterator(_ string, version int64, start, end []byte) (ty return db.Iterator(EVMStoreKey, version, start, end) } -func (s *EVMStateStore) ReverseIterator(_ string, version int64, start, end []byte) (types.DBIterator, error) { +func (s *EVMStateStore) ReverseIterator(_ string, version int64, start, end []byte) (dbm.Iterator, error) { if !s.separateDBs { return s.primaryDB().ReverseIterator(EVMStoreKey, version, start, end) } diff --git a/sei-db/state_db/ss/pruning/manager_test.go b/sei-db/state_db/ss/pruning/manager_test.go index e46f873f6a..33bceedf52 100644 --- a/sei-db/state_db/ss/pruning/manager_test.go +++ b/sei-db/state_db/ss/pruning/manager_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + dbm "github.com/tendermint/tm-db" + "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/stretchr/testify/require" @@ -40,10 +42,10 @@ func (m *mockStateStore) SetEarliestVersion(version int64, ignoreVersion bool) e func (m *mockStateStore) WriteBlockRangeHash(storeKey string, beginBlockRange, endBlockRange int64, hash []byte) error { return nil } -func (m *mockStateStore) Iterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (m *mockStateStore) Iterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { return nil, nil } -func (m *mockStateStore) ReverseIterator(storeKey string, version int64, start, end []byte) (types.DBIterator, error) { +func (m *mockStateStore) ReverseIterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) { return nil, nil } func (m *mockStateStore) ApplyChangesetAsync(version int64, changesets []*proto.NamedChangeSet) error { diff --git a/sei-db/tools/cmd/seidb/operations/dump_flatkv.go b/sei-db/tools/cmd/seidb/operations/dump_flatkv.go index 1c804d6d71..184a34e194 100644 --- a/sei-db/tools/cmd/seidb/operations/dump_flatkv.go +++ b/sei-db/tools/cmd/seidb/operations/dump_flatkv.go @@ -18,10 +18,9 @@ const ( flatkvBucketLegacy = "legacy" ) -// flatkvBucketOrder lists the logical bucket names in the same order -// RawGlobalIterator returns them (account → code → storage → legacy). Keeping -// this as the single source of truth lets us loop once for both CLI -// validation and per-bucket file allocation. +// flatkvBucketOrder lists the logical bucket names for dump output files. +// RawGlobalIterator emits keys in global lex order; this order is used only +// for CLI validation and per-bucket file allocation. var flatkvBucketOrder = []string{flatkvBucketAccount, flatkvBucketCode, flatkvBucketStorage, flatkvBucketLegacy} // DumpFlatKVCmd dumps every (physical key, value) pair of a FlatKV store @@ -184,8 +183,7 @@ func flushAndCloseBucketWriters(files map[string]*os.File, writers map[string]*b // openBucketWriters creates per-bucket output files inside outputDir. When // bucket != "" only that bucket's writer is populated; unselected buckets // are absent from the returned maps, which the scan loop treats as "skip -// writes for this key but keep iterating" (the iterator is sequential and -// cannot cheaply skip an entire sub-DB without package-private access). +// writes for this key but keep iterating" over the full merged keyspace. func openBucketWriters(outputDir string, version int64, bucket string) (map[string]*os.File, map[string]*bufio.Writer, error) { files := make(map[string]*os.File, len(flatkvBucketOrder)) writers := make(map[string]*bufio.Writer, len(flatkvBucketOrder)) From caf43db20465e5e3f181829bcc0d995542ecfd53 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 08:43:00 -0500 Subject: [PATCH 04/16] made suggested changes --- sei-db/common/iterators/mapping_iterator.go | 5 +- .../common/iterators/mapping_iterator_test.go | 41 ++++++++++++ sei-db/common/iterators/merging_iterator.go | 64 ++++++++++++++----- .../common/iterators/merging_iterator_test.go | 41 ++++++++++-- sei-db/state_db/sc/composite/store_test.go | 26 ++++---- sei-db/state_db/sc/flatkv/api.go | 2 +- sei-db/state_db/sc/flatkv/exporter.go | 6 +- sei-db/state_db/sc/flatkv/store_read.go | 12 ++-- sei-db/state_db/sc/flatkv/store_read_test.go | 20 ++++-- sei-db/state_db/sc/flatkv/testutil_test.go | 5 +- sei-db/state_db/sc/migration/OPERATIONS.md | 2 +- .../migration_test_framework_test.go | 3 +- .../tools/cmd/seidb/operations/dump_flatkv.go | 5 +- .../cmd/seidb/operations/flatkv_state_size.go | 5 +- 14 files changed, 184 insertions(+), 53 deletions(-) diff --git a/sei-db/common/iterators/mapping_iterator.go b/sei-db/common/iterators/mapping_iterator.go index 8aafac77e4..357a4dbe7f 100644 --- a/sei-db/common/iterators/mapping_iterator.go +++ b/sei-db/common/iterators/mapping_iterator.go @@ -43,7 +43,7 @@ type mappingIterator struct { // NewMappingIterator returns an iterator that emits remapped key/value pairs // from parent, skipping pairs for which remapper returns skip=true. -func NewMappingIterator(parent dbm.Iterator, remapper IteratorRemapper) *mappingIterator { +func NewMappingIterator(parent dbm.Iterator, remapper IteratorRemapper) dbm.Iterator { m := &mappingIterator{ parent: parent, remapper: remapper, @@ -91,6 +91,9 @@ func (m *mappingIterator) advance() { } m.parent.Next() } + if err := m.parent.Error(); err != nil { + m.fail(err) + } } // fail records the first error, closes the parent, and clears it so no further diff --git a/sei-db/common/iterators/mapping_iterator_test.go b/sei-db/common/iterators/mapping_iterator_test.go index a61f0512e0..ff4e74eb7d 100644 --- a/sei-db/common/iterators/mapping_iterator_test.go +++ b/sei-db/common/iterators/mapping_iterator_test.go @@ -1,11 +1,13 @@ package iterators_test import ( + "bytes" "errors" "testing" "github.com/sei-protocol/sei-chain/sei-db/common/iterators" "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" ) var errRemap = errors.New("remap failed") @@ -63,6 +65,45 @@ func TestMappingIterator_EmptyParent(t *testing.T) { require.NoError(t, mapIter.Error()) } +var errSkipNext = errors.New("skip next failed") + +// invalidAfterFirstNextIterator becomes invalid with a sticky error after the first +// Next(), matching pebble/tm-db behavior when Next hits an I/O failure. +type invalidAfterFirstNextIterator struct { + dbm.Iterator + didNext bool +} + +func (child *invalidAfterFirstNextIterator) Next() { + child.didNext = true + child.Iterator.Next() +} + +func (child *invalidAfterFirstNextIterator) Valid() bool { + if child.didNext { + return false + } + return child.Iterator.Valid() +} + +func (child *invalidAfterFirstNextIterator) Error() error { + if child.didNext { + return errSkipNext + } + return child.Iterator.Error() +} + +func TestMappingIterator_ParentErrorAfterSkipNext(t *testing.T) { + // Keys must sort with the skipped key first (memDB iterates in lex order). + parent := &invalidAfterFirstNextIterator{Iterator: memIter(t, []byte("_meta"), []byte("user"))} + mapIter := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + return key, value, bytes.HasPrefix(key, []byte("_meta")), nil + }) + + require.False(t, mapIter.Valid()) + require.ErrorIs(t, mapIter.Error(), errSkipNext) +} + func TestInvalidIterator(t *testing.T) { errConstruction := errors.New("open failed") it := iterators.NewInvalidIterator(errConstruction) diff --git a/sei-db/common/iterators/merging_iterator.go b/sei-db/common/iterators/merging_iterator.go index 73f4c210f0..749208418f 100644 --- a/sei-db/common/iterators/merging_iterator.go +++ b/sei-db/common/iterators/merging_iterator.go @@ -9,9 +9,14 @@ import ( var _ dbm.Iterator = (*mergingIterator)(nil) -// mergingIterator merges multiple iterators into a single iterator. Output is -// emitted in lexicographic order so long as each input iterator is already in -// lexicographic order. +// mergingIterator merges multiple iterators into a single iterator. +// +// Each child must be in ascending lexicographic order and must not emit +// duplicate keys; if either assumption is violated, behavior is undefined. +// +// Output is in global lexicographic order. When multiple children share the +// same key, that key is emitted once; the rightmost child (highest argument +// index) supplies the value and lower-index copies are dropped. type mergingIterator struct { // the nested iterators to combine iterators []dbm.Iterator @@ -29,12 +34,14 @@ type mergingIterator struct { err error } -// NewMergingIterator combines iterators into a single iterator. Output is -// emitted in lexicographic order so long as each input iterator is already in -// lexicographic order. +// NewMergingIterator combines iterators into a single iterator. // -// Intended for a small number of iterators (on the order of half a dozen). May not be performant for -// combining large numbers of iterators. +// Each child must be in ascending lexicographic order without duplicate keys; +// otherwise behavior is undefined. Output is in global lex order. Duplicate +// keys across children are emitted once; the last child wins. +// +// Intended for a small number of iterators (on the order of half a dozen). May +// not be performant for combining large numbers of iterators. func NewMergingIterator(iterators ...dbm.Iterator) (dbm.Iterator, error) { m := &mergingIterator{ iterators: make([]dbm.Iterator, len(iterators)), @@ -56,9 +63,9 @@ func NewMergingIterator(iterators ...dbm.Iterator) (dbm.Iterator, error) { return m, nil } -// findMin sets nextIteratorIndex to the index of the valid child with the -// smallest current key, or -1 if no child is valid. Child errors are checked -// here and cached on the merge iterator via fail. +// findMin sets nextIteratorIndex to the valid child with the smallest current +// key, breaking ties toward the highest index. Child errors are checked here +// and cached on the merge iterator via fail. func (m *mergingIterator) findMin() { if m.err != nil { return @@ -77,13 +84,40 @@ func (m *mergingIterator) findMin() { continue } childKey := child.Key() - if m.nextIteratorIndex < 0 || bytes.Compare(childKey, smallestKey) < 0 { + if m.nextIteratorIndex < 0 { + m.nextIteratorIndex = i + smallestKey = childKey + continue + } + cmp := bytes.Compare(childKey, smallestKey) + if cmp < 0 || (cmp == 0 && i > m.nextIteratorIndex) { m.nextIteratorIndex = i smallestKey = childKey } } } +// advanceChildrenAtKey advances every child positioned at key past that key. +func (m *mergingIterator) advanceChildrenAtKey(key []byte) { + for _, child := range m.iterators { + if child == nil { + continue + } + if err := child.Error(); err != nil { + m.fail(err) + return + } + if !child.Valid() || !bytes.Equal(child.Key(), key) { + continue + } + child.Next() + if err := child.Error(); err != nil { + m.fail(err) + return + } + } +} + // fail records the first error, closes all children, and clears iterators so no // further child methods are invoked. func (m *mergingIterator) fail(err error) { @@ -182,9 +216,9 @@ func (m *mergingIterator) Next() { return } - m.iterators[m.nextIteratorIndex].Next() - if err := m.iterators[m.nextIteratorIndex].Error(); err != nil { - m.fail(err) + currentKey := m.iterators[m.nextIteratorIndex].Key() + m.advanceChildrenAtKey(currentKey) + if m.err != nil { return } m.findMin() diff --git a/sei-db/common/iterators/merging_iterator_test.go b/sei-db/common/iterators/merging_iterator_test.go index 40c006e2cf..679ccbf5d7 100644 --- a/sei-db/common/iterators/merging_iterator_test.go +++ b/sei-db/common/iterators/merging_iterator_test.go @@ -22,6 +22,17 @@ func memIter(t *testing.T, keys ...[]byte) dbm.Iterator { return it } +func memIterKV(t *testing.T, pairs ...[2][]byte) dbm.Iterator { + t.Helper() + db := dbm.NewMemDB() + for _, pair := range pairs { + require.NoError(t, db.Set(pair[0], pair[1])) + } + it, err := db.Iterator(nil, nil) + require.NoError(t, err) + return it +} + func collect(t *testing.T, it dbm.Iterator) [][2][]byte { t.Helper() var out [][2][]byte @@ -76,18 +87,34 @@ func TestMergingIterator_LexOrder(t *testing.T) { } func TestMergingIterator_DuplicateKeys(t *testing.T) { - a := memIter(t, []byte("k"), []byte("z")) - b := memIter(t, []byte("k"), []byte("m")) - it, err := iterators.NewMergingIterator(a, b) + left := memIterKV(t, [2][]byte{[]byte("k"), []byte("v0")}, [2][]byte{[]byte("z"), []byte("z0")}) + right := memIterKV(t, [2][]byte{[]byte("k"), []byte("v1")}, [2][]byte{[]byte("m"), []byte("m1")}) + it, err := iterators.NewMergingIterator(left, right) require.NoError(t, err) defer it.Close() got := collect(t, it) require.Equal(t, [][2][]byte{ - {[]byte("k"), []byte("a")}, - {[]byte("k"), []byte("a")}, - {[]byte("m"), []byte("b")}, - {[]byte("z"), []byte("b")}, + {[]byte("k"), []byte("v1")}, + {[]byte("m"), []byte("m1")}, + {[]byte("z"), []byte("z0")}, + }, got) +} + +func TestMergingIterator_RightmostWinsOnDuplicateKey(t *testing.T) { + child0 := memIterKV(t, [2][]byte{[]byte("k"), []byte("v0")}, [2][]byte{[]byte("a"), []byte("a0")}) + child1 := memIter(t, []byte("b")) + child2 := memIterKV(t, [2][]byte{[]byte("k"), []byte("v2")}, [2][]byte{[]byte("c"), []byte("c0")}) + it, err := iterators.NewMergingIterator(child0, child1, child2) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("a"), []byte("a0")}, + {[]byte("b"), []byte("a")}, + {[]byte("c"), []byte("c0")}, + {[]byte("k"), []byte("v2")}, }, got) } diff --git a/sei-db/state_db/sc/composite/store_test.go b/sei-db/state_db/sc/composite/store_test.go index 53622c81da..0c98bb5bb2 100644 --- a/sei-db/state_db/sc/composite/store_test.go +++ b/sei-db/state_db/sc/composite/store_test.go @@ -36,19 +36,19 @@ func (f *failingEVMStore) Get(string, []byte) ([]byte, bool) { retur func (f *failingEVMStore) GetBlockHeightModified(string, []byte) (int64, bool, error) { return -1, false, nil } -func (f *failingEVMStore) Has(string, []byte) bool { return false } -func (f *failingEVMStore) RawGlobalIterator() dbm.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 (f *failingEVMStore) Has(string, []byte) bool { return false } +func (f *failingEVMStore) RawGlobalIterator() (dbm.Iterator, error) { return nil, 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 { var b [32]byte diff --git a/sei-db/state_db/sc/flatkv/api.go b/sei-db/state_db/sc/flatkv/api.go index 31b4117ca8..276cebffd8 100644 --- a/sei-db/state_db/sc/flatkv/api.go +++ b/sei-db/state_db/sc/flatkv/api.go @@ -62,7 +62,7 @@ type Store interface { // Keys are physical format: "evm/" + type_prefix_byte + stripped_key. // Pending writes are not visible. Keys and values are read-only; copy // before modifying. Caller must Close when done. - RawGlobalIterator() dbm.Iterator + RawGlobalIterator() (dbm.Iterator, error) // RootHash returns the 32-byte checksum of the working LtHash. // Note: This is the Blake3-256 digest of the underlying 2048-byte diff --git a/sei-db/state_db/sc/flatkv/exporter.go b/sei-db/state_db/sc/flatkv/exporter.go index 86317908b1..241d6a0aa5 100644 --- a/sei-db/state_db/sc/flatkv/exporter.go +++ b/sei-db/state_db/sc/flatkv/exporter.go @@ -35,7 +35,11 @@ func NewKVExporter(store *CommitStore, version int64) *KVExporter { func (e *KVExporter) Next() (interface{}, error) { if e.iter == nil { - e.iter = e.store.RawGlobalIterator() + var err error + e.iter, err = e.store.RawGlobalIterator() + if err != nil { + return nil, fmt.Errorf("raw global iterator: %w", err) + } if !e.iter.Valid() { if err := e.iter.Error(); err != nil { return nil, fmt.Errorf("iterator error: %w", err) diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index 2844fc8ff8..695735c721 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -223,23 +223,27 @@ func (s *CommitStore) getLegacyValue(moduleName string, key []byte) ([]byte, err // data DBs (account, code, storage, legacy), merged in global lexicographic // order. Within each DB, keys are in Pebble order. Per-DB _meta/* keys are // skipped. Pending writes are not visible. metadataDB is not included. -func (s *CommitStore) RawGlobalIterator() dbm.Iterator { +func (s *CommitStore) RawGlobalIterator() (dbm.Iterator, error) { dbs := s.dataDBs() children := make([]dbm.Iterator, 0, len(dbs)) for _, db := range dbs { pebbleIter, err := db.NewIter(nil) if err != nil { closeIterators(children) - return iterators.NewInvalidIterator(fmt.Errorf("open data DB iterator: %w", err)) + return nil, fmt.Errorf("open data DB iterator: %w", err) } children = append(children, iterators.NewMappingIterator(pebbleIter, skipMetaKeys)) } merged, err := iterators.NewMergingIterator(children...) if err != nil { closeIterators(children) - return iterators.NewInvalidIterator(err) + return nil, err + } + if err := merged.Error(); err != nil { + _ = merged.Close() + return nil, err } - return merged + return merged, nil } // Used to cause the raw global iterator to skip _meta/* keys. diff --git a/sei-db/state_db/sc/flatkv/store_read_test.go b/sei-db/state_db/sc/flatkv/store_read_test.go index 5c5de0ddaa..f110278fd9 100644 --- a/sei-db/state_db/sc/flatkv/store_read_test.go +++ b/sei-db/state_db/sc/flatkv/store_read_test.go @@ -663,7 +663,7 @@ func TestRawGlobalIterator_LexOrderAcrossDBs(t *testing.T) { codeKey := ktype.EVMPhysicalKey(keys.EVMKeyCode, addr[:]) accountKey := accountPhysKey(addr) - keys := collectIterKeys(t, s.RawGlobalIterator()) + keys := collectIterKeys(t, requireRawGlobalIterator(t, s)) storageIdx, codeIdx, accountIdx := -1, -1, -1 for i, key := range keys { @@ -692,7 +692,7 @@ func TestRawGlobalIterator_SkipsMetaKeys(t *testing.T) { })) commitAndCheck(t, s) - iter := s.RawGlobalIterator() + iter := requireRawGlobalIterator(t, s) defer iter.Close() for ; iter.Valid(); iter.Next() { require.False(t, ktype.IsMetaKey(iter.Key()), "iterator must skip _meta/* keys: %x", iter.Key()) @@ -716,14 +716,14 @@ func TestIteratorDoesNotSeePendingWrites(t *testing.T) { })) // Before commit: iterator should not see the pending write - iter := s.RawGlobalIterator() + iter := requireRawGlobalIterator(t, s) require.False(t, iter.Valid(), "iterator should not see pending writes") require.NoError(t, iter.Close()) commitAndCheck(t, s) // After commit: iterator should see it - iter = s.RawGlobalIterator() + iter = requireRawGlobalIterator(t, s) defer iter.Close() require.True(t, iter.Valid(), "iterator should see committed entry") require.Equal(t, storagePhysKey(addr, slot), iter.Key()) @@ -751,13 +751,13 @@ func TestIteratorDoesNotSeePendingDeletes(t *testing.T) { })) // Iterator should still see all 3 (pending delete not visible) - count := iterCount(t, s.RawGlobalIterator()) + count := iterCount(t, requireRawGlobalIterator(t, s)) require.Equal(t, 3, count, "pending delete should not affect iterator") commitAndCheck(t, s) // After commit: only 2 remain - count = iterCount(t, s.RawGlobalIterator()) + count = iterCount(t, requireRawGlobalIterator(t, s)) require.Equal(t, 2, count, "committed delete should remove entry from iterator") } @@ -765,6 +765,14 @@ func TestIteratorDoesNotSeePendingDeletes(t *testing.T) { // Helpers // ============================================================================= +func requireRawGlobalIterator(t *testing.T, s *CommitStore) dbm.Iterator { + t.Helper() + iter, err := s.RawGlobalIterator() + require.NoError(t, err) + require.NotNil(t, iter) + return iter +} + func iterCount(t *testing.T, iter dbm.Iterator) int { t.Helper() defer iter.Close() diff --git a/sei-db/state_db/sc/flatkv/testutil_test.go b/sei-db/state_db/sc/flatkv/testutil_test.go index d865a60898..5b1543ba60 100644 --- a/sei-db/state_db/sc/flatkv/testutil_test.go +++ b/sei-db/state_db/sc/flatkv/testutil_test.go @@ -189,7 +189,10 @@ func namedCS(pairs ...*proto.KVPair) *proto.NamedChangeSet { // CountKeys returns the total number of non-meta keys across all data DBs in s. // It uses RawGlobalIterator, so pending (uncommitted) writes are not counted. func CountKeys(s *CommitStore) (int64, error) { - iter := s.RawGlobalIterator() + iter, err := s.RawGlobalIterator() + if err != nil { + return 0, err + } defer func() { _ = iter.Close() }() var count int64 for ; iter.Valid(); iter.Next() { diff --git a/sei-db/state_db/sc/migration/OPERATIONS.md b/sei-db/state_db/sc/migration/OPERATIONS.md index d6319ce901..cdee753e11 100644 --- a/sei-db/state_db/sc/migration/OPERATIONS.md +++ b/sei-db/state_db/sc/migration/OPERATIONS.md @@ -254,7 +254,7 @@ Exit 0 on pass, non-zero on any failure. On non-zero, print up to `N` divergent **Library dependencies**: - T1's package-private migration helpers (boundary read) -- `flatkv.RawGlobalIterator` (returns a positioned `dbm.Iterator` over all data DBs in global lex order; use `for ; iter.Valid(); iter.Next()`) +- `flatkv.RawGlobalIterator` (returns a positioned `dbm.Iterator` and error over all data DBs in global lex order; use `for ; iter.Valid(); iter.Next()`) - `memiavl.NewMultiTreeExporter` for the memiavl-evm walk - `flatkv.NewImportTranslator` for the memiavl -> physical-key mapping (`ground-truth` mode only) - `flatkv/lthash` for the digest 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 705a6f9108..739e321452 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 @@ -335,7 +335,8 @@ func (r *TestInMemoryRouter) VerifyContainsSameData(t *testing.T, that Router) { // probability of such a collision in a test is negligible. func GetFlatKVKeyCount(t *testing.T, flatKV *flatkv.CommitStore) int64 { t.Helper() - iter := flatKV.RawGlobalIterator() + iter, err := flatKV.RawGlobalIterator() + require.NoError(t, err) defer func() { _ = iter.Close() }() var count int64 for ; iter.Valid(); iter.Next() { diff --git a/sei-db/tools/cmd/seidb/operations/dump_flatkv.go b/sei-db/tools/cmd/seidb/operations/dump_flatkv.go index 184a34e194..1c938dd0f2 100644 --- a/sei-db/tools/cmd/seidb/operations/dump_flatkv.go +++ b/sei-db/tools/cmd/seidb/operations/dump_flatkv.go @@ -122,7 +122,10 @@ func dumpFlatKVFromStore(store *flatkv.CommitStore, outputDir string, version in } }() - iter := store.RawGlobalIterator() + iter, err := store.RawGlobalIterator() + if err != nil { + return fmt.Errorf("raw global iterator: %w", err) + } defer func() { _ = iter.Close() }() counts := make(map[string]uint64, len(flatkvBucketOrder)) diff --git a/sei-db/tools/cmd/seidb/operations/flatkv_state_size.go b/sei-db/tools/cmd/seidb/operations/flatkv_state_size.go index 5a416e6d4e..873624c47a 100644 --- a/sei-db/tools/cmd/seidb/operations/flatkv_state_size.go +++ b/sei-db/tools/cmd/seidb/operations/flatkv_state_size.go @@ -45,7 +45,10 @@ func collectFlatKVStateSize(store *flatkv.CommitStore) (*FlatKVStateSizeResult, ContractSizes: make(map[string]*utils.ContractSizeEntry), } - iter := store.RawGlobalIterator() + iter, err := store.RawGlobalIterator() + if err != nil { + return nil, fmt.Errorf("raw global iterator: %w", err) + } defer func() { _ = iter.Close() }() for ; iter.Valid(); iter.Next() { From f01b9b0c68ecacd166839ee29f4ec8bd01d051dd Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 08:57:55 -0500 Subject: [PATCH 05/16] made suggested changes --- sei-db/common/iterators/invalid_iterator.go | 31 --------- sei-db/common/iterators/merging_iterator.go | 2 +- .../common/iterators/merging_iterator_test.go | 66 ++++++++++++++++++- 3 files changed, 66 insertions(+), 33 deletions(-) delete mode 100644 sei-db/common/iterators/invalid_iterator.go diff --git a/sei-db/common/iterators/invalid_iterator.go b/sei-db/common/iterators/invalid_iterator.go deleted file mode 100644 index 72abe0e04d..0000000000 --- a/sei-db/common/iterators/invalid_iterator.go +++ /dev/null @@ -1,31 +0,0 @@ -package iterators - -import dbm "github.com/tendermint/tm-db" - -var _ dbm.Iterator = (*invalidIterator)(nil) - -// invalidIterator is always invalid and reports a fixed construction error. -type invalidIterator struct { - err error -} - -// NewInvalidIterator returns an iterator that is never valid and reports err -// from Error(). Used when iterator construction fails but the API cannot -// return an error (e.g. RawGlobalIterator). -func NewInvalidIterator(err error) dbm.Iterator { - return &invalidIterator{err: err} -} - -func (m *invalidIterator) Close() error { return nil } - -func (m *invalidIterator) Domain() ([]byte, []byte) { return nil, nil } - -func (m *invalidIterator) Error() error { return m.err } - -func (m *invalidIterator) Key() []byte { return nil } - -func (m *invalidIterator) Next() {} - -func (m *invalidIterator) Valid() bool { return false } - -func (m *invalidIterator) Value() []byte { return nil } diff --git a/sei-db/common/iterators/merging_iterator.go b/sei-db/common/iterators/merging_iterator.go index 749208418f..58e5d3243a 100644 --- a/sei-db/common/iterators/merging_iterator.go +++ b/sei-db/common/iterators/merging_iterator.go @@ -216,7 +216,7 @@ func (m *mergingIterator) Next() { return } - currentKey := m.iterators[m.nextIteratorIndex].Key() + currentKey := bytes.Clone(m.iterators[m.nextIteratorIndex].Key()) m.advanceChildrenAtKey(currentKey) if m.err != nil { return diff --git a/sei-db/common/iterators/merging_iterator_test.go b/sei-db/common/iterators/merging_iterator_test.go index 679ccbf5d7..e905c10944 100644 --- a/sei-db/common/iterators/merging_iterator_test.go +++ b/sei-db/common/iterators/merging_iterator_test.go @@ -1,6 +1,7 @@ package iterators_test import ( + "bytes" "errors" "testing" @@ -37,7 +38,10 @@ func collect(t *testing.T, it dbm.Iterator) [][2][]byte { t.Helper() var out [][2][]byte for ; it.Valid(); it.Next() { - out = append(out, [2][]byte{it.Key(), it.Value()}) + out = append(out, [2][]byte{ + bytes.Clone(it.Key()), + bytes.Clone(it.Value()), + }) } require.NoError(t, it.Error()) return out @@ -192,6 +196,66 @@ func TestMergingIterator_CachesChildError(t *testing.T) { require.NoError(t, merged.Close()) } +// sharedKeyBufIterator models backends that reuse one key buffer across iterators +// (e.g. a shared Pebble key scratch). Next() on any child overwrites Key() for all. +type sharedKeyBufIterator struct { + keys [][]byte + values [][]byte + idx int + keyBuf *[]byte +} + +func (s *sharedKeyBufIterator) Domain() (start, end []byte) { return nil, nil } +func (s *sharedKeyBufIterator) Valid() bool { return s.idx < len(s.keys) } +func (s *sharedKeyBufIterator) Key() []byte { + if !s.Valid() { + return nil + } + *s.keyBuf = append((*s.keyBuf)[:0], s.keys[s.idx]...) + return *s.keyBuf +} +func (s *sharedKeyBufIterator) Value() []byte { + if !s.Valid() { + return nil + } + return s.values[s.idx] +} +func (s *sharedKeyBufIterator) Next() { + if !s.Valid() { + return + } + s.idx++ + if s.Valid() { + *s.keyBuf = append((*s.keyBuf)[:0], s.keys[s.idx]...) + } +} +func (s *sharedKeyBufIterator) Error() error { return nil } +func (s *sharedKeyBufIterator) Close() error { return nil } + +func TestMergingIterator_DuplicateKeys_SharedKeyBuffer(t *testing.T) { + var keyBuf []byte + left := &sharedKeyBufIterator{ + keyBuf: &keyBuf, + keys: [][]byte{[]byte("k"), []byte("z")}, + values: [][]byte{[]byte("v0"), []byte("z0")}, + } + right := &sharedKeyBufIterator{ + keyBuf: &keyBuf, + keys: [][]byte{[]byte("k"), []byte("m")}, + values: [][]byte{[]byte("v1"), []byte("m1")}, + } + it, err := iterators.NewMergingIterator(left, right) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("k"), []byte("v1")}, + {[]byte("m"), []byte("m1")}, + {[]byte("z"), []byte("z0")}, + }, got) +} + func TestMergingIterator_ClosesChildren(t *testing.T) { child := &closeTrackingIterator{Iterator: memIter(t, []byte("x"))} it, err := iterators.NewMergingIterator(child) From 155cc8b50560b4eb0a66016144801fd1c123b323 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 09:08:01 -0500 Subject: [PATCH 06/16] fix compile issue --- sei-db/common/iterators/mapping_iterator.go | 23 ++++----- .../common/iterators/mapping_iterator_test.go | 51 +++++++++++++++---- sei-db/state_db/sc/flatkv/store_read.go | 7 ++- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/sei-db/common/iterators/mapping_iterator.go b/sei-db/common/iterators/mapping_iterator.go index 357a4dbe7f..f3c647ece9 100644 --- a/sei-db/common/iterators/mapping_iterator.go +++ b/sei-db/common/iterators/mapping_iterator.go @@ -43,25 +43,24 @@ type mappingIterator struct { // NewMappingIterator returns an iterator that emits remapped key/value pairs // from parent, skipping pairs for which remapper returns skip=true. -func NewMappingIterator(parent dbm.Iterator, remapper IteratorRemapper) dbm.Iterator { - m := &mappingIterator{ - parent: parent, - remapper: remapper, - } +func NewMappingIterator(parent dbm.Iterator, remapper IteratorRemapper) (dbm.Iterator, error) { if parent == nil { - m.err = fmt.Errorf("nil parent iterator") - return m + return nil, fmt.Errorf("nil parent iterator") } if remapper == nil { - m.err = fmt.Errorf("nil remapper") - return m + _ = parent.Close() + return nil, fmt.Errorf("nil remapper") } if err := parent.Error(); err != nil { - m.fail(err) - return m + _ = parent.Close() + return nil, fmt.Errorf("parent iterator error: %w", err) + } + m := &mappingIterator{ + parent: parent, + remapper: remapper, } m.advance() - return m + return m, nil } // advance moves to the next non-skipped parent entry, or clears the position if diff --git a/sei-db/common/iterators/mapping_iterator_test.go b/sei-db/common/iterators/mapping_iterator_test.go index ff4e74eb7d..bc40349a90 100644 --- a/sei-db/common/iterators/mapping_iterator_test.go +++ b/sei-db/common/iterators/mapping_iterator_test.go @@ -14,12 +14,13 @@ var errRemap = errors.New("remap failed") func TestMappingIterator_SkipsKeys(t *testing.T) { parent := memIter(t, []byte("a"), []byte("b"), []byte("c")) - mapIter := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + mapIter, err := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { if key[0] == 'b' { return nil, nil, true, nil } return key, value, false, nil }) + require.NoError(t, err) got := collect(t, mapIter) require.Equal(t, [][2][]byte{ @@ -30,9 +31,10 @@ func TestMappingIterator_SkipsKeys(t *testing.T) { func TestMappingIterator_RemapsKeyValue(t *testing.T) { parent := memIter(t, []byte("k")) - mapIter := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + mapIter, err := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { return append([]byte("x"), key...), append([]byte("y"), value...), false, nil }) + require.NoError(t, err) require.True(t, mapIter.Valid()) require.Equal(t, []byte("xk"), mapIter.Key()) @@ -42,12 +44,13 @@ func TestMappingIterator_RemapsKeyValue(t *testing.T) { func TestMappingIterator_RemapperError(t *testing.T) { parent := memIter(t, []byte("a"), []byte("b")) - mapIter := iterators.NewMappingIterator(parent, func(key, _ []byte) ([]byte, []byte, bool, error) { + mapIter, err := iterators.NewMappingIterator(parent, func(key, _ []byte) ([]byte, []byte, bool, error) { if key[0] == 'b' { return nil, nil, false, errRemap } return key, key, false, nil }) + require.NoError(t, err) require.True(t, mapIter.Valid()) require.Equal(t, []byte("a"), mapIter.Key()) @@ -58,9 +61,10 @@ func TestMappingIterator_RemapperError(t *testing.T) { func TestMappingIterator_EmptyParent(t *testing.T) { parent := memIter(t) - mapIter := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + mapIter, err := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { return key, value, false, nil }) + require.NoError(t, err) require.False(t, mapIter.Valid()) require.NoError(t, mapIter.Error()) } @@ -96,18 +100,43 @@ func (child *invalidAfterFirstNextIterator) Error() error { func TestMappingIterator_ParentErrorAfterSkipNext(t *testing.T) { // Keys must sort with the skipped key first (memDB iterates in lex order). parent := &invalidAfterFirstNextIterator{Iterator: memIter(t, []byte("_meta"), []byte("user"))} - mapIter := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + mapIter, err := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { return key, value, bytes.HasPrefix(key, []byte("_meta")), nil }) + require.NoError(t, err) require.False(t, mapIter.Valid()) require.ErrorIs(t, mapIter.Error(), errSkipNext) } -func TestInvalidIterator(t *testing.T) { - errConstruction := errors.New("open failed") - it := iterators.NewInvalidIterator(errConstruction) - require.False(t, it.Valid()) - require.ErrorIs(t, it.Error(), errConstruction) - require.NoError(t, it.Close()) +func TestNewMappingIterator_NilParent(t *testing.T) { + _, err := iterators.NewMappingIterator(nil, func([]byte, []byte) ([]byte, []byte, bool, error) { + return nil, nil, false, nil + }) + require.Error(t, err) +} + +func TestNewMappingIterator_NilRemapper(t *testing.T) { + parent := memIter(t, []byte("k")) + _, err := iterators.NewMappingIterator(parent, nil) + require.Error(t, err) +} + +var errConstruction = errors.New("open failed") + +// errAtConstructionIterator reports a sticky error from construction. +type errAtConstructionIterator struct { + dbm.Iterator +} + +func (child *errAtConstructionIterator) Error() error { + return errConstruction +} + +func TestNewMappingIterator_ParentError(t *testing.T) { + parent := &errAtConstructionIterator{Iterator: memIter(t, []byte("k"))} + _, err := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + return key, value, false, nil + }) + require.ErrorIs(t, err, errConstruction) } diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index 695735c721..cea795be21 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -232,7 +232,12 @@ func (s *CommitStore) RawGlobalIterator() (dbm.Iterator, error) { closeIterators(children) return nil, fmt.Errorf("open data DB iterator: %w", err) } - children = append(children, iterators.NewMappingIterator(pebbleIter, skipMetaKeys)) + mapped, err := iterators.NewMappingIterator(pebbleIter, skipMetaKeys) + if err != nil { + closeIterators(children) + return nil, err + } + children = append(children, mapped) } merged, err := iterators.NewMergingIterator(children...) if err != nil { From d85e888d933f8ee48539befdc748ff674a5604d3 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 09:25:56 -0500 Subject: [PATCH 07/16] fix lint --- evmrpc/watermark_manager_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evmrpc/watermark_manager_test.go b/evmrpc/watermark_manager_test.go index 810e915081..95e4d868da 100644 --- a/evmrpc/watermark_manager_test.go +++ b/evmrpc/watermark_manager_test.go @@ -16,7 +16,6 @@ import ( storetypes "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" - dbm "github.com/tendermint/tm-db" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" "github.com/sei-protocol/sei-chain/sei-db/proto" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -25,6 +24,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" + dbm "github.com/tendermint/tm-db" ) func TestWatermarksAggregatesSources(t *testing.T) { From d673ffd68ce6f085bfb9aa9241e471f97a17bf80 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 10:26:37 -0500 Subject: [PATCH 08/16] made suggested changes --- sei-db/common/iterators/mapping_iterator.go | 4 ++++ sei-db/common/iterators/merging_iterator.go | 2 ++ sei-db/db_engine/rocksdb/mvcc/iterator.go | 2 -- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sei-db/common/iterators/mapping_iterator.go b/sei-db/common/iterators/mapping_iterator.go index f3c647ece9..a3415bfbaf 100644 --- a/sei-db/common/iterators/mapping_iterator.go +++ b/sei-db/common/iterators/mapping_iterator.go @@ -60,6 +60,10 @@ func NewMappingIterator(parent dbm.Iterator, remapper IteratorRemapper) (dbm.Ite remapper: remapper, } m.advance() + if err := m.Error(); err != nil { + _ = m.Close() + return nil, err + } return m, nil } diff --git a/sei-db/common/iterators/merging_iterator.go b/sei-db/common/iterators/merging_iterator.go index 58e5d3243a..7d18726204 100644 --- a/sei-db/common/iterators/merging_iterator.go +++ b/sei-db/common/iterators/merging_iterator.go @@ -51,9 +51,11 @@ func NewMergingIterator(iterators ...dbm.Iterator) (dbm.Iterator, error) { for i, child := range m.iterators { if child == nil { + _ = m.Close() return nil, fmt.Errorf("nil iterator at index %d", i) } if err := child.Error(); err != nil { + _ = m.Close() return nil, fmt.Errorf("error in iterator at index %d: %w", i, err) } } diff --git a/sei-db/db_engine/rocksdb/mvcc/iterator.go b/sei-db/db_engine/rocksdb/mvcc/iterator.go index b8266d24d9..47e52d2338 100644 --- a/sei-db/db_engine/rocksdb/mvcc/iterator.go +++ b/sei-db/db_engine/rocksdb/mvcc/iterator.go @@ -9,8 +9,6 @@ import ( "github.com/linxGnu/grocksdb" dbm "github.com/tendermint/tm-db" - - "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" ) var _ dbm.Iterator = (*iterator)(nil) From 5460987f50ce03a33d56c180c278546d12488e90 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 10:35:29 -0500 Subject: [PATCH 09/16] fix broken test --- sei-db/common/iterators/mapping_iterator_test.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sei-db/common/iterators/mapping_iterator_test.go b/sei-db/common/iterators/mapping_iterator_test.go index bc40349a90..6be609392f 100644 --- a/sei-db/common/iterators/mapping_iterator_test.go +++ b/sei-db/common/iterators/mapping_iterator_test.go @@ -97,16 +97,13 @@ func (child *invalidAfterFirstNextIterator) Error() error { return child.Iterator.Error() } -func TestMappingIterator_ParentErrorAfterSkipNext(t *testing.T) { +func TestNewMappingIterator_ParentErrorAfterSkipNext(t *testing.T) { // Keys must sort with the skipped key first (memDB iterates in lex order). parent := &invalidAfterFirstNextIterator{Iterator: memIter(t, []byte("_meta"), []byte("user"))} - mapIter, err := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { + _, err := iterators.NewMappingIterator(parent, func(key, value []byte) ([]byte, []byte, bool, error) { return key, value, bytes.HasPrefix(key, []byte("_meta")), nil }) - require.NoError(t, err) - - require.False(t, mapIter.Valid()) - require.ErrorIs(t, mapIter.Error(), errSkipNext) + require.ErrorIs(t, err, errSkipNext) } func TestNewMappingIterator_NilParent(t *testing.T) { From 9948bbbfe7fc834be10a4e2b77117bebece671a6 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 10:58:53 -0500 Subject: [PATCH 10/16] made suggested change --- sei-db/common/iterators/merging_iterator.go | 4 +-- .../common/iterators/merging_iterator_test.go | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sei-db/common/iterators/merging_iterator.go b/sei-db/common/iterators/merging_iterator.go index 7d18726204..37947f6a75 100644 --- a/sei-db/common/iterators/merging_iterator.go +++ b/sei-db/common/iterators/merging_iterator.go @@ -88,13 +88,13 @@ func (m *mergingIterator) findMin() { childKey := child.Key() if m.nextIteratorIndex < 0 { m.nextIteratorIndex = i - smallestKey = childKey + smallestKey = bytes.Clone(childKey) continue } cmp := bytes.Compare(childKey, smallestKey) if cmp < 0 || (cmp == 0 && i > m.nextIteratorIndex) { m.nextIteratorIndex = i - smallestKey = childKey + smallestKey = bytes.Clone(childKey) } } } diff --git a/sei-db/common/iterators/merging_iterator_test.go b/sei-db/common/iterators/merging_iterator_test.go index e905c10944..2a1a73e364 100644 --- a/sei-db/common/iterators/merging_iterator_test.go +++ b/sei-db/common/iterators/merging_iterator_test.go @@ -256,6 +256,31 @@ func TestMergingIterator_DuplicateKeys_SharedKeyBuffer(t *testing.T) { }, got) } +// Children with different initial keys and a shared key buffer: findMin must +// clone the first child's key before the second child's Key() overwrites it. +func TestMergingIterator_SharedKeyBuffer_DifferentInitialKeys(t *testing.T) { + var keyBuf []byte + left := &sharedKeyBufIterator{ + keyBuf: &keyBuf, + keys: [][]byte{[]byte("m"), []byte("z")}, + values: [][]byte{[]byte("m0"), []byte("z0")}, + } + right := &sharedKeyBufIterator{ + keyBuf: &keyBuf, + keys: [][]byte{[]byte("z")}, + values: [][]byte{[]byte("z1")}, + } + it, err := iterators.NewMergingIterator(left, right) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("m"), []byte("m0")}, + {[]byte("z"), []byte("z1")}, + }, got) +} + func TestMergingIterator_ClosesChildren(t *testing.T) { child := &closeTrackingIterator{Iterator: memIter(t, []byte("x"))} it, err := iterators.NewMergingIterator(child) From c9e20f14f1dfd9ffb641d5baf4aedb5e45c1252b Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 11:30:40 -0500 Subject: [PATCH 11/16] update flatKV api, add map iterator --- sei-db/common/iterators/map_iterator.go | 145 +++++++++++++++++ sei-db/common/iterators/map_iterator_test.go | 156 +++++++++++++++++++ sei-db/state_db/sc/composite/store_test.go | 25 +-- sei-db/state_db/sc/flatkv/api.go | 14 ++ sei-db/state_db/sc/flatkv/store_read.go | 10 ++ 5 files changed, 339 insertions(+), 11 deletions(-) create mode 100644 sei-db/common/iterators/map_iterator.go create mode 100644 sei-db/common/iterators/map_iterator_test.go diff --git a/sei-db/common/iterators/map_iterator.go b/sei-db/common/iterators/map_iterator.go new file mode 100644 index 0000000000..1a0238d6a4 --- /dev/null +++ b/sei-db/common/iterators/map_iterator.go @@ -0,0 +1,145 @@ +package iterators + +import ( + "bytes" + "fmt" + "sort" + + "github.com/sei-protocol/sei-chain/sei-db/common/utils" + dbm "github.com/tendermint/tm-db" +) + +var _ dbm.Iterator = (*mapIterator)(nil) + +// Iterates over a map of key/value pairs. +type mapIterator struct { + // kvPairs holds keys in iteration order, filtered to the domain. + kvPairs []kvPair + // currentIndex is the index of the entry returned by Key/Value. + currentIndex int + // start is the inclusive lower bound of the domain. + start []byte + // end is the exclusive upper bound of the domain. + end []byte +} + +type kvPair struct { + key []byte + value []byte +} + +// NewMapIterator returns an iterator over the union of maps in lexicographic order +// (or reverse lex order when ascending is false). start is inclusive; end is +// exclusive. nil start or end means unbounded on that side. Duplicate keys across +// maps are rejected. +func NewMapIterator( + start []byte, + end []byte, + ascending bool, + maps ...map[string][]byte, +) (dbm.Iterator, error) { + pairs, err := buildMapPairs(start, end, ascending, maps...) + if err != nil { + return nil, err + } + return &mapIterator{ + kvPairs: pairs, + start: start, + end: end, + }, nil +} + +func buildMapPairs(start, end []byte, ascending bool, maps ...map[string][]byte) ([]kvPair, error) { + if start != nil && end != nil && bytes.Compare(start, end) > 0 { + return nil, nil + } + + total := 0 + for _, data := range maps { + total += len(data) + } + if total == 0 { + return nil, nil + } + + seen := make(map[string]struct{}, total) + pairs := make([]kvPair, 0, total) + for _, data := range maps { + if data == nil { + continue + } + for k, v := range data { + if _, dup := seen[k]; dup { + return nil, fmt.Errorf("duplicate key %q", k) + } + seen[k] = struct{}{} + + key := []byte(k) + if !keyInRange(key, start, end) { + continue + } + pairs = append(pairs, kvPair{ + key: utils.Clone(key), + value: utils.Clone(v), + }) + } + } + + sort.Slice(pairs, func(i, j int) bool { + cmp := bytes.Compare(pairs[i].key, pairs[j].key) + if ascending { + return cmp < 0 + } + return cmp > 0 + }) + return pairs, nil +} + +func keyInRange(key, start, end []byte) bool { + if start != nil && bytes.Compare(key, start) < 0 { + return false + } + if end != nil && bytes.Compare(key, end) >= 0 { + return false + } + return true +} + +func (m *mapIterator) Close() error { + m.kvPairs = nil + m.currentIndex = 0 + return nil +} + +func (m *mapIterator) Domain() ([]byte, []byte) { + return m.start, m.end +} + +func (m *mapIterator) Error() error { + return nil +} + +func (m *mapIterator) Key() []byte { + if !m.Valid() { + return nil + } + return m.kvPairs[m.currentIndex].key +} + +func (m *mapIterator) Next() { + if !m.Valid() { + return + } + m.currentIndex++ +} + +func (m *mapIterator) Valid() bool { + return m.currentIndex >= 0 && m.currentIndex < len(m.kvPairs) +} + +func (m *mapIterator) Value() []byte { + if !m.Valid() { + return nil + } + return m.kvPairs[m.currentIndex].value +} diff --git a/sei-db/common/iterators/map_iterator_test.go b/sei-db/common/iterators/map_iterator_test.go new file mode 100644 index 0000000000..52127cf6e3 --- /dev/null +++ b/sei-db/common/iterators/map_iterator_test.go @@ -0,0 +1,156 @@ +package iterators_test + +import ( + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/common/iterators" + "github.com/stretchr/testify/require" +) + +func TestNewMapIterator_Empty(t *testing.T) { + it, err := iterators.NewMapIterator(nil, nil, true) + require.NoError(t, err) + require.False(t, it.Valid()) + require.Nil(t, it.Key()) + require.Nil(t, it.Value()) + require.NoError(t, it.Error()) + start, end := it.Domain() + require.Nil(t, start) + require.Nil(t, end) + require.NoError(t, it.Close()) + require.False(t, it.Valid()) +} + +func TestNewMapIterator_Ascending(t *testing.T) { + data := map[string][]byte{ + "c": []byte("vc"), + "a": []byte("va"), + "b": []byte("vb"), + } + it, err := iterators.NewMapIterator(nil, nil, true, data) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("a"), []byte("va")}, + {[]byte("b"), []byte("vb")}, + {[]byte("c"), []byte("vc")}, + }, got) +} + +func TestNewMapIterator_Descending(t *testing.T) { + data := map[string][]byte{ + "c": []byte("vc"), + "a": []byte("va"), + "b": []byte("vb"), + } + it, err := iterators.NewMapIterator(nil, nil, false, data) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("c"), []byte("vc")}, + {[]byte("b"), []byte("vb")}, + {[]byte("a"), []byte("va")}, + }, got) +} + +func TestNewMapIterator_CombinesMaps(t *testing.T) { + left := map[string][]byte{"a": []byte("1"), "c": []byte("3")} + right := map[string][]byte{"b": []byte("2")} + it, err := iterators.NewMapIterator(nil, nil, true, left, right) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("a"), []byte("1")}, + {[]byte("b"), []byte("2")}, + {[]byte("c"), []byte("3")}, + }, got) +} + +func TestNewMapIterator_DuplicateKey(t *testing.T) { + left := map[string][]byte{"k": []byte("v0")} + right := map[string][]byte{"k": []byte("v1")} + _, err := iterators.NewMapIterator(nil, nil, true, left, right) + require.Error(t, err) + require.Contains(t, err.Error(), `duplicate key "k"`) +} + +func TestNewMapIterator_Domain(t *testing.T) { + data := map[string][]byte{ + "a": []byte("1"), + "b": []byte("2"), + "c": []byte("3"), + "d": []byte("4"), + } + start := []byte("b") + end := []byte("d") + it, err := iterators.NewMapIterator(start, end, true, data) + require.NoError(t, err) + defer it.Close() + + got := collect(t, it) + require.Equal(t, [][2][]byte{ + {[]byte("b"), []byte("2")}, + {[]byte("c"), []byte("3")}, + }, got) + + domainStart, domainEnd := it.Domain() + require.Equal(t, start, domainStart) + require.Equal(t, end, domainEnd) +} + +func TestNewMapIterator_StartInclusiveEndExclusive(t *testing.T) { + data := map[string][]byte{ + "k1": []byte("v1"), + "k2": []byte("v2"), + } + it, err := iterators.NewMapIterator([]byte("k1"), []byte("k1"), true, data) + require.NoError(t, err) + require.False(t, it.Valid()) + require.NoError(t, it.Close()) + + it, err = iterators.NewMapIterator([]byte("k1"), []byte("k2"), true, data) + require.NoError(t, err) + got := collect(t, it) + require.Equal(t, [][2][]byte{{[]byte("k1"), []byte("v1")}}, got) +} + +func TestNewMapIterator_InvalidRange(t *testing.T) { + data := map[string][]byte{"a": []byte("1")} + it, err := iterators.NewMapIterator([]byte("z"), []byte("a"), true, data) + require.NoError(t, err) + require.False(t, it.Valid()) + require.NoError(t, it.Close()) +} + +func TestNewMapIterator_IsolatedFromMapMutations(t *testing.T) { + data := map[string][]byte{"k": []byte("v")} + it, err := iterators.NewMapIterator(nil, nil, true, data) + require.NoError(t, err) + require.True(t, it.Valid()) + + data["k"] = []byte("mutated") + delete(data, "k") + + require.Equal(t, []byte("k"), it.Key()) + require.Equal(t, []byte("v"), it.Value()) + require.NoError(t, it.Close()) +} + +func TestMapIterator_NextAfterExhausted(t *testing.T) { + it, err := iterators.NewMapIterator(nil, nil, true, map[string][]byte{"a": []byte("1")}) + require.NoError(t, err) + require.True(t, it.Valid()) + it.Next() + require.False(t, it.Valid()) + require.Nil(t, it.Key()) + require.Nil(t, it.Value()) + it.Next() // no-op + require.False(t, it.Valid()) + require.NoError(t, it.Close()) +} diff --git a/sei-db/state_db/sc/composite/store_test.go b/sei-db/state_db/sc/composite/store_test.go index 0c98bb5bb2..511d6d40fe 100644 --- a/sei-db/state_db/sc/composite/store_test.go +++ b/sei-db/state_db/sc/composite/store_test.go @@ -38,17 +38,20 @@ func (f *failingEVMStore) GetBlockHeightModified(string, []byte) (int64, bool, e } func (f *failingEVMStore) Has(string, []byte) bool { return false } func (f *failingEVMStore) RawGlobalIterator() (dbm.Iterator, error) { return nil, 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 (f *failingEVMStore) Iterator(string, []byte, []byte, bool) (dbm.Iterator, error) { + return nil, 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 { var b [32]byte diff --git a/sei-db/state_db/sc/flatkv/api.go b/sei-db/state_db/sc/flatkv/api.go index 276cebffd8..fe972b04e8 100644 --- a/sei-db/state_db/sc/flatkv/api.go +++ b/sei-db/state_db/sc/flatkv/api.go @@ -64,6 +64,20 @@ type Store interface { // before modifying. Caller must Close when done. RawGlobalIterator() (dbm.Iterator, error) + // Create an iterator over a range of keys in a given store. + Iterator( + // The store to iterate over. + store string, + // The start key of the range to iterate over, inclusive. + // If nil, the iterator will start at the beginning of the store. + start []byte, + // The end key of the range to iterate over, exclusive. + // If nil, the iterator will iterate until the end of the store. + end []byte, + // Whether to iterate in ascending order. + ascending bool, + ) (dbm.Iterator, error) + // RootHash returns the 32-byte checksum of the working LtHash. // Note: This is the Blake3-256 digest of the underlying 2048-byte // raw LtHash vector. diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index cea795be21..4d0fb3f64b 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -251,6 +251,16 @@ func (s *CommitStore) RawGlobalIterator() (dbm.Iterator, error) { return merged, nil } +func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending bool) (dbm.Iterator, error) { + if store == "" { + return nil, fmt.Errorf("store name cannot be empty") + } + + // TODO + + return nil, nil +} + // Used to cause the raw global iterator to skip _meta/* keys. func skipMetaKeys(key, value []byte) ([]byte, []byte, bool, error) { return key, value, ktype.IsMetaKey(key), nil From 39132dfa5242eb8fb11ccc633d3769be40478ca1 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 13:37:16 -0500 Subject: [PATCH 12/16] incremental progress --- sei-db/common/iterators/map_iterator.go | 61 +++--- sei-db/common/iterators/merging_iterator.go | 58 +++++- .../common/iterators/merging_iterator_test.go | 22 +- sei-db/db_engine/pebbledb/iterator.go | 14 +- sei-db/db_engine/types/types.go | 2 + sei-db/state_db/sc/flatkv/store_iterator.go | 191 ++++++++++++++++++ sei-db/state_db/sc/flatkv/store_read.go | 58 ------ 7 files changed, 306 insertions(+), 100 deletions(-) create mode 100644 sei-db/state_db/sc/flatkv/store_iterator.go diff --git a/sei-db/common/iterators/map_iterator.go b/sei-db/common/iterators/map_iterator.go index 1a0238d6a4..55f6d6416b 100644 --- a/sei-db/common/iterators/map_iterator.go +++ b/sei-db/common/iterators/map_iterator.go @@ -9,18 +9,14 @@ import ( dbm "github.com/tendermint/tm-db" ) -var _ dbm.Iterator = (*mapIterator)(nil) +var _ dbm.Iterator = (*mapIterator[any])(nil) // Iterates over a map of key/value pairs. -type mapIterator struct { - // kvPairs holds keys in iteration order, filtered to the domain. - kvPairs []kvPair - // currentIndex is the index of the entry returned by Key/Value. +type mapIterator[T any] struct { + kvPairs []kvPair currentIndex int - // start is the inclusive lower bound of the domain. - start []byte - // end is the exclusive upper bound of the domain. - end []byte + start []byte + end []byte } type kvPair struct { @@ -28,28 +24,42 @@ type kvPair struct { value []byte } +// BytesSerializer is a pass-through serializer for map[string][]byte. +func BytesSerializer(v []byte) ([]byte, error) { + return v, nil +} + // NewMapIterator returns an iterator over the union of maps in lexicographic order // (or reverse lex order when ascending is false). start is inclusive; end is // exclusive. nil start or end means unbounded on that side. Duplicate keys across -// maps are rejected. -func NewMapIterator( +// maps are rejected. Values are serialized with serializer before iteration. +func NewMapIterator[T any]( start []byte, end []byte, ascending bool, - maps ...map[string][]byte, + serializer func(T) ([]byte, error), + maps ...map[string]T, ) (dbm.Iterator, error) { - pairs, err := buildMapPairs(start, end, ascending, maps...) + if serializer == nil { + return nil, fmt.Errorf("nil serializer") + } + pairs, err := buildMapPairs(start, end, ascending, serializer, maps...) if err != nil { return nil, err } - return &mapIterator{ + return &mapIterator[T]{ kvPairs: pairs, start: start, end: end, }, nil } -func buildMapPairs(start, end []byte, ascending bool, maps ...map[string][]byte) ([]kvPair, error) { +func buildMapPairs[T any]( + start, end []byte, + ascending bool, + serializer func(T) ([]byte, error), + maps ...map[string]T, +) ([]kvPair, error) { if start != nil && end != nil && bytes.Compare(start, end) > 0 { return nil, nil } @@ -78,9 +88,14 @@ func buildMapPairs(start, end []byte, ascending bool, maps ...map[string][]byte) if !keyInRange(key, start, end) { continue } + + serialized, err := serializer(v) + if err != nil { + return nil, fmt.Errorf("serialize key %q: %w", k, err) + } pairs = append(pairs, kvPair{ key: utils.Clone(key), - value: utils.Clone(v), + value: utils.Clone(serialized), }) } } @@ -105,39 +120,39 @@ func keyInRange(key, start, end []byte) bool { return true } -func (m *mapIterator) Close() error { +func (m *mapIterator[T]) Close() error { m.kvPairs = nil m.currentIndex = 0 return nil } -func (m *mapIterator) Domain() ([]byte, []byte) { +func (m *mapIterator[T]) Domain() ([]byte, []byte) { return m.start, m.end } -func (m *mapIterator) Error() error { +func (m *mapIterator[T]) Error() error { return nil } -func (m *mapIterator) Key() []byte { +func (m *mapIterator[T]) Key() []byte { if !m.Valid() { return nil } return m.kvPairs[m.currentIndex].key } -func (m *mapIterator) Next() { +func (m *mapIterator[T]) Next() { if !m.Valid() { return } m.currentIndex++ } -func (m *mapIterator) Valid() bool { +func (m *mapIterator[T]) Valid() bool { return m.currentIndex >= 0 && m.currentIndex < len(m.kvPairs) } -func (m *mapIterator) Value() []byte { +func (m *mapIterator[T]) Value() []byte { if !m.Valid() { return nil } diff --git a/sei-db/common/iterators/merging_iterator.go b/sei-db/common/iterators/merging_iterator.go index 37947f6a75..297df3f481 100644 --- a/sei-db/common/iterators/merging_iterator.go +++ b/sei-db/common/iterators/merging_iterator.go @@ -30,22 +30,27 @@ type mergingIterator struct { // the index of the iterator that should next emit a value nextIteratorIndex int + // ascending is true when children iterate in ascending key order. + ascending bool + // the error encountered by the iterator, if any err error } // NewMergingIterator combines iterators into a single iterator. // -// Each child must be in ascending lexicographic order without duplicate keys; -// otherwise behavior is undefined. Output is in global lex order. Duplicate -// keys across children are emitted once; the last child wins. +// Each child must iterate in the same direction as ascending (lex ascending +// when true, lex descending when false) without duplicate keys; otherwise +// behavior is undefined. Duplicate keys across children are emitted once; the +// last child wins. // // Intended for a small number of iterators (on the order of half a dozen). May // not be performant for combining large numbers of iterators. -func NewMergingIterator(iterators ...dbm.Iterator) (dbm.Iterator, error) { +func NewMergingIterator(ascending bool, iterators ...dbm.Iterator) (dbm.Iterator, error) { m := &mergingIterator{ iterators: make([]dbm.Iterator, len(iterators)), nextIteratorIndex: -1, + ascending: ascending, } copy(m.iterators, iterators) @@ -61,10 +66,18 @@ func NewMergingIterator(iterators ...dbm.Iterator) (dbm.Iterator, error) { } m.start, m.end = mergeDomain(m.iterators) - m.findMin() + m.findNext() return m, nil } +func (m *mergingIterator) findNext() { + if m.ascending { + m.findMin() + } else { + m.findMax() + } +} + // findMin sets nextIteratorIndex to the valid child with the smallest current // key, breaking ties toward the highest index. Child errors are checked here // and cached on the merge iterator via fail. @@ -99,6 +112,39 @@ func (m *mergingIterator) findMin() { } } +// findMax sets nextIteratorIndex to the valid child with the largest current +// key, breaking ties toward the highest index. +func (m *mergingIterator) findMax() { + if m.err != nil { + return + } + m.nextIteratorIndex = -1 + var largestKey []byte + for i, child := range m.iterators { + if child == nil { + continue + } + if err := child.Error(); err != nil { + m.fail(err) + return + } + if !child.Valid() { + continue + } + childKey := child.Key() + if m.nextIteratorIndex < 0 { + m.nextIteratorIndex = i + largestKey = bytes.Clone(childKey) + continue + } + cmp := bytes.Compare(childKey, largestKey) + if cmp > 0 || (cmp == 0 && i > m.nextIteratorIndex) { + m.nextIteratorIndex = i + largestKey = bytes.Clone(childKey) + } + } +} + // advanceChildrenAtKey advances every child positioned at key past that key. func (m *mergingIterator) advanceChildrenAtKey(key []byte) { for _, child := range m.iterators { @@ -223,7 +269,7 @@ func (m *mergingIterator) Next() { if m.err != nil { return } - m.findMin() + m.findNext() } func (m *mergingIterator) Valid() bool { diff --git a/sei-db/common/iterators/merging_iterator_test.go b/sei-db/common/iterators/merging_iterator_test.go index 2a1a73e364..47688849b7 100644 --- a/sei-db/common/iterators/merging_iterator_test.go +++ b/sei-db/common/iterators/merging_iterator_test.go @@ -48,12 +48,12 @@ func collect(t *testing.T, it dbm.Iterator) [][2][]byte { } func TestNewMergingIterator_NilIterator(t *testing.T) { - _, err := iterators.NewMergingIterator(memIter(t, []byte("a")), nil) + _, err := iterators.NewMergingIterator(true, memIter(t, []byte("a")), nil) require.Error(t, err) } func TestNewMergingIterator_Empty(t *testing.T) { - it, err := iterators.NewMergingIterator() + it, err := iterators.NewMergingIterator(true) require.NoError(t, err) require.False(t, it.Valid()) require.Nil(t, it.Key()) @@ -63,7 +63,7 @@ func TestNewMergingIterator_Empty(t *testing.T) { func TestMergingIterator_Single(t *testing.T) { child := memIter(t, []byte("b"), []byte("c")) - it, err := iterators.NewMergingIterator(child) + it, err := iterators.NewMergingIterator(true, child) require.NoError(t, err) defer it.Close() @@ -77,7 +77,7 @@ func TestMergingIterator_Single(t *testing.T) { func TestMergingIterator_LexOrder(t *testing.T) { a := memIter(t, []byte("a"), []byte("d")) b := memIter(t, []byte("b"), []byte("c"), []byte("e")) - it, err := iterators.NewMergingIterator(a, b) + it, err := iterators.NewMergingIterator(true, a, b) require.NoError(t, err) defer it.Close() @@ -93,7 +93,7 @@ func TestMergingIterator_LexOrder(t *testing.T) { func TestMergingIterator_DuplicateKeys(t *testing.T) { left := memIterKV(t, [2][]byte{[]byte("k"), []byte("v0")}, [2][]byte{[]byte("z"), []byte("z0")}) right := memIterKV(t, [2][]byte{[]byte("k"), []byte("v1")}, [2][]byte{[]byte("m"), []byte("m1")}) - it, err := iterators.NewMergingIterator(left, right) + it, err := iterators.NewMergingIterator(true, left, right) require.NoError(t, err) defer it.Close() @@ -109,7 +109,7 @@ func TestMergingIterator_RightmostWinsOnDuplicateKey(t *testing.T) { child0 := memIterKV(t, [2][]byte{[]byte("k"), []byte("v0")}, [2][]byte{[]byte("a"), []byte("a0")}) child1 := memIter(t, []byte("b")) child2 := memIterKV(t, [2][]byte{[]byte("k"), []byte("v2")}, [2][]byte{[]byte("c"), []byte("c0")}) - it, err := iterators.NewMergingIterator(child0, child1, child2) + it, err := iterators.NewMergingIterator(true, child0, child1, child2) require.NoError(t, err) defer it.Close() @@ -129,7 +129,7 @@ func TestMergingIterator_Domain(t *testing.T) { it2, err := db.Iterator([]byte("a"), nil) require.NoError(t, err) - merged, err := iterators.NewMergingIterator(it1, it2) + merged, err := iterators.NewMergingIterator(true, it1, it2) require.NoError(t, err) defer merged.Close() @@ -174,7 +174,7 @@ func (child *errOnSecondNextIterator) Close() error { func TestMergingIterator_CachesChildError(t *testing.T) { ok := memIter(t, []byte("a"), []byte("c")) bad := &errOnSecondNextIterator{Iterator: memIter(t, []byte("b"), []byte("d"))} - merged, err := iterators.NewMergingIterator(ok, bad) + merged, err := iterators.NewMergingIterator(true, ok, bad) require.NoError(t, err) require.True(t, merged.Valid()) @@ -244,7 +244,7 @@ func TestMergingIterator_DuplicateKeys_SharedKeyBuffer(t *testing.T) { keys: [][]byte{[]byte("k"), []byte("m")}, values: [][]byte{[]byte("v1"), []byte("m1")}, } - it, err := iterators.NewMergingIterator(left, right) + it, err := iterators.NewMergingIterator(true, left, right) require.NoError(t, err) defer it.Close() @@ -270,7 +270,7 @@ func TestMergingIterator_SharedKeyBuffer_DifferentInitialKeys(t *testing.T) { keys: [][]byte{[]byte("z")}, values: [][]byte{[]byte("z1")}, } - it, err := iterators.NewMergingIterator(left, right) + it, err := iterators.NewMergingIterator(true, left, right) require.NoError(t, err) defer it.Close() @@ -283,7 +283,7 @@ func TestMergingIterator_SharedKeyBuffer_DifferentInitialKeys(t *testing.T) { func TestMergingIterator_ClosesChildren(t *testing.T) { child := &closeTrackingIterator{Iterator: memIter(t, []byte("x"))} - it, err := iterators.NewMergingIterator(child) + it, err := iterators.NewMergingIterator(true, child) require.NoError(t, err) require.NoError(t, it.Close()) require.True(t, child.closed) diff --git a/sei-db/db_engine/pebbledb/iterator.go b/sei-db/db_engine/pebbledb/iterator.go index 5f2687896f..2be785639a 100644 --- a/sei-db/db_engine/pebbledb/iterator.go +++ b/sei-db/db_engine/pebbledb/iterator.go @@ -15,6 +15,7 @@ type pebbleIterator struct { it *pebble.Iterator lowerBound []byte upperBound []byte + reverse bool } func newPebbleIterator(it *pebble.Iterator, opts *types.IterOptions) *pebbleIterator { @@ -22,8 +23,13 @@ func newPebbleIterator(it *pebble.Iterator, opts *types.IterOptions) *pebbleIter if opts != nil { pi.lowerBound = opts.LowerBound pi.upperBound = opts.UpperBound + pi.reverse = opts.Reverse + } + if pi.reverse { + pi.it.Last() + } else { + pi.it.First() } - pi.it.First() return pi } @@ -39,7 +45,11 @@ func (pi *pebbleIterator) Next() { if !pi.Valid() { return } - pi.it.Next() + if pi.reverse { + pi.it.Prev() + } else { + pi.it.Next() + } } func (pi *pebbleIterator) Key() []byte { diff --git a/sei-db/db_engine/types/types.go b/sei-db/db_engine/types/types.go index 23cacb31f8..69abbdd976 100644 --- a/sei-db/db_engine/types/types.go +++ b/sei-db/db_engine/types/types.go @@ -16,9 +16,11 @@ type WriteOptions struct { // IterOptions controls iterator bounds. // - LowerBound is inclusive. // - UpperBound is exclusive. +// - Reverse iterates in descending key order when true (default false). type IterOptions struct { LowerBound []byte UpperBound []byte + Reverse bool } // BatchGetResult describes the result of a single key lookup within a BatchGet call. diff --git a/sei-db/state_db/sc/flatkv/store_iterator.go b/sei-db/state_db/sc/flatkv/store_iterator.go new file mode 100644 index 0000000000..affc6602e6 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/store_iterator.go @@ -0,0 +1,191 @@ +package flatkv + +import ( + "fmt" + + "github.com/sei-protocol/sei-chain/sei-db/common/iterators" + "github.com/sei-protocol/sei-chain/sei-db/common/keys" + seidbtypes "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/vtype" + dbm "github.com/tendermint/tm-db" +) + +// RawGlobalIterator returns an iterator over all committed keys across the +// data DBs (account, code, storage, legacy), merged in global lexicographic +// order. Within each DB, keys are in Pebble order. Per-DB _meta/* keys are +// skipped. Pending writes are not visible. metadataDB is not included. +func (s *CommitStore) RawGlobalIterator() (dbm.Iterator, error) { + dbs := s.dataDBs() + children := make([]dbm.Iterator, 0, len(dbs)) + for _, db := range dbs { + pebbleIter, err := db.NewIter(nil) + if err != nil { + closeIterators(children) + return nil, fmt.Errorf("open data DB iterator: %w", err) + } + mapped, err := iterators.NewMappingIterator(pebbleIter, skipMetaKeys) + if err != nil { + closeIterators(children) + return nil, err + } + children = append(children, mapped) + } + merged, err := iterators.NewMergingIterator(true, children...) + if err != nil { + closeIterators(children) + return nil, err + } + if err := merged.Error(); err != nil { + _ = merged.Close() + return nil, err + } + return merged, nil +} + +func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending bool) (dbm.Iterator, error) { + if store == "" { + return nil, fmt.Errorf("store name cannot be empty") + } + + if store == keys.EVMStoreKey { + return s.buildEvmIterator(start, end, ascending) + } else { + return s.buildLegacyIterator(store, start, end, ascending) + } +} + +func (s *CommitStore) buildEvmIterator( + start []byte, + end []byte, + ascending bool, +) (dbm.Iterator, error) { + + return nil, nil +} + +/* Data flow: buildLegacyIterator (non-EVM modules) + + ┌────────────────────────┐ ┌─────────────────┐ + │ legacyWrites (pending) │ │ legacyDB pebble │ + └────────────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ + ┌──────────────┐ ┌─────────────────┐ + │ map iterator │ │ pebble iterator │ + └──────────────┘ └─────────────────┘ + │ │ + └──────┐ ┌────────────────┘ + │ │ + ▼ ▼ + ┌────────────────┐ + │ merge iterator │ pending writes "win" + └────────────────┘ + │ + physical key + serialized LegacyData + includes deleted values + │ + ▼ + ┌──────────────────┐ + │ mapping iterator │ + └──────────────────┘ + │ + logical module key + raw value bytes + excludes deleted values + │ + ▼ +*/ + +// Build an iterator that can walk non-EVM data (which is called legacy data in the codebase) +func (s *CommitStore) buildLegacyIterator( + store string, + start []byte, + end []byte, + ascending bool, +) (dbm.Iterator, error) { + + modulePrefix := ktype.ModulePhysicalKey(store, nil) + lowerBound := modulePrefix + if start != nil { + lowerBound = ktype.ModulePhysicalKey(store, start) + } + var upperBound []byte + if end != nil { + upperBound = ktype.ModulePhysicalKey(store, end) + } else { + upperBound = ktype.PrefixEnd(modulePrefix) + } + + // Create an iterator that walks the pending writes. + serializer := func(v *vtype.LegacyData) ([]byte, error) { + if v == nil { + return nil, nil + } + return v.Serialize(), nil + } + pendingDataIterator, err := iterators.NewMapIterator(lowerBound, upperBound, ascending, serializer, s.legacyWrites) + if err != nil { + return nil, fmt.Errorf("failed to create pending data iterator: %w", err) + } + + // Create an iterator that walks the data in pebble. + pebbleIterator, err := s.legacyDB.NewIter(&seidbtypes.IterOptions{ + LowerBound: lowerBound, + UpperBound: upperBound, + Reverse: !ascending, + }) + if err != nil { + _ = pendingDataIterator.Close() + return nil, fmt.Errorf("failed to create pebble iterator: %w", err) + } + + // Pebble first, pending second: the rightmost child (pending) wins on duplicate keys. + mergingIterator, err := iterators.NewMergingIterator(ascending, pebbleIterator, pendingDataIterator) + if err != nil { + _ = pendingDataIterator.Close() + _ = pebbleIterator.Close() + return nil, fmt.Errorf("failed to create merging iterator: %w", err) + } + + // Translate data into the form expected by the caller and skip deleted keys. + remapper := func(key []byte, value []byte) ([]byte, []byte, bool, error) { + moduleName, logicalKey, err := ktype.StripModulePrefix(key) + if err != nil { + return nil, nil, false, err + } + if moduleName != store { + return nil, nil, false, fmt.Errorf( + "legacy iterator key %q has module %q, expected %q", + key, moduleName, store, + ) + } + ld, err := vtype.DeserializeLegacyData(value) + if err != nil { + return nil, nil, false, err + } + if ld.IsDelete() { + return nil, nil, true, nil + } + return logicalKey, ld.GetValue(), false, nil + } + translatedIterator, err := iterators.NewMappingIterator(mergingIterator, remapper) + if err != nil { + _ = mergingIterator.Close() + return nil, fmt.Errorf("failed to create translated iterator: %w", err) + } + + return translatedIterator, nil +} + +// Used to cause the raw global iterator to skip _meta/* keys. +func skipMetaKeys(key, value []byte) ([]byte, []byte, bool, error) { + return key, value, ktype.IsMetaKey(key), nil +} + +func closeIterators(iters []dbm.Iterator) { + for _, it := range iters { + if it != nil { + _ = it.Close() + } + } +} diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index 4d0fb3f64b..630955ad3f 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -4,10 +4,7 @@ import ( "encoding/binary" "fmt" - dbm "github.com/tendermint/tm-db" - errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" - "github.com/sei-protocol/sei-chain/sei-db/common/iterators" "github.com/sei-protocol/sei-chain/sei-db/common/keys" seidbtypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/ktype" @@ -218,58 +215,3 @@ func (s *CommitStore) getLegacyValue(moduleName string, key []byte) ([]byte, err } return ld.GetValue(), nil } - -// RawGlobalIterator returns an iterator over all committed keys across the -// data DBs (account, code, storage, legacy), merged in global lexicographic -// order. Within each DB, keys are in Pebble order. Per-DB _meta/* keys are -// skipped. Pending writes are not visible. metadataDB is not included. -func (s *CommitStore) RawGlobalIterator() (dbm.Iterator, error) { - dbs := s.dataDBs() - children := make([]dbm.Iterator, 0, len(dbs)) - for _, db := range dbs { - pebbleIter, err := db.NewIter(nil) - if err != nil { - closeIterators(children) - return nil, fmt.Errorf("open data DB iterator: %w", err) - } - mapped, err := iterators.NewMappingIterator(pebbleIter, skipMetaKeys) - if err != nil { - closeIterators(children) - return nil, err - } - children = append(children, mapped) - } - merged, err := iterators.NewMergingIterator(children...) - if err != nil { - closeIterators(children) - return nil, err - } - if err := merged.Error(); err != nil { - _ = merged.Close() - return nil, err - } - return merged, nil -} - -func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending bool) (dbm.Iterator, error) { - if store == "" { - return nil, fmt.Errorf("store name cannot be empty") - } - - // TODO - - return nil, nil -} - -// Used to cause the raw global iterator to skip _meta/* keys. -func skipMetaKeys(key, value []byte) ([]byte, []byte, bool, error) { - return key, value, ktype.IsMetaKey(key), nil -} - -func closeIterators(iters []dbm.Iterator) { - for _, it := range iters { - if it != nil { - _ = it.Close() - } - } -} From be408f11dd1806c29c896c8bfbee48d15b16f12c Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Thu, 28 May 2026 14:58:58 -0500 Subject: [PATCH 13/16] ascii diagram --- sei-db/state_db/sc/flatkv/store_iteration.go | 108 ++++++++++++------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/sei-db/state_db/sc/flatkv/store_iteration.go b/sei-db/state_db/sc/flatkv/store_iteration.go index 370a77ae7c..4142e41696 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration.go +++ b/sei-db/state_db/sc/flatkv/store_iteration.go @@ -114,45 +114,75 @@ func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending logical module key + raw value bytes │ │ excludes deleted values │ │ │ - └----------------------------------------------------------------------------------------┐ │ - │ │ - │ │ - ┌─────────────────────────┐ ┌──────────────────────────┐ │ │ - │ accountWrites (pending) │ │ accountDB (pebble) │ │ │ - └─────────────────────────┘ └──────────────────────────┘ │ │ - │ │ │ │ │ │ │ │ - ▼ ▼ ▼ ▼ ▼ ▼ │ │ -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ -│ map iterator │ │ map iterator │ │ map iterator │ │ pebble iterator │ │ pebble iterator │ │ pebble iterator │ │ │ -└──────────────┘ └──────────────┘ └──────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ - │ │ │ │ │ │ │ │ - │ ┌──────────────────────────────────────────────────────────┘ │ │ │ │ - │ │ │ │ │ │ │ │ - │ │ │ ┌──────────────────────────────────────────────────────┘ │ │ │ - │ │ │ │ │ │ │ │ - │ │ │ │ │ ┌──────────────────────────────────────────────────────┘ │ │ - │ │ │ │ │ │ │ │ - ▼ ▼ ▼ ▼ ▼ ▼ │ │ -┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ -│ merge iterator │ │ merge iterator │ │ merge iterator │ pending writes "win" │ │ -└────────────────┘ └────────────────┘ └────────────────┘ │ │ - | | | │ │ - | | | │ │ - physical key + full serialized account data, includes deletions │ │ - | | | │ │ - | | | │ │ - ▼ ▼ ▼ │ │ -┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │ │ -│ transform iterator │ │ transform iterator │ │ transform iterator │ │ │ -└────────────────────┘ └────────────────────┘ └────────────────────┘ │ │ - | | | │ │ - | | | │ │ - balance* nonce codehash │ │ - logical key logical key logical key │ │ - no deletions no deletions no deletions │ │ - | | | │ │ - | | | │ │ - ▼ ▼ ▼ ▼ ▼ + └------------------------------------------------------------------------------------------┐ │ + │ │ + │ │ + ┌─────────────────────────┐ ┌────────────────────┐ │ │ + │ legacyWrites (pending) │ │ legacyDB (pebble) │ │ │ + └─────────────────────────┘ └────────────────────┘ │ │ + │ │ │ │ + ▼ ▼ │ │ + ┌──────────────┐ ┌─────────────────┐ │ │ + │ map iterator │ │ pebble iterator │ │ │ + └──────────────┘ └─────────────────┘ │ │ + │ │ │ │ + └──────┐ ┌─────────────┘ │ │ + │ │ │ │ + ▼ ▼ │ │ + ┌────────────────┐ │ │ + │ merge iterator │ pending writes "win" │ │ + └────────────────┘ │ │ + │ │ │ + physical key + serialized legacy data │ │ + includes deleted values │ │ + │ │ │ + ▼ │ │ + ┌────────────────────┐ │ │ + │ transform iterator │ │ │ + └────────────────────┘ │ │ + │ │ │ + logical module key + raw value bytes │ │ │ + excludes deleted values │ │ + │ │ │ + └----------------------------------------------------------------------------------------┐ │ │ + │ │ │ + │ │ │ + ┌─────────────────────────┐ ┌──────────────────────────┐ │ │ │ + │ accountWrites (pending) │ │ accountDB (pebble) │ │ │ │ + └─────────────────────────┘ └──────────────────────────┘ │ │ │ + │ │ │ │ │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ ▼ │ │ │ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ +│ map iterator │ │ map iterator │ │ map iterator │ │ pebble iterator │ │ pebble iterator │ │ pebble iterator │ │ │ │ +└──────────────┘ └──────────────┘ └──────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ + │ │ │ │ │ │ │ │ │ + │ ┌──────────────────────────────────────────────────────────┘ │ │ │ │ │ + │ │ │ │ │ │ │ │ │ + │ │ │ ┌──────────────────────────────────────────────────────┘ │ │ │ │ + │ │ │ │ │ │ │ │ │ + │ │ │ │ │ ┌──────────────────────────────────────────────────────┘ │ │ │ + │ │ │ │ │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ ▼ │ │ │ +┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ │ +│ merge iterator │ │ merge iterator │ │ merge iterator │ pending writes "win" │ │ │ +└────────────────┘ └────────────────┘ └────────────────┘ │ │ │ + | | | │ │ │ + | | | │ │ │ + physical key + full serialized account data, includes deletions │ │ │ + | | | │ │ │ + | | | │ │ │ + ▼ ▼ ▼ │ │ │ +┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │ │ │ +│ transform iterator │ │ transform iterator │ │ transform iterator │ │ │ │ +└────────────────────┘ └────────────────────┘ └────────────────────┘ │ │ │ + | | | │ │ │ + | | | │ │ │ + balance* nonce codehash │ │ │ + logical key logical key logical key │ │ │ + no deletions no deletions no deletions │ │ │ + | | | │ │ │ + | | | │ │ │ + ▼ ▼ ▼ ▼ ▼ ▼ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ merge iterator │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ From 16880d03197109c11a0772af9a9e9ef90a1a075c Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Fri, 29 May 2026 09:07:01 -0500 Subject: [PATCH 14/16] implement iterators --- sei-db/common/iterators/map_iterator_test.go | 43 +- sei-db/db_engine/dbcache/unwrap.go | 14 + sei-db/db_engine/pebbledb/table_iters.go | 19 + sei-db/state_db/sc/flatkv/store_iteration.go | 600 +++++++++++++----- .../sc/flatkv/store_iteration_test.go | 577 +++++++++++++++++ 5 files changed, 1076 insertions(+), 177 deletions(-) create mode 100644 sei-db/db_engine/dbcache/unwrap.go create mode 100644 sei-db/db_engine/pebbledb/table_iters.go create mode 100644 sei-db/state_db/sc/flatkv/store_iteration_test.go diff --git a/sei-db/common/iterators/map_iterator_test.go b/sei-db/common/iterators/map_iterator_test.go index 52127cf6e3..095f3a75f8 100644 --- a/sei-db/common/iterators/map_iterator_test.go +++ b/sei-db/common/iterators/map_iterator_test.go @@ -5,10 +5,11 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/iterators" "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" ) func TestNewMapIterator_Empty(t *testing.T) { - it, err := iterators.NewMapIterator(nil, nil, true) + it, err := iterators.NewMapIterator(nil, nil, true, iterators.BytesSerializer) require.NoError(t, err) require.False(t, it.Valid()) require.Nil(t, it.Key()) @@ -27,11 +28,11 @@ func TestNewMapIterator_Ascending(t *testing.T) { "a": []byte("va"), "b": []byte("vb"), } - it, err := iterators.NewMapIterator(nil, nil, true, data) + it, err := iterators.NewMapIterator(nil, nil, true, iterators.BytesSerializer, data) require.NoError(t, err) defer it.Close() - got := collect(t, it) + got := collectMapIterPairs(t, it) require.Equal(t, [][2][]byte{ {[]byte("a"), []byte("va")}, {[]byte("b"), []byte("vb")}, @@ -45,11 +46,11 @@ func TestNewMapIterator_Descending(t *testing.T) { "a": []byte("va"), "b": []byte("vb"), } - it, err := iterators.NewMapIterator(nil, nil, false, data) + it, err := iterators.NewMapIterator(nil, nil, false, iterators.BytesSerializer, data) require.NoError(t, err) defer it.Close() - got := collect(t, it) + got := collectMapIterPairs(t, it) require.Equal(t, [][2][]byte{ {[]byte("c"), []byte("vc")}, {[]byte("b"), []byte("vb")}, @@ -60,11 +61,11 @@ func TestNewMapIterator_Descending(t *testing.T) { func TestNewMapIterator_CombinesMaps(t *testing.T) { left := map[string][]byte{"a": []byte("1"), "c": []byte("3")} right := map[string][]byte{"b": []byte("2")} - it, err := iterators.NewMapIterator(nil, nil, true, left, right) + it, err := iterators.NewMapIterator(nil, nil, true, iterators.BytesSerializer, left, right) require.NoError(t, err) defer it.Close() - got := collect(t, it) + got := collectMapIterPairs(t, it) require.Equal(t, [][2][]byte{ {[]byte("a"), []byte("1")}, {[]byte("b"), []byte("2")}, @@ -75,7 +76,7 @@ func TestNewMapIterator_CombinesMaps(t *testing.T) { func TestNewMapIterator_DuplicateKey(t *testing.T) { left := map[string][]byte{"k": []byte("v0")} right := map[string][]byte{"k": []byte("v1")} - _, err := iterators.NewMapIterator(nil, nil, true, left, right) + _, err := iterators.NewMapIterator(nil, nil, true, iterators.BytesSerializer, left, right) require.Error(t, err) require.Contains(t, err.Error(), `duplicate key "k"`) } @@ -89,11 +90,11 @@ func TestNewMapIterator_Domain(t *testing.T) { } start := []byte("b") end := []byte("d") - it, err := iterators.NewMapIterator(start, end, true, data) + it, err := iterators.NewMapIterator(start, end, true, iterators.BytesSerializer, data) require.NoError(t, err) defer it.Close() - got := collect(t, it) + got := collectMapIterPairs(t, it) require.Equal(t, [][2][]byte{ {[]byte("b"), []byte("2")}, {[]byte("c"), []byte("3")}, @@ -109,20 +110,20 @@ func TestNewMapIterator_StartInclusiveEndExclusive(t *testing.T) { "k1": []byte("v1"), "k2": []byte("v2"), } - it, err := iterators.NewMapIterator([]byte("k1"), []byte("k1"), true, data) + it, err := iterators.NewMapIterator([]byte("k1"), []byte("k1"), true, iterators.BytesSerializer, data) require.NoError(t, err) require.False(t, it.Valid()) require.NoError(t, it.Close()) - it, err = iterators.NewMapIterator([]byte("k1"), []byte("k2"), true, data) + it, err = iterators.NewMapIterator([]byte("k1"), []byte("k2"), true, iterators.BytesSerializer, data) require.NoError(t, err) - got := collect(t, it) + got := collectMapIterPairs(t, it) require.Equal(t, [][2][]byte{{[]byte("k1"), []byte("v1")}}, got) } func TestNewMapIterator_InvalidRange(t *testing.T) { data := map[string][]byte{"a": []byte("1")} - it, err := iterators.NewMapIterator([]byte("z"), []byte("a"), true, data) + it, err := iterators.NewMapIterator([]byte("z"), []byte("a"), true, iterators.BytesSerializer, data) require.NoError(t, err) require.False(t, it.Valid()) require.NoError(t, it.Close()) @@ -130,7 +131,7 @@ func TestNewMapIterator_InvalidRange(t *testing.T) { func TestNewMapIterator_IsolatedFromMapMutations(t *testing.T) { data := map[string][]byte{"k": []byte("v")} - it, err := iterators.NewMapIterator(nil, nil, true, data) + it, err := iterators.NewMapIterator(nil, nil, true, iterators.BytesSerializer, data) require.NoError(t, err) require.True(t, it.Valid()) @@ -143,7 +144,7 @@ func TestNewMapIterator_IsolatedFromMapMutations(t *testing.T) { } func TestMapIterator_NextAfterExhausted(t *testing.T) { - it, err := iterators.NewMapIterator(nil, nil, true, map[string][]byte{"a": []byte("1")}) + it, err := iterators.NewMapIterator(nil, nil, true, iterators.BytesSerializer, map[string][]byte{"a": []byte("1")}) require.NoError(t, err) require.True(t, it.Valid()) it.Next() @@ -154,3 +155,13 @@ func TestMapIterator_NextAfterExhausted(t *testing.T) { require.False(t, it.Valid()) require.NoError(t, it.Close()) } + +func collectMapIterPairs(t *testing.T, it dbm.Iterator) [][2][]byte { + t.Helper() + var got [][2][]byte + for ; it.Valid(); it.Next() { + got = append(got, [2][]byte{it.Key(), it.Value()}) + } + require.NoError(t, it.Error()) + return got +} diff --git a/sei-db/db_engine/dbcache/unwrap.go b/sei-db/db_engine/dbcache/unwrap.go new file mode 100644 index 0000000000..9d5296fd0a --- /dev/null +++ b/sei-db/db_engine/dbcache/unwrap.go @@ -0,0 +1,14 @@ +package dbcache + +import "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + +// Unwrap returns the innermost KeyValueDB, stripping cached wrappers. +func Unwrap(db types.KeyValueDB) types.KeyValueDB { + for { + c, ok := db.(*cachedKeyValueDB) + if !ok { + return db + } + db = c.db + } +} diff --git a/sei-db/db_engine/pebbledb/table_iters.go b/sei-db/db_engine/pebbledb/table_iters.go new file mode 100644 index 0000000000..1347f7c4e0 --- /dev/null +++ b/sei-db/db_engine/pebbledb/table_iters.go @@ -0,0 +1,19 @@ +package pebbledb + +import ( + "fmt" + + "github.com/sei-protocol/sei-chain/sei-db/db_engine/dbcache" + "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" +) + +// TableIters returns the number of open SSTable iterators for db. +// db may be wrapped in one or more cachedKeyValueDB layers. +func TableIters(db types.KeyValueDB) (int64, error) { + inner := dbcache.Unwrap(db) + p, ok := inner.(*pebbleDB) + if !ok { + return 0, fmt.Errorf("expected pebbleDB, got %T", inner) + } + return p.db.Metrics().TableIters, nil +} diff --git a/sei-db/state_db/sc/flatkv/store_iteration.go b/sei-db/state_db/sc/flatkv/store_iteration.go index 4142e41696..37eea6adeb 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration.go +++ b/sei-db/state_db/sc/flatkv/store_iteration.go @@ -1,6 +1,7 @@ package flatkv import ( + "encoding/binary" "fmt" "github.com/sei-protocol/sei-chain/sei-db/common/iterators" @@ -50,30 +51,206 @@ func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending if store == keys.EVMStoreKey { return s.buildEvmIterator(start, end, ascending) + } + lowerBound, upperBound := moduleIteratorBounds(store, start, end) + return s.buildLegacyDBLane(store, lowerBound, upperBound, ascending) +} + +/* Data flow: buildEvmIterator + +buildCodeLane ──────────────┐ +buildStorageLane ───────────┤ +buildLegacyDBLane (evm/) ───┼──► merge iterator ──► memiavl keys + values +buildAccountNonceLane ──────┤ +buildAccountCodehashLane ───┘ + +* balance not iterated — not stored in FlatKV yet +*/ + +func (s *CommitStore) buildEvmIterator( + start []byte, + end []byte, + ascending bool, +) (dbm.Iterator, error) { + lowerBound, upperBound := moduleIteratorBounds(keys.EVMStoreKey, start, end) + + lanes := make([]dbm.Iterator, 0, 5) + codeLane, err := s.buildCodeLane(lowerBound, upperBound, ascending) + if err != nil { + return nil, err + } + lanes = append(lanes, codeLane) + + storageLane, err := s.buildStorageLane(lowerBound, upperBound, ascending) + if err != nil { + closeIterators(lanes) + return nil, err + } + lanes = append(lanes, storageLane) + + legacyLane, err := s.buildLegacyDBLane(keys.EVMStoreKey, lowerBound, upperBound, ascending) + if err != nil { + closeIterators(lanes) + return nil, err + } + lanes = append(lanes, legacyLane) + + nonceLane, err := s.buildAccountNonceLane(lowerBound, upperBound, ascending) + if err != nil { + closeIterators(lanes) + return nil, err + } + lanes = append(lanes, nonceLane) + + codehashLane, err := s.buildAccountCodehashLane(lowerBound, upperBound, ascending) + if err != nil { + closeIterators(lanes) + return nil, err + } + lanes = append(lanes, codehashLane) + + // TODO: once we move account balances to FlatKV, we need to add a lane for them here. + + merged, err := iterators.NewMergingIterator(ascending, lanes...) + if err != nil { + closeIterators(lanes) + return nil, fmt.Errorf("failed to create EVM merge iterator: %w", err) + } + return merged, nil +} + +// moduleIteratorBounds translates caller logical [start, end) keys into physical +// bounds for iterating a module-prefixed keyspace in the data DBs. +func moduleIteratorBounds(store string, start, end []byte) (lowerBound, upperBound []byte) { + modulePrefix := ktype.ModulePhysicalKey(store, nil) + lowerBound = modulePrefix + if start != nil { + lowerBound = ktype.ModulePhysicalKey(store, start) + } + if end != nil { + upperBound = ktype.ModulePhysicalKey(store, end) } else { - return s.buildLegacyIterator(store, start, end, ascending) + upperBound = ktype.PrefixEnd(modulePrefix) } + return lowerBound, upperBound } -/* Data flow: buildLegacyIterator (non-EVM modules) +/* Data flow: buildLegacyDBLane - ┌──────────────────────┐ ┌─────────────────┐ - │ codeWrites (pending) │ │ codeDB (pebble) │ - └──────────────────────┘ └─────────────────┘ - │ │ - ▼ ▼ + ┌────────────────────────┐ ┌───────────────────┐ + │ legacyWrites (pending) │ │ legacyDB (pebble) │ + └────────────────────────┘ └───────────────────┘ + │ │ + ▼ ▼ + ┌──────────────┐ ┌─────────────────┐ + │ map iterator │ │ pebble iterator │ + └──────────────┘ └─────────────────┘ + │ │ + └──────┐ ┌────────────────┘ + │ │ + ▼ ▼ + ┌────────────────┐ + │ merge iterator │ pending writes "win" + └────────────────┘ + │ + physical key + serialized LegacyData + includes deleted values + │ + ▼ + ┌────────────────────┐ + │ transform iterator │ + └────────────────────┘ + │ + logical module key + raw value bytes + excludes deleted values + │ + ▼ +*/ + +func (s *CommitStore) buildLegacyDBLane( + store string, + lowerBound, upperBound []byte, + ascending bool, +) (dbm.Iterator, error) { + legacySerializer := func(v *vtype.LegacyData) ([]byte, error) { + if v == nil || v.IsDelete() { + return nil, nil + } + return v.Serialize(), nil + } + pendingDataIterator, err := iterators.NewMapIterator( + lowerBound, upperBound, ascending, legacySerializer, s.legacyWrites) + if err != nil { + return nil, fmt.Errorf("failed to create pending legacy iterator: %w", err) + } + + pebbleIterator, err := s.legacyDB.NewIter(&seidbtypes.IterOptions{ + LowerBound: lowerBound, + UpperBound: upperBound, + Reverse: !ascending, + }) + if err != nil { + _ = pendingDataIterator.Close() + return nil, fmt.Errorf("failed to create legacy pebble iterator: %w", err) + } + + mergingIterator, err := iterators.NewMergingIterator(ascending, pebbleIterator, pendingDataIterator) + if err != nil { + _ = pendingDataIterator.Close() + _ = pebbleIterator.Close() + return nil, fmt.Errorf("failed to create legacy merge iterator: %w", err) + } + + transform := func(key []byte, value []byte) ([]byte, []byte, bool, error) { + if len(value) == 0 { + return nil, nil, true, nil + } + moduleName, logicalKey, err := ktype.StripModulePrefix(key) + if err != nil { + return nil, nil, false, err + } + if moduleName != store { + return nil, nil, false, fmt.Errorf( + "legacy iterator key %q has module %q, expected %q", + key, moduleName, store, + ) + } + ld, err := vtype.DeserializeLegacyData(value) + if err != nil { + return nil, nil, false, err + } + if ld.IsDelete() { + return nil, nil, true, nil + } + return logicalKey, ld.GetValue(), false, nil + } + transformedIterator, err := iterators.NewTransformingIterator(mergingIterator, transform) + if err != nil { + _ = mergingIterator.Close() + return nil, fmt.Errorf("failed to create legacy transform iterator: %w", err) + } + return transformedIterator, nil +} + +/* Data flow: buildCodeLane + + ┌─────────────────────┐ ┌────────────────┐ + │ codeWrites (pending)│ │ codeDB (pebble)│ + └─────────────────────┘ └────────────────┘ + │ │ + ▼ ▼ ┌──────────────┐ ┌─────────────────┐ │ map iterator │ │ pebble iterator │ └──────────────┘ └─────────────────┘ - │ │ - └──────┐ ┌─────────────┘ + │ │ + └──────┐ ┌────────────┘ │ │ ▼ ▼ ┌────────────────┐ │ merge iterator │ pending writes "win" └────────────────┘ │ - physical key + serialized code data + physical key + serialized CodeData includes deleted values │ ▼ @@ -81,130 +258,72 @@ func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending │ transform iterator │ └────────────────────┘ │ - logical module key + raw value bytes + 0x07‖addr + bytecode excludes deleted values │ - └--------------------------------------------------------------------------------------------┐ - │ - │ - ┌─────────────────────────┐ ┌────────────────────┐ │ - │ storageWrites (pending) │ │ storageDB (pebble) │ │ - └─────────────────────────┘ └────────────────────┘ │ - │ │ │ - ▼ ▼ │ - ┌──────────────┐ ┌─────────────────┐ │ - │ map iterator │ │ pebble iterator │ │ - └──────────────┘ └─────────────────┘ │ - │ │ │ - └──────┐ ┌─────────────┘ │ - │ │ │ - ▼ ▼ │ - ┌────────────────┐ │ - │ merge iterator │ pending writes "win" │ - └────────────────┘ │ - │ │ - physical key + serialized storage data │ - includes deleted values │ - │ │ - ▼ │ - ┌────────────────────┐ │ - │ transform iterator │ │ - └────────────────────┘ │ - │ │ - logical module key + raw value bytes │ │ - excludes deleted values │ - │ │ - └------------------------------------------------------------------------------------------┐ │ - │ │ - │ │ - ┌─────────────────────────┐ ┌────────────────────┐ │ │ - │ legacyWrites (pending) │ │ legacyDB (pebble) │ │ │ - └─────────────────────────┘ └────────────────────┘ │ │ - │ │ │ │ - ▼ ▼ │ │ - ┌──────────────┐ ┌─────────────────┐ │ │ - │ map iterator │ │ pebble iterator │ │ │ - └──────────────┘ └─────────────────┘ │ │ - │ │ │ │ - └──────┐ ┌─────────────┘ │ │ - │ │ │ │ - ▼ ▼ │ │ - ┌────────────────┐ │ │ - │ merge iterator │ pending writes "win" │ │ - └────────────────┘ │ │ - │ │ │ - physical key + serialized legacy data │ │ - includes deleted values │ │ - │ │ │ - ▼ │ │ - ┌────────────────────┐ │ │ - │ transform iterator │ │ │ - └────────────────────┘ │ │ - │ │ │ - logical module key + raw value bytes │ │ │ - excludes deleted values │ │ - │ │ │ - └----------------------------------------------------------------------------------------┐ │ │ - │ │ │ - │ │ │ - ┌─────────────────────────┐ ┌──────────────────────────┐ │ │ │ - │ accountWrites (pending) │ │ accountDB (pebble) │ │ │ │ - └─────────────────────────┘ └──────────────────────────┘ │ │ │ - │ │ │ │ │ │ │ │ │ - ▼ ▼ ▼ ▼ ▼ ▼ │ │ │ -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ -│ map iterator │ │ map iterator │ │ map iterator │ │ pebble iterator │ │ pebble iterator │ │ pebble iterator │ │ │ │ -└──────────────┘ └──────────────┘ └──────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ - │ │ │ │ │ │ │ │ │ - │ ┌──────────────────────────────────────────────────────────┘ │ │ │ │ │ - │ │ │ │ │ │ │ │ │ - │ │ │ ┌──────────────────────────────────────────────────────┘ │ │ │ │ - │ │ │ │ │ │ │ │ │ - │ │ │ │ │ ┌──────────────────────────────────────────────────────┘ │ │ │ - │ │ │ │ │ │ │ │ │ - ▼ ▼ ▼ ▼ ▼ ▼ │ │ │ -┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ │ -│ merge iterator │ │ merge iterator │ │ merge iterator │ pending writes "win" │ │ │ -└────────────────┘ └────────────────┘ └────────────────┘ │ │ │ - | | | │ │ │ - | | | │ │ │ - physical key + full serialized account data, includes deletions │ │ │ - | | | │ │ │ - | | | │ │ │ - ▼ ▼ ▼ │ │ │ -┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │ │ │ -│ transform iterator │ │ transform iterator │ │ transform iterator │ │ │ │ -└────────────────────┘ └────────────────────┘ └────────────────────┘ │ │ │ - | | | │ │ │ - | | | │ │ │ - balance* nonce codehash │ │ │ - logical key logical key logical key │ │ │ - no deletions no deletions no deletions │ │ │ - | | | │ │ │ - | | | │ │ │ - ▼ ▼ ▼ ▼ ▼ ▼ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ merge iterator │ -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - │ - ▼ + ▼ */ -// Build an iterator that can walk evm data -func (s *CommitStore) buildEvmIterator( - start []byte, - end []byte, +func (s *CommitStore) buildCodeLane( + lowerBound, upperBound []byte, ascending bool, ) (dbm.Iterator, error) { + codeSerializer := func(v *vtype.CodeData) ([]byte, error) { + if v == nil { + return nil, nil + } + return v.Serialize(), nil + } + pendingDataIterator, err := iterators.NewMapIterator( + lowerBound, upperBound, ascending, codeSerializer, s.codeWrites) + if err != nil { + return nil, fmt.Errorf("failed to create pending code iterator: %w", err) + } - return nil, nil + pebbleIterator, err := s.codeDB.NewIter(&seidbtypes.IterOptions{ + LowerBound: lowerBound, + UpperBound: upperBound, + Reverse: !ascending, + }) + if err != nil { + _ = pendingDataIterator.Close() + return nil, fmt.Errorf("failed to create code pebble iterator: %w", err) + } + + mergingIterator, err := iterators.NewMergingIterator(ascending, pebbleIterator, pendingDataIterator) + if err != nil { + _ = pendingDataIterator.Close() + _ = pebbleIterator.Close() + return nil, fmt.Errorf("failed to create code merge iterator: %w", err) + } + + transform := func(key []byte, value []byte) ([]byte, []byte, bool, error) { + _, strippedKey, err := ktype.StripEVMPhysicalKey(key) + if err != nil { + return nil, nil, false, err + } + cd, err := vtype.DeserializeCodeData(value) + if err != nil { + return nil, nil, false, err + } + if cd.IsDelete() { + return nil, nil, true, nil + } + return keys.BuildEVMKey(keys.EVMKeyCode, strippedKey), cd.GetBytecode(), false, nil + } + transformedIterator, err := iterators.NewTransformingIterator(mergingIterator, transform) + if err != nil { + _ = mergingIterator.Close() + return nil, fmt.Errorf("failed to create code transform iterator: %w", err) + } + return transformedIterator, nil } -/* Data flow: buildLegacyIterator (non-EVM modules) +/* Data flow: buildStorageLane - ┌────────────────────────┐ ┌───────────────────┐ - │ legacyWrites (pending) │ │ legacyDB (pebble) │ - └────────────────────────┘ └───────────────────┘ + ┌─────────────────────────┐ ┌────────────────────┐ + │ storageWrites (pending) │ │ storageDB (pebble) │ + └─────────────────────────┘ └────────────────────┘ │ │ ▼ ▼ ┌──────────────┐ ┌─────────────────┐ @@ -218,7 +337,7 @@ func (s *CommitStore) buildEvmIterator( │ merge iterator │ pending writes "win" └────────────────┘ │ - physical key + serialized LegacyData + physical key + serialized StorageData includes deleted values │ ▼ @@ -226,90 +345,249 @@ func (s *CommitStore) buildEvmIterator( │ transform iterator │ └────────────────────┘ │ - logical module key + raw value bytes + 0x03‖addr‖slot + 32-byte value excludes deleted values │ ▼ */ -// Build an iterator that can walk non-EVM data (which is called legacy data in the codebase) -func (s *CommitStore) buildLegacyIterator( - store string, - start []byte, - end []byte, +func (s *CommitStore) buildStorageLane( + lowerBound, upperBound []byte, ascending bool, ) (dbm.Iterator, error) { + storageSerializer := func(v *vtype.StorageData) ([]byte, error) { + if v == nil { + return nil, nil + } + return v.Serialize(), nil + } + pendingDataIterator, err := iterators.NewMapIterator( + lowerBound, upperBound, ascending, storageSerializer, s.storageWrites) + if err != nil { + return nil, fmt.Errorf("failed to create pending storage iterator: %w", err) + } - modulePrefix := ktype.ModulePhysicalKey(store, nil) - lowerBound := modulePrefix - if start != nil { - lowerBound = ktype.ModulePhysicalKey(store, start) + pebbleIterator, err := s.storageDB.NewIter(&seidbtypes.IterOptions{ + LowerBound: lowerBound, + UpperBound: upperBound, + Reverse: !ascending, + }) + if err != nil { + _ = pendingDataIterator.Close() + return nil, fmt.Errorf("failed to create storage pebble iterator: %w", err) } - var upperBound []byte - if end != nil { - upperBound = ktype.ModulePhysicalKey(store, end) - } else { - upperBound = ktype.PrefixEnd(modulePrefix) + + mergingIterator, err := iterators.NewMergingIterator(ascending, pebbleIterator, pendingDataIterator) + if err != nil { + _ = pendingDataIterator.Close() + _ = pebbleIterator.Close() + return nil, fmt.Errorf("failed to create storage merge iterator: %w", err) } - // Create an iterator that walks the pending writes. - serializer := func(v *vtype.LegacyData) ([]byte, error) { + transform := func(key []byte, value []byte) ([]byte, []byte, bool, error) { + _, strippedKey, err := ktype.StripEVMPhysicalKey(key) + if err != nil { + return nil, nil, false, err + } + sd, err := vtype.DeserializeStorageData(value) + if err != nil { + return nil, nil, false, err + } + if sd.IsDelete() { + return nil, nil, true, nil + } + return keys.BuildEVMKey(keys.EVMKeyStorage, strippedKey), sd.GetValue()[:], false, nil + } + transformedIterator, err := iterators.NewTransformingIterator(mergingIterator, transform) + if err != nil { + _ = mergingIterator.Close() + return nil, fmt.Errorf("failed to create storage transform iterator: %w", err) + } + return transformedIterator, nil +} + +/* Data flow: buildAccountNonceLane + + Same accountWrites + accountDB as buildAccountCodehashLane (one pending map, one DB). + + ┌─────────────────────────┐ ┌────────────────────┐ + │ accountWrites (pending) │ │ accountDB (pebble) │ + └─────────────────────────┘ └────────────────────┘ + │ │ + ▼ ▼ + ┌──────────────┐ ┌─────────────────┐ + │ map iterator │ │ pebble iterator │ + └──────────────┘ └─────────────────┘ + │ │ + └──────┐ ┌────────────────┘ + │ │ + ▼ ▼ + ┌────────────────┐ + │ merge iterator │ pending writes "win" + └────────────────┘ + │ + physical key + serialized AccountData + includes deleted values + │ + ▼ + ┌────────────────────┐ + │ transform iterator │ + └────────────────────┘ + │ + 0x0a‖addr + 8-byte nonce + excludes deleted values + │ + ▼ +*/ + +func (s *CommitStore) buildAccountNonceLane( + lowerBound, upperBound []byte, + ascending bool, +) (dbm.Iterator, error) { + accountSerializer := func(v *vtype.AccountData) ([]byte, error) { if v == nil { return nil, nil } return v.Serialize(), nil } - pendingDataIterator, err := iterators.NewMapIterator(lowerBound, upperBound, ascending, serializer, s.legacyWrites) + pendingDataIterator, err := iterators.NewMapIterator( + lowerBound, upperBound, ascending, accountSerializer, s.accountWrites) if err != nil { - return nil, fmt.Errorf("failed to create pending data iterator: %w", err) + return nil, fmt.Errorf("failed to create pending account iterator: %w", err) } - // Create an iterator that walks the data in pebble. - pebbleIterator, err := s.legacyDB.NewIter(&seidbtypes.IterOptions{ + pebbleIterator, err := s.accountDB.NewIter(&seidbtypes.IterOptions{ LowerBound: lowerBound, UpperBound: upperBound, Reverse: !ascending, }) if err != nil { _ = pendingDataIterator.Close() - return nil, fmt.Errorf("failed to create pebble iterator: %w", err) + return nil, fmt.Errorf("failed to create account pebble iterator: %w", err) } - // Pebble first, pending second: the rightmost child (pending) wins on duplicate keys. mergingIterator, err := iterators.NewMergingIterator(ascending, pebbleIterator, pendingDataIterator) if err != nil { _ = pendingDataIterator.Close() _ = pebbleIterator.Close() - return nil, fmt.Errorf("failed to create merging iterator: %w", err) + return nil, fmt.Errorf("failed to create account merge iterator: %w", err) } - // Transform data into the form expected by the caller and skip deleted keys. transform := func(key []byte, value []byte) ([]byte, []byte, bool, error) { - moduleName, logicalKey, err := ktype.StripModulePrefix(key) + _, addrBytes, err := ktype.StripEVMPhysicalKey(key) if err != nil { return nil, nil, false, err } - if moduleName != store { - return nil, nil, false, fmt.Errorf( - "legacy iterator key %q has module %q, expected %q", - key, moduleName, store, - ) - } - ld, err := vtype.DeserializeLegacyData(value) + ad, err := vtype.DeserializeAccountData(value) if err != nil { return nil, nil, false, err } - if ld.IsDelete() { + if ad.IsDelete() { return nil, nil, true, nil } - return logicalKey, ld.GetValue(), false, nil + nonceBytes := make([]byte, vtype.NonceLen) + binary.BigEndian.PutUint64(nonceBytes, ad.GetNonce()) + return keys.BuildEVMKey(keys.EVMKeyNonce, addrBytes), nonceBytes, false, nil } transformedIterator, err := iterators.NewTransformingIterator(mergingIterator, transform) if err != nil { _ = mergingIterator.Close() - return nil, fmt.Errorf("failed to create transformed iterator: %w", err) + return nil, fmt.Errorf("failed to create account nonce transform iterator: %w", err) + } + return transformedIterator, nil +} + +/* Data flow: buildAccountCodehashLane + + Same accountWrites + accountDB as buildAccountNonceLane (one pending map, one DB). + + ┌─────────────────────────┐ ┌────────────────────┐ + │ accountWrites (pending) │ │ accountDB (pebble) │ + └─────────────────────────┘ └────────────────────┘ + │ │ + ▼ ▼ + ┌──────────────┐ ┌─────────────────┐ + │ map iterator │ │ pebble iterator │ + └──────────────┘ └─────────────────┘ + │ │ + └──────┐ ┌────────────────┘ + │ │ + ▼ ▼ + ┌────────────────┐ + │ merge iterator │ pending writes "win" + └────────────────┘ + │ + physical key + serialized AccountData + includes deleted values + │ + ▼ + ┌────────────────────┐ + │ transform iterator │ + └────────────────────┘ + │ + 0x08‖addr + code hash bytes + excludes deleted values and zero hash + │ + ▼ +*/ + +func (s *CommitStore) buildAccountCodehashLane( + lowerBound, upperBound []byte, + ascending bool, +) (dbm.Iterator, error) { + accountSerializer := func(v *vtype.AccountData) ([]byte, error) { + if v == nil { + return nil, nil + } + return v.Serialize(), nil + } + pendingDataIterator, err := iterators.NewMapIterator( + lowerBound, upperBound, ascending, accountSerializer, s.accountWrites) + if err != nil { + return nil, fmt.Errorf("failed to create pending account iterator: %w", err) } + pebbleIterator, err := s.accountDB.NewIter(&seidbtypes.IterOptions{ + LowerBound: lowerBound, + UpperBound: upperBound, + Reverse: !ascending, + }) + if err != nil { + _ = pendingDataIterator.Close() + return nil, fmt.Errorf("failed to create account pebble iterator: %w", err) + } + + mergingIterator, err := iterators.NewMergingIterator(ascending, pebbleIterator, pendingDataIterator) + if err != nil { + _ = pendingDataIterator.Close() + _ = pebbleIterator.Close() + return nil, fmt.Errorf("failed to create account merge iterator: %w", err) + } + + transform := func(key []byte, value []byte) ([]byte, []byte, bool, error) { + _, addrBytes, err := ktype.StripEVMPhysicalKey(key) + if err != nil { + return nil, nil, false, err + } + ad, err := vtype.DeserializeAccountData(value) + if err != nil { + return nil, nil, false, err + } + if ad.IsDelete() { + return nil, nil, true, nil + } + codeHash := ad.GetCodeHash() + var zeroCodeHash vtype.CodeHash + if *codeHash == zeroCodeHash { + return nil, nil, true, nil + } + return keys.BuildEVMKey(keys.EVMKeyCodeHash, addrBytes), codeHash[:], false, nil + } + transformedIterator, err := iterators.NewTransformingIterator(mergingIterator, transform) + if err != nil { + _ = mergingIterator.Close() + return nil, fmt.Errorf("failed to create account codehash transform iterator: %w", err) + } return transformedIterator, nil } diff --git a/sei-db/state_db/sc/flatkv/store_iteration_test.go b/sei-db/state_db/sc/flatkv/store_iteration_test.go new file mode 100644 index 0000000000..436919921b --- /dev/null +++ b/sei-db/state_db/sc/flatkv/store_iteration_test.go @@ -0,0 +1,577 @@ +package flatkv + +import ( + "bytes" + "flag" + "hash/fnv" + "math/rand" + "os" + "sort" + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/common/keys" + "github.com/sei-protocol/sei-chain/sei-db/db_engine/pebbledb" + "github.com/sei-protocol/sei-chain/sei-db/proto" + "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/vtype" + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" +) + +// evmIterSeed overrides the deterministic seed for EVM iterator tests when non-zero. +var evmIterSeed = flag.Int64("evm-iter-seed", 0, "seed for EVM iterator fixture generation (0 = derive from test name)") + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +type evmIteratorEntry struct { + Key []byte + Value []byte +} + +type evmIteratorDisposition int + +const ( + dispositionPebbleOnly evmIteratorDisposition = iota + dispositionPendingOnly + dispositionOverlap + dispositionTombstone +) + +type evmIteratorFixture struct { + Seed int64 + Store *CommitStore + Sorted []evmIteratorEntry + OverlapSamples []evmIteratorEntry + TombstonedKeys [][]byte +} + +func TestEvmIterator(t *testing.T) { + seed := iteratorTestSeed(t, "TestEvmIterator") + fixture := buildEvmIteratorFixture(t, seed) + + baseline, err := sumFlatKVTableIters(fixture.Store) + require.NoError(t, err) + t.Cleanup(func() { + current, sumErr := sumFlatKVTableIters(fixture.Store) + require.NoError(t, sumErr) + require.Equal(t, baseline, current, "leaked pebble table iterators") + require.NoError(t, fixture.Store.Close()) + }) + + storageStart := keys.StateKeyPrefix() + storageEnd := ktype.PrefixEnd(storageStart) + codeStart := []byte{0x07} + codeEnd := ktype.PrefixEnd(codeStart) + legacyStart := []byte{0x09} + legacyEnd := ktype.PrefixEnd(legacyStart) + + cases := []struct { + name string + start []byte + end []byte + ascending bool + }{ + {name: "full module ascending", ascending: true}, + {name: "full module descending", ascending: false}, + {name: "storage prefix range", start: storageStart, end: storageEnd, ascending: true}, + {name: "legacy sub-range", start: legacyStart, end: legacyEnd, ascending: true}, + {name: "code prefix range", start: codeStart, end: codeEnd, ascending: true}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + want := subrange(fixture.Sorted, tc.start, tc.end, tc.ascending) + iter, err := fixture.Store.Iterator(keys.EVMStoreKey, tc.start, tc.end, tc.ascending) + require.NoError(t, err) + got := collectIterEntries(t, iter) + require.NoError(t, iter.Close()) + require.Equal(t, want, got) + }) + } + + t.Run("overlap keys use pending values", func(t *testing.T) { + require.NotEmpty(t, fixture.OverlapSamples) + + iter, err := fixture.Store.Iterator(keys.EVMStoreKey, nil, nil, true) + require.NoError(t, err) + defer func() { require.NoError(t, iter.Close()) }() + + got := collectIterEntries(t, iter) + for _, sample := range fixture.OverlapSamples { + entry, ok := findEntry(got, sample.Key) + require.True(t, ok, "overlap key %x missing from iterator (seed=%d)", sample.Key, fixture.Seed) + require.Equal(t, sample.Value, entry.Value, "overlap key %x (seed=%d)", sample.Key, fixture.Seed) + } + }) + + t.Run("tombstones absent", func(t *testing.T) { + require.NotEmpty(t, fixture.TombstonedKeys) + + iter, err := fixture.Store.Iterator(keys.EVMStoreKey, nil, nil, true) + require.NoError(t, err) + got := collectIterEntries(t, iter) + require.NoError(t, iter.Close()) + + for _, key := range fixture.TombstonedKeys { + for _, entry := range got { + require.False(t, bytes.Equal(entry.Key, key), + "tombstoned key %x should not appear (seed=%d)", key, fixture.Seed) + } + for _, entry := range fixture.Sorted { + require.False(t, bytes.Equal(entry.Key, key), + "tombstoned key %x should not be in expected sorted output (seed=%d)", key, fixture.Seed) + } + } + }) +} + +func TestLegacyIteratorNonEVM(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ + bankNamedCS(&proto.KVPair{Key: []byte("alpha"), Value: []byte("A")}), + })) + commitAndCheck(t, s) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ + bankNamedCS( + &proto.KVPair{Key: []byte("beta"), Value: []byte("B")}, + &proto.KVPair{Key: []byte("alpha"), Value: []byte("A2")}, + ), + })) + + iter, err := s.Iterator("bank", nil, nil, true) + require.NoError(t, err) + got := collectIterEntries(t, iter) + require.NoError(t, iter.Close()) + + require.Equal(t, []evmIteratorEntry{ + {Key: []byte("alpha"), Value: []byte("A2")}, + {Key: []byte("beta"), Value: []byte("B")}, + }, got) +} + +func iteratorTestSeed(t *testing.T, label string) int64 { + t.Helper() + if *evmIterSeed != 0 { + t.Logf("evm iterator seed=%d (from -evm-iter-seed)", *evmIterSeed) + return *evmIterSeed + } + h := fnv.New64a() + _, _ = h.Write([]byte(t.Name())) + if label != "" { + _, _ = h.Write([]byte{0}) + _, _ = h.Write([]byte(label)) + } + seed := int64(h.Sum64()) //nolint:gosec // deterministic test data only + t.Logf("evm iterator seed=%d (reproduce with -evm-iter-seed=%d)", seed, seed) + return seed +} + +func buildEvmIteratorFixture(t *testing.T, seed int64) *evmIteratorFixture { + t.Helper() + t.Logf("building EVM iterator fixture with seed=%d", seed) + + s := setupTestStore(t) + rng := rand.New(rand.NewSource(seed)) //nolint:gosec // deterministic test data only + + latest := make(map[string]evmIteratorEntry) + var batch1, batch2 []*proto.KVPair + var overlapSamples []evmIteratorEntry + var tombstonedKeys [][]byte + usedAddrs := make(map[byte]struct{}, 32) + + gen := &evmIteratorGenerator{ + rng: rng, + latest: latest, + batch1: &batch1, + batch2: &batch2, + overlaps: &overlapSamples, + tombstones: &tombstonedKeys, + usedAddrs: usedAddrs, + } + + for _, disp := range []evmIteratorDisposition{ + dispositionPebbleOnly, + dispositionPendingOnly, + dispositionOverlap, + dispositionTombstone, + } { + gen.addStorage(disp) + gen.addCode(disp) + gen.addLegacy(disp) + gen.addAccount(disp) + } + + // Nonce-only account (no codehash key in iterator output). + gen.addNonceOnlyAccount() + + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS(batch1...)})) + commitAndCheck(t, s) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS(batch2...)})) + + return &evmIteratorFixture{ + Seed: seed, + Store: s, + Sorted: sortedEvmEntries(latest), + OverlapSamples: overlapSamples, + TombstonedKeys: tombstonedKeys, + } +} + +type evmIteratorGenerator struct { + rng *rand.Rand + latest map[string]evmIteratorEntry + batch1 *[]*proto.KVPair + batch2 *[]*proto.KVPair + overlaps *[]evmIteratorEntry + tombstones *[][]byte + usedAddrs map[byte]struct{} +} + +func (g *evmIteratorGenerator) uniqueAddr() ktype.Address { + for attempts := 0; attempts < 512; attempts++ { + b := byte(g.rng.Intn(256)) + if _, used := g.usedAddrs[b]; used { + continue + } + g.usedAddrs[b] = struct{}{} + return addrN(b) + } + panic("failed to allocate unique test address") +} + +func (g *evmIteratorGenerator) uniqueSlot() ktype.Slot { + var s ktype.Slot + g.rng.Read(s[:]) + if s == (ktype.Slot{}) { + s[31] = 1 + } + return s +} + +func (g *evmIteratorGenerator) storageVal() []byte { + return padLeft32(g.rngByte()) +} + +func (g *evmIteratorGenerator) codeVal() []byte { + n := 1 + g.rng.Intn(16) + out := make([]byte, n) + g.rng.Read(out) + return out +} + +func (g *evmIteratorGenerator) legacyVal() []byte { + n := 1 + g.rng.Intn(32) + out := make([]byte, n) + g.rng.Read(out) + return out +} + +func (g *evmIteratorGenerator) rngByte() byte { + return byte(g.rng.Intn(256)) //nolint:gosec +} + +func (g *evmIteratorGenerator) rngNonce() uint64 { + return g.rng.Uint64() +} + +func (g *evmIteratorGenerator) rngCodeHash() vtype.CodeHash { + var h vtype.CodeHash + g.rng.Read(h[:]) + if h == (vtype.CodeHash{}) { + h[0] = 1 + } + return h +} + +func (g *evmIteratorGenerator) recordOverlap(key, value []byte) { + *g.overlaps = append(*g.overlaps, evmIteratorEntry{ + Key: bytes.Clone(key), + Value: bytes.Clone(value), + }) +} + +func (g *evmIteratorGenerator) recordTombstone(key []byte) { + *g.tombstones = append(*g.tombstones, bytes.Clone(key)) +} + +func (g *evmIteratorGenerator) addStorage(disp evmIteratorDisposition) { + addr := g.uniqueAddr() + slot := g.uniqueSlot() + v1 := g.storageVal() + v2 := g.storageVal() + for bytes.Equal(v1, v2) { + v2 = g.storageVal() + } + + key := keys.BuildEVMKey(keys.EVMKeyStorage, ktype.StorageKey(addr, slot)) + switch disp { + case dispositionPebbleOnly: + *g.batch1 = append(*g.batch1, storagePair(addr, slot, v1)) + recordStorageLatest(g.latest, addr, slot, v1) + case dispositionPendingOnly: + *g.batch2 = append(*g.batch2, storagePair(addr, slot, v2)) + recordStorageLatest(g.latest, addr, slot, v2) + case dispositionOverlap: + *g.batch1 = append(*g.batch1, storagePair(addr, slot, v1)) + *g.batch2 = append(*g.batch2, storagePair(addr, slot, v2)) + recordStorageLatest(g.latest, addr, slot, v2) + g.recordOverlap(key, padLeft32(v2...)) + case dispositionTombstone: + *g.batch1 = append(*g.batch1, storagePair(addr, slot, v1)) + *g.batch2 = append(*g.batch2, storageDeletePair(addr, slot)) + removeStorageLatest(g.latest, addr, slot) + g.recordTombstone(key) + } +} + +func (g *evmIteratorGenerator) addCode(disp evmIteratorDisposition) { + addr := g.uniqueAddr() + v1 := g.codeVal() + v2 := g.codeVal() + for bytes.Equal(v1, v2) { + v2 = g.codeVal() + } + + key := keys.BuildEVMKey(keys.EVMKeyCode, addr[:]) + switch disp { + case dispositionPebbleOnly: + *g.batch1 = append(*g.batch1, codePair(addr, v1)) + recordCodeLatest(g.latest, addr, v1) + case dispositionPendingOnly: + *g.batch2 = append(*g.batch2, codePair(addr, v2)) + recordCodeLatest(g.latest, addr, v2) + case dispositionOverlap: + *g.batch1 = append(*g.batch1, codePair(addr, v1)) + *g.batch2 = append(*g.batch2, codePair(addr, v2)) + recordCodeLatest(g.latest, addr, v2) + g.recordOverlap(key, bytes.Clone(v2)) + case dispositionTombstone: + *g.batch1 = append(*g.batch1, codePair(addr, v1)) + *g.batch2 = append(*g.batch2, codeDeletePair(addr)) + removeCodeLatest(g.latest, addr) + g.recordTombstone(key) + } +} + +func (g *evmIteratorGenerator) addLegacy(disp evmIteratorDisposition) { + addr := g.uniqueAddr() + v1 := g.legacyVal() + v2 := g.legacyVal() + for bytes.Equal(v1, v2) { + v2 = g.legacyVal() + } + + key := append([]byte{0x09}, addr[:]...) + switch disp { + case dispositionPebbleOnly: + *g.batch1 = append(*g.batch1, legacyPair(addr, v1)) + recordLegacyLatest(g.latest, addr, v1) + case dispositionPendingOnly: + *g.batch2 = append(*g.batch2, legacyPair(addr, v2)) + recordLegacyLatest(g.latest, addr, v2) + case dispositionOverlap: + *g.batch1 = append(*g.batch1, legacyPair(addr, v1)) + *g.batch2 = append(*g.batch2, legacyPair(addr, v2)) + recordLegacyLatest(g.latest, addr, v2) + g.recordOverlap(key, bytes.Clone(v2)) + case dispositionTombstone: + *g.batch1 = append(*g.batch1, legacyPair(addr, v1)) + *g.batch2 = append(*g.batch2, legacyDeletePair(addr)) + removeLegacyLatest(g.latest, addr) + g.recordTombstone(key) + } +} + +func (g *evmIteratorGenerator) addAccount(disp evmIteratorDisposition) { + addr := g.uniqueAddr() + n1 := g.rngNonce() + n2 := g.rngNonce() + for n1 == n2 { + n2 = g.rngNonce() + } + ch1 := g.rngCodeHash() + ch2 := g.rngCodeHash() + for ch1 == ch2 { + ch2 = g.rngCodeHash() + } + + nonceKey := keys.BuildEVMKey(keys.EVMKeyNonce, addr[:]) + codeHashKey := keys.BuildEVMKey(keys.EVMKeyCodeHash, addr[:]) + + switch disp { + case dispositionPebbleOnly: + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) + recordNonceLatest(g.latest, addr, n1) + recordCodeHashLatest(g.latest, addr, ch1) + case dispositionPendingOnly: + *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2)) + recordNonceLatest(g.latest, addr, n2) + recordCodeHashLatest(g.latest, addr, ch2) + case dispositionOverlap: + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) + *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2)) + recordNonceLatest(g.latest, addr, n2) + recordCodeHashLatest(g.latest, addr, ch2) + g.recordOverlap(nonceKey, nonceBytes(n2)) + g.recordOverlap(codeHashKey, ch2[:]) + case dispositionTombstone: + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) + *g.batch2 = append(*g.batch2, nonceDeletePair(addr), codeHashDeletePair(addr)) + removeAccountLatest(g.latest, addr) + g.recordTombstone(nonceKey) + g.recordTombstone(codeHashKey) + } +} + +func (g *evmIteratorGenerator) addNonceOnlyAccount() { + addr := g.uniqueAddr() + n := g.rngNonce() + *g.batch1 = append(*g.batch1, noncePair(addr, n)) + recordNonceLatest(g.latest, addr, n) +} + +func bankNamedCS(pairs ...*proto.KVPair) *proto.NamedChangeSet { + return &proto.NamedChangeSet{ + Name: "bank", + Changeset: proto.ChangeSet{Pairs: pairs}, + } +} + +func legacyPair(addr ktype.Address, val []byte) *proto.KVPair { + return &proto.KVPair{ + Key: append([]byte{0x09}, addr[:]...), + Value: val, + } +} + +func legacyDeletePair(addr ktype.Address) *proto.KVPair { + return &proto.KVPair{ + Key: append([]byte{0x09}, addr[:]...), + Delete: true, + } +} + +func setEvmLatest(latest map[string]evmIteratorEntry, key, value []byte) { + latest[string(key)] = evmIteratorEntry{ + Key: bytes.Clone(key), + Value: bytes.Clone(value), + } +} + +func removeEvmLatest(latest map[string]evmIteratorEntry, key []byte) { + delete(latest, string(key)) +} + +func recordStorageLatest(latest map[string]evmIteratorEntry, addr ktype.Address, slot ktype.Slot, val []byte) { + key := keys.BuildEVMKey(keys.EVMKeyStorage, ktype.StorageKey(addr, slot)) + setEvmLatest(latest, key, padLeft32(val...)) +} + +func removeStorageLatest(latest map[string]evmIteratorEntry, addr ktype.Address, slot ktype.Slot) { + removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyStorage, ktype.StorageKey(addr, slot))) +} + +func recordCodeLatest(latest map[string]evmIteratorEntry, addr ktype.Address, bytecode []byte) { + setEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyCode, addr[:]), bytes.Clone(bytecode)) +} + +func removeCodeLatest(latest map[string]evmIteratorEntry, addr ktype.Address) { + removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyCode, addr[:])) +} + +func recordLegacyLatest(latest map[string]evmIteratorEntry, addr ktype.Address, val []byte) { + key := append([]byte{0x09}, addr[:]...) + setEvmLatest(latest, key, bytes.Clone(val)) +} + +func removeLegacyLatest(latest map[string]evmIteratorEntry, addr ktype.Address) { + removeEvmLatest(latest, append([]byte{0x09}, addr[:]...)) +} + +func recordNonceLatest(latest map[string]evmIteratorEntry, addr ktype.Address, nonce uint64) { + setEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyNonce, addr[:]), nonceBytes(nonce)) +} + +func recordCodeHashLatest(latest map[string]evmIteratorEntry, addr ktype.Address, ch vtype.CodeHash) { + key := keys.BuildEVMKey(keys.EVMKeyCodeHash, addr[:]) + var zero vtype.CodeHash + if ch == zero { + removeEvmLatest(latest, key) + return + } + setEvmLatest(latest, key, ch[:]) +} + +func removeAccountLatest(latest map[string]evmIteratorEntry, addr ktype.Address) { + removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyNonce, addr[:])) + removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyCodeHash, addr[:])) +} + +func sortedEvmEntries(latest map[string]evmIteratorEntry) []evmIteratorEntry { + out := make([]evmIteratorEntry, 0, len(latest)) + for _, e := range latest { + out = append(out, e) + } + sort.Slice(out, func(i, j int) bool { + return bytes.Compare(out[i].Key, out[j].Key) < 0 + }) + return out +} + +func findEntry(entries []evmIteratorEntry, key []byte) (evmIteratorEntry, bool) { + for _, e := range entries { + if bytes.Equal(e.Key, key) { + return e, true + } + } + return evmIteratorEntry{}, false +} + +func subrange(sorted []evmIteratorEntry, start, end []byte, ascending bool) []evmIteratorEntry { + out := make([]evmIteratorEntry, 0, len(sorted)) + for _, e := range sorted { + if start != nil && bytes.Compare(e.Key, start) < 0 { + continue + } + if end != nil && bytes.Compare(e.Key, end) >= 0 { + continue + } + out = append(out, e) + } + if !ascending { + for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 { + out[i], out[j] = out[j], out[i] + } + } + return out +} + +func collectIterEntries(t *testing.T, iter dbm.Iterator) []evmIteratorEntry { + t.Helper() + var out []evmIteratorEntry + for ; iter.Valid(); iter.Next() { + out = append(out, evmIteratorEntry{ + Key: bytes.Clone(iter.Key()), + Value: bytes.Clone(iter.Value()), + }) + } + require.NoError(t, iter.Error()) + return out +} + +func sumFlatKVTableIters(s *CommitStore) (int64, error) { + var sum int64 + for _, db := range s.dataDBs() { + n, err := pebbledb.TableIters(db) + if err != nil { + return 0, err + } + sum += n + } + return sum, nil +} From 907268a11e11840b91c574b3835642a29bb72ccc Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Fri, 29 May 2026 13:00:30 -0500 Subject: [PATCH 15/16] fix bug --- sei-db/state_db/sc/flatkv/store_iteration.go | 143 ++++++-- .../sc/flatkv/store_iteration_test.go | 316 +++++++++++++++++- 2 files changed, 426 insertions(+), 33 deletions(-) diff --git a/sei-db/state_db/sc/flatkv/store_iteration.go b/sei-db/state_db/sc/flatkv/store_iteration.go index 37eea6adeb..67e2311e41 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration.go +++ b/sei-db/state_db/sc/flatkv/store_iteration.go @@ -1,6 +1,7 @@ package flatkv import ( + "bytes" "encoding/binary" "fmt" @@ -72,43 +73,39 @@ func (s *CommitStore) buildEvmIterator( end []byte, ascending bool, ) (dbm.Iterator, error) { - lowerBound, upperBound := moduleIteratorBounds(keys.EVMStoreKey, start, end) - lanes := make([]dbm.Iterator, 0, 5) - codeLane, err := s.buildCodeLane(lowerBound, upperBound, ascending) - if err != nil { - return nil, err - } - lanes = append(lanes, codeLane) - storageLane, err := s.buildStorageLane(lowerBound, upperBound, ascending) - if err != nil { - closeIterators(lanes) - return nil, err + // Each optimized lane scans its own physical keyspace and re-labels rows to + // a logical key. The codehash lane is the only one whose logical type byte + // (0x08) differs from the physical byte it scans (account rows live under + // 0x0a), so its bounds must be translated against the account keyspace. + for _, laneSpec := range s.evmLaneSpecs() { + lower, upper, empty, err := laneSpec.bounds(start, end) + if err != nil { + closeIterators(lanes) + return nil, err + } + if empty { + continue + } + lane, err := laneSpec.build(lower, upper, ascending) + if err != nil { + closeIterators(lanes) + return nil, err + } + lanes = append(lanes, lane) } - lanes = append(lanes, storageLane) - legacyLane, err := s.buildLegacyDBLane(keys.EVMStoreKey, lowerBound, upperBound, ascending) + // Legacy is the identity-mapped catch-all (no single type prefix), so it uses + // the whole-range translation and is always built. + legacyLower, legacyUpper := moduleIteratorBounds(keys.EVMStoreKey, start, end) + legacyLane, err := s.buildLegacyDBLane(keys.EVMStoreKey, legacyLower, legacyUpper, ascending) if err != nil { closeIterators(lanes) return nil, err } lanes = append(lanes, legacyLane) - nonceLane, err := s.buildAccountNonceLane(lowerBound, upperBound, ascending) - if err != nil { - closeIterators(lanes) - return nil, err - } - lanes = append(lanes, nonceLane) - - codehashLane, err := s.buildAccountCodehashLane(lowerBound, upperBound, ascending) - if err != nil { - closeIterators(lanes) - return nil, err - } - lanes = append(lanes, codehashLane) - // TODO: once we move account balances to FlatKV, we need to add a lane for them here. merged, err := iterators.NewMergingIterator(ascending, lanes...) @@ -119,6 +116,98 @@ func (s *CommitStore) buildEvmIterator( return merged, nil } +// evmLaneSpec describes one EVM iterator lane. +type evmLaneSpec struct { + // logical is the type byte callers query with. + logical keys.EVMKeyKind + // physical is the type byte the lane's rows are stored under; equal to + // logical for every lane except codehash, whose rows live in the account DB + // under 0x0a. + physical keys.EVMKeyKind + // build constructs the iterator that scans the lane's physical keyspace. + build func(lower []byte, upper []byte, ascending bool) (dbm.Iterator, error) +} + +// bounds resolves the lane's logical and physical type bytes and translates the +// caller's logical [start,end) into this lane's physical [lower,upper) via +// evmLaneBounds. empty is true when the lane's span is disjoint from [start,end) +// and the lane should be skipped. +func (sp evmLaneSpec) bounds(start []byte, end []byte) (lower []byte, upper []byte, empty bool, err error) { + // the logical prefix, i.e. the prefix from the perspective of the external caller + logicalByte, ok := keys.EVMKeyPrefixByte(sp.logical) + if !ok { + return nil, nil, false, fmt.Errorf("no prefix byte for EVM key kind %v", sp.logical) + } + // the physical type byte the rows are stored under in the low level DB; + // the full physical prefix is the module name "evm/" followed by this byte + physByte, ok := keys.EVMKeyPrefixByte(sp.physical) + if !ok { + return nil, nil, false, fmt.Errorf("no prefix byte for EVM key kind %v", sp.physical) + } + lower, upper, empty = evmLaneBounds(start, end, logicalByte, physByte) + return lower, upper, empty, nil +} + +// evmLaneSpecs returns the optimized lanes, in no particular order (the merging +// iterator orders the combined output). The legacy catch-all lane is handled +// separately because it has no single type prefix. +func (s *CommitStore) evmLaneSpecs() []evmLaneSpec { + return []evmLaneSpec{ + {keys.EVMKeyStorage, keys.EVMKeyStorage, s.buildStorageLane}, + {keys.EVMKeyCode, keys.EVMKeyCode, s.buildCodeLane}, + {keys.EVMKeyCodeHash, ktype.EVMKeyAccount, s.buildAccountCodehashLane}, + {keys.EVMKeyNonce, ktype.EVMKeyAccount, s.buildAccountNonceLane}, + } +} + +// evmLaneBounds maps the caller's logical [start,end) range to the physical +// [lower,upper) range for a single EVM lane. Physical keys are +// "evm/" + physByte + suffix while logical keys are logicalPrefix + suffix, so +// the suffix and intra-lane ordering are preserved: translating the clamped +// logical bounds yields physical bounds that select exactly the in-range rows. +func evmLaneBounds( + // start is the inclusive lower bound of the caller's logical range; nil means unbounded. + start []byte, + // end is the exclusive upper bound of the caller's logical range; nil means unbounded. + end []byte, + // logicalPrefix is the lane's logical type byte (the prefix callers use, e.g. 0x08 for codehash). + logicalPrefix byte, + // physByte is the physical type byte the rows are stored under. It equals logicalPrefix for every + // lane except codehash, whose rows live in the account DB under 0x0a. + physByte byte, +) ( + // lower is the physical inclusive lower bound for the lane. + lower []byte, + // upper is the physical exclusive upper bound for the lane. + upper []byte, + // empty is true when [start,end) is disjoint from the lane's span, so the lane should be skipped. + empty bool, +) { + lp := []byte{logicalPrefix} + lpEnd := ktype.PrefixEnd(lp) + + lo := lp + if start != nil && bytes.Compare(start, lp) > 0 { + lo = start + } + hi := lpEnd + if end != nil && bytes.Compare(end, lpEnd) < 0 { + hi = end + } + if bytes.Compare(lo, hi) >= 0 { + return nil, nil, true + } + + physPrefix := ktype.ModulePhysicalKey(keys.EVMStoreKey, []byte{physByte}) + lower = ktype.ModulePhysicalKey(keys.EVMStoreKey, append([]byte{physByte}, lo[1:]...)) + if bytes.Equal(hi, lpEnd) { + upper = ktype.PrefixEnd(physPrefix) + } else { + upper = ktype.ModulePhysicalKey(keys.EVMStoreKey, append([]byte{physByte}, hi[1:]...)) + } + return lower, upper, false +} + // moduleIteratorBounds translates caller logical [start, end) keys into physical // bounds for iterating a module-prefixed keyspace in the data DBs. func moduleIteratorBounds(store string, start, end []byte) (lowerBound, upperBound []byte) { diff --git a/sei-db/state_db/sc/flatkv/store_iteration_test.go b/sei-db/state_db/sc/flatkv/store_iteration_test.go index 436919921b..7a0c9e2890 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration_test.go +++ b/sei-db/state_db/sc/flatkv/store_iteration_test.go @@ -3,6 +3,7 @@ package flatkv import ( "bytes" "flag" + "fmt" "hash/fnv" "math/rand" "os" @@ -65,8 +66,16 @@ func TestEvmIterator(t *testing.T) { storageEnd := ktype.PrefixEnd(storageStart) codeStart := []byte{0x07} codeEnd := ktype.PrefixEnd(codeStart) + codeHashStart := []byte{0x08} + codeHashEnd := ktype.PrefixEnd(codeHashStart) legacyStart := []byte{0x09} legacyEnd := ktype.PrefixEnd(legacyStart) + nonceStart := []byte{0x0a} + nonceEnd := ktype.PrefixEnd(nonceStart) + midAddr := addrN(0x80) + crossSpanStart := keys.BuildEVMKey(keys.EVMKeyCodeHash, midAddr[:]) // 0x08 || addr + crossSpanEnd := keys.BuildEVMKey(keys.EVMKeyNonce, midAddr[:]) // 0x0a || addr + storageResumeStart := evmStorageKey(addrN(0x40), slotN(0x10)) // 0x03 || addr || slot cases := []struct { name string @@ -79,6 +88,13 @@ func TestEvmIterator(t *testing.T) { {name: "storage prefix range", start: storageStart, end: storageEnd, ascending: true}, {name: "legacy sub-range", start: legacyStart, end: legacyEnd, ascending: true}, {name: "code prefix range", start: codeStart, end: codeEnd, ascending: true}, + {name: "codehash prefix range ascending", start: codeHashStart, end: codeHashEnd, ascending: true}, + {name: "codehash prefix range descending", start: codeHashStart, end: codeHashEnd, ascending: false}, + {name: "nonce prefix range ascending", start: nonceStart, end: nonceEnd, ascending: true}, + {name: "nonce prefix range descending", start: nonceStart, end: nonceEnd, ascending: false}, + {name: "cross span codehash to nonce ascending", start: crossSpanStart, end: crossSpanEnd, ascending: true}, + {name: "cross span codehash to nonce descending", start: crossSpanStart, end: crossSpanEnd, ascending: false}, + {name: "storage resume ascending", start: storageResumeStart, end: nil, ascending: true}, } for _, tc := range cases { @@ -154,6 +170,278 @@ func TestLegacyIteratorNonEVM(t *testing.T) { }, got) } +// TestEvmLaneBounds exercises every branch of evmLaneBounds in +// isolation, for an aligned lane (logical == physical byte) and the misaligned +// codehash lane (logical 0x08 -> physical account byte 0x0a). +func TestEvmLaneBounds(t *testing.T) { + phys := func(b byte, suffix ...byte) []byte { + return append([]byte{'e', 'v', 'm', '/', b}, suffix...) + } + + cases := []struct { + name string + start []byte + end []byte + logical byte + physical byte + wantEmpty bool + wantLower []byte + wantUpper []byte + }{ + // Aligned lane (nonce: logical 0x0a, physical 0x0a). + { + name: "aligned within span", + start: []byte{0x0a, 0x01}, + end: []byte{0x0a, 0x02}, + logical: 0x0a, + physical: 0x0a, + wantLower: phys(0x0a, 0x01), + wantUpper: phys(0x0a, 0x02), + }, + { + name: "aligned low clamp nil start", + start: nil, + end: []byte{0x0a, 0x02}, + logical: 0x0a, + physical: 0x0a, + wantLower: phys(0x0a), + wantUpper: phys(0x0a, 0x02), + }, + { + name: "aligned high clamp nil end", + start: []byte{0x0a, 0x01}, + end: nil, + logical: 0x0a, + physical: 0x0a, + wantLower: phys(0x0a, 0x01), + wantUpper: phys(0x0b), + }, + { + name: "aligned full range", + start: nil, + end: nil, + logical: 0x0a, + physical: 0x0a, + wantLower: phys(0x0a), + wantUpper: phys(0x0b), + }, + { + name: "aligned start below span", + start: []byte{0x05}, + end: nil, + logical: 0x0a, + physical: 0x0a, + wantLower: phys(0x0a), + wantUpper: phys(0x0b), + }, + { + name: "aligned end above span", + start: nil, + end: []byte{0x20}, + logical: 0x0a, + physical: 0x0a, + wantLower: phys(0x0a), + wantUpper: phys(0x0b), + }, + { + name: "aligned exact bare endpoints", + start: []byte{0x0a}, + end: []byte{0x0b}, + logical: 0x0a, + physical: 0x0a, + wantLower: phys(0x0a), + wantUpper: phys(0x0b), + }, + { + name: "aligned disjoint below", + start: nil, + end: []byte{0x09}, + logical: 0x0a, + physical: 0x0a, + wantEmpty: true, + }, + { + name: "aligned disjoint above", + start: []byte{0x0b}, + end: nil, + logical: 0x0a, + physical: 0x0a, + wantEmpty: true, + }, + { + name: "aligned single key empty", + start: []byte{0x0a, 0x01}, + end: []byte{0x0a, 0x01}, + logical: 0x0a, + physical: 0x0a, + wantEmpty: true, + }, + + // Misaligned codehash lane (logical 0x08, physical 0x0a). + { + name: "codehash within span swaps prefix", + start: []byte{0x08, 0x01}, + end: []byte{0x08, 0x02}, + logical: 0x08, + physical: 0x0a, + wantLower: phys(0x0a, 0x01), + wantUpper: phys(0x0a, 0x02), + }, + { + name: "codehash full prefix maps to account region", + start: []byte{0x08}, + end: []byte{0x09}, + logical: 0x08, + physical: 0x0a, + wantLower: phys(0x0a), + wantUpper: phys(0x0b), + }, + { + name: "codehash nil maps to account region", + start: nil, + end: nil, + logical: 0x08, + physical: 0x0a, + wantLower: phys(0x0a), + wantUpper: phys(0x0b), + }, + { + name: "codehash disjoint from nonce query", + start: []byte{0x0a}, + end: []byte{0x0b}, + logical: 0x08, + physical: 0x0a, + wantEmpty: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + lower, upper, empty := evmLaneBounds(tc.start, tc.end, tc.logical, tc.physical) + require.Equal(t, tc.wantEmpty, empty) + if tc.wantEmpty { + return + } + require.Equal(t, tc.wantLower, lower, "lower") + require.Equal(t, tc.wantUpper, upper, "upper") + }) + } +} + +// TestEvmIteratorDifferential compares the EVM iterator against the +// independent subrange oracle across many randomized boundary-relevant ranges +// and both directions. Because the oracle is implementation-independent and the +// comparison is over full ordered slices, this catches out-of-range emission, +// missing keys, mis-ordering, inclusive/exclusive boundary errors, and +// direction bugs. +func TestEvmIteratorDifferential(t *testing.T) { + seed := iteratorTestSeed(t, "TestEvmIteratorDifferential") + fixture := buildEvmIteratorFixture(t, seed) + defer func() { require.NoError(t, fixture.Store.Close()) }() + + rng := rand.New(rand.NewSource(seed ^ 0x5deece66d)) //nolint:gosec // deterministic test data only + + // Pool of boundary-relevant keys: every committed/pending key, plus each + // type prefix and its prefix-end. + var pool [][]byte + for _, e := range fixture.Sorted { + pool = append(pool, bytes.Clone(e.Key)) + } + for _, p := range [][]byte{{0x03}, {0x07}, {0x08}, {0x09}, {0x0a}} { + pool = append(pool, bytes.Clone(p), ktype.PrefixEnd(p)) + } + + pick := func() []byte { + switch rng.Intn(6) { + case 0: + return nil + case 1: + return bytes.Clone(pool[rng.Intn(len(pool))]) + case 2: + return decrementKey(pool[rng.Intn(len(pool))]) + case 3: + return incrementKey(pool[rng.Intn(len(pool))]) + default: + n := 1 + rng.Intn(21) + b := make([]byte, n) + rng.Read(b) + return b + } + } + + const iterations = 400 + for i := 0; i < iterations; i++ { + start := pick() + end := pick() + ascending := rng.Intn(2) == 0 + + want := subrange(fixture.Sorted, start, end, ascending) + iter, err := fixture.Store.Iterator(keys.EVMStoreKey, start, end, ascending) + require.NoError(t, err) + got := collectIterEntries(t, iter) + require.NoError(t, iter.Close()) + msg := fmt.Sprintf("iter %d start=%x end=%x ascending=%v seed=%d", i, start, end, ascending, fixture.Seed) + if len(want) == 0 { + require.Empty(t, got, msg) + } else { + require.Equal(t, want, got, msg) + } + } +} + +// TestEvmIteratorEmptyAndDegenerate covers an empty store and +// degenerate ranges (equal bounds, inverted bounds) on a populated store. +func TestEvmIteratorEmptyAndDegenerate(t *testing.T) { + t.Run("empty store", func(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + iter, err := s.Iterator(keys.EVMStoreKey, nil, nil, true) + require.NoError(t, err) + require.Empty(t, collectIterEntries(t, iter)) + require.NoError(t, iter.Close()) + }) + + seed := iteratorTestSeed(t, "TestEvmIteratorEmptyAndDegenerate") + fixture := buildEvmIteratorFixture(t, seed) + defer func() { require.NoError(t, fixture.Store.Close()) }() + require.NotEmpty(t, fixture.Sorted) + + lo := fixture.Sorted[0].Key + hi := fixture.Sorted[len(fixture.Sorted)-1].Key + + t.Run("equal bounds", func(t *testing.T) { + iter, err := fixture.Store.Iterator(keys.EVMStoreKey, lo, lo, true) + require.NoError(t, err) + require.Empty(t, collectIterEntries(t, iter)) + require.NoError(t, iter.Close()) + }) + + t.Run("inverted bounds", func(t *testing.T) { + iter, err := fixture.Store.Iterator(keys.EVMStoreKey, hi, lo, true) + require.NoError(t, err) + require.Empty(t, collectIterEntries(t, iter)) + require.NoError(t, iter.Close()) + }) +} + +// decrementKey returns the largest key strictly less than k (for boundary +// fuzzing). incrementKey returns the smallest key strictly greater than k. +func decrementKey(k []byte) []byte { + if len(k) == 0 { + return nil + } + out := bytes.Clone(k) + if out[len(out)-1] > 0 { + out[len(out)-1]-- + return out + } + return out[:len(out)-1] +} + +func incrementKey(k []byte) []byte { + return append(bytes.Clone(k), 0x00) +} + func iteratorTestSeed(t *testing.T, label string) int64 { t.Helper() if *evmIterSeed != 0 { @@ -182,7 +470,7 @@ func buildEvmIteratorFixture(t *testing.T, seed int64) *evmIteratorFixture { var batch1, batch2 []*proto.KVPair var overlapSamples []evmIteratorEntry var tombstonedKeys [][]byte - usedAddrs := make(map[byte]struct{}, 32) + usedAddrs := make(map[string]struct{}, 32) gen := &evmIteratorGenerator{ rng: rng, @@ -209,6 +497,11 @@ func buildEvmIteratorFixture(t *testing.T, seed int64) *evmIteratorFixture { // Nonce-only account (no codehash key in iterator output). gen.addNonceOnlyAccount() + // Malformed account-prefixed legacy key: lands in the account physical + // region (evm/0x0a...) but is routed to legacyDB, exercising the overlap + // between the legacy lane and the account-derived lanes. + gen.addMalformedAccountLegacyKey() + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS(batch1...)})) commitAndCheck(t, s) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS(batch2...)})) @@ -229,21 +522,32 @@ type evmIteratorGenerator struct { batch2 *[]*proto.KVPair overlaps *[]evmIteratorEntry tombstones *[][]byte - usedAddrs map[byte]struct{} + usedAddrs map[string]struct{} } func (g *evmIteratorGenerator) uniqueAddr() ktype.Address { for attempts := 0; attempts < 512; attempts++ { - b := byte(g.rng.Intn(256)) - if _, used := g.usedAddrs[b]; used { + var a ktype.Address + g.rng.Read(a[:]) + if _, used := g.usedAddrs[string(a[:])]; used { continue } - g.usedAddrs[b] = struct{}{} - return addrN(b) + g.usedAddrs[string(a[:])] = struct{}{} + return a } panic("failed to allocate unique test address") } +// addMalformedAccountLegacyKey writes a 0x0a-prefixed key whose length does not +// match a well-formed nonce key, so it routes to legacyDB while physically +// living in the account keyspace (evm/0x0a...). +func (g *evmIteratorGenerator) addMalformedAccountLegacyKey() { + key := append([]byte{0x0a}, bytes.Repeat([]byte{0x7f}, 19)...) + val := []byte{0xab, 0xcd} + *g.batch1 = append(*g.batch1, &proto.KVPair{Key: bytes.Clone(key), Value: bytes.Clone(val)}) + setEvmLatest(g.latest, key, val) +} + func (g *evmIteratorGenerator) uniqueSlot() ktype.Slot { var s ktype.Slot g.rng.Read(s[:]) From 4f08135323980e72845fc7c169cb76618f4e33c6 Mon Sep 17 00:00:00 2001 From: Cody Littley Date: Fri, 29 May 2026 14:06:28 -0500 Subject: [PATCH 16/16] fixes --- sei-db/common/iterators/domain_iterator.go | 33 +++++ .../common/iterators/domain_iterator_test.go | 63 ++++++++ .../common/iterators/transforming_iterator.go | 10 +- sei-db/state_db/sc/flatkv/api.go | 15 +- sei-db/state_db/sc/flatkv/snapshot.go | 5 + sei-db/state_db/sc/flatkv/store.go | 21 ++- sei-db/state_db/sc/flatkv/store_apply.go | 6 + sei-db/state_db/sc/flatkv/store_iteration.go | 26 +++- .../sc/flatkv/store_iteration_test.go | 136 ++++++++++++++++++ sei-db/state_db/sc/flatkv/store_read.go | 12 ++ sei-db/state_db/sc/flatkv/store_write.go | 10 ++ 11 files changed, 331 insertions(+), 6 deletions(-) create mode 100644 sei-db/common/iterators/domain_iterator.go create mode 100644 sei-db/common/iterators/domain_iterator_test.go diff --git a/sei-db/common/iterators/domain_iterator.go b/sei-db/common/iterators/domain_iterator.go new file mode 100644 index 0000000000..304867b82f --- /dev/null +++ b/sei-db/common/iterators/domain_iterator.go @@ -0,0 +1,33 @@ +package iterators + +import ( + "fmt" + + dbm "github.com/tendermint/tm-db" +) + +var _ dbm.Iterator = (*domainIterator)(nil) + +// domainIterator wraps a parent iterator and overrides Domain() to report a +// caller-supplied [start, end) range. It is useful when the parent is built +// over a physical/translated keyspace (so its own Domain() reflects physical +// bounds) but callers expect the logical bounds they requested, as required by +// the dbm.Iterator contract. All other methods are inherited from the parent. +type domainIterator struct { + dbm.Iterator + start []byte + end []byte +} + +// NewDomainIterator returns an iterator that behaves exactly like parent except +// that Domain() reports [start, end). The parent must be non-nil. +func NewDomainIterator(parent dbm.Iterator, start, end []byte) (dbm.Iterator, error) { + if parent == nil { + return nil, fmt.Errorf("nil parent iterator") + } + return &domainIterator{Iterator: parent, start: start, end: end}, nil +} + +func (d *domainIterator) Domain() ([]byte, []byte) { + return d.start, d.end +} diff --git a/sei-db/common/iterators/domain_iterator_test.go b/sei-db/common/iterators/domain_iterator_test.go new file mode 100644 index 0000000000..32bdcaf05a --- /dev/null +++ b/sei-db/common/iterators/domain_iterator_test.go @@ -0,0 +1,63 @@ +package iterators_test + +import ( + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/common/iterators" + "github.com/stretchr/testify/require" +) + +func TestNewDomainIterator_NilParent(t *testing.T) { + it, err := iterators.NewDomainIterator(nil, []byte("a"), []byte("z")) + require.Error(t, err) + require.Nil(t, it) +} + +func TestNewDomainIterator_OverridesDomain(t *testing.T) { + data := map[string][]byte{ + "b": []byte("vb"), + "a": []byte("va"), + "c": []byte("vc"), + } + parent, err := iterators.NewMapIterator(nil, nil, true, iterators.BytesSerializer, data) + require.NoError(t, err) + + // Sanity check: the parent reports nil bounds before wrapping. + pStart, pEnd := parent.Domain() + require.Nil(t, pStart) + require.Nil(t, pEnd) + + start, end := []byte("a"), []byte("d") + it, err := iterators.NewDomainIterator(parent, start, end) + require.NoError(t, err) + defer it.Close() + + gotStart, gotEnd := it.Domain() + require.Equal(t, start, gotStart) + require.Equal(t, end, gotEnd) +} + +func TestNewDomainIterator_DelegatesIteration(t *testing.T) { + data := map[string][]byte{ + "a": []byte("va"), + "b": []byte("vb"), + "c": []byte("vc"), + } + parent, err := iterators.NewMapIterator(nil, nil, true, iterators.BytesSerializer, data) + require.NoError(t, err) + + it, err := iterators.NewDomainIterator(parent, []byte("a"), []byte("d")) + require.NoError(t, err) + defer it.Close() + + var got [][2][]byte + for ; it.Valid(); it.Next() { + got = append(got, [2][]byte{it.Key(), it.Value()}) + } + require.NoError(t, it.Error()) + require.Equal(t, [][2][]byte{ + {[]byte("a"), []byte("va")}, + {[]byte("b"), []byte("vb")}, + {[]byte("c"), []byte("vc")}, + }, got) +} diff --git a/sei-db/common/iterators/transforming_iterator.go b/sei-db/common/iterators/transforming_iterator.go index 0ac43a981b..7021220b3e 100644 --- a/sei-db/common/iterators/transforming_iterator.go +++ b/sei-db/common/iterators/transforming_iterator.go @@ -37,6 +37,10 @@ type transformingIterator struct { key []byte // The next value to emit. value []byte + // valid reports whether key/value hold a current entry to emit. Tracked + // explicitly rather than inferred from key != nil so a transform may + // legitimately emit a nil/empty key without terminating iteration. + valid bool // The error encountered by the iterator, if any. err error } @@ -72,6 +76,7 @@ func NewTransformingIterator(parent dbm.Iterator, transform IteratorTransform) ( func (m *transformingIterator) advance() { m.key = nil m.value = nil + m.valid = false if m.parent == nil { return } @@ -90,6 +95,7 @@ func (m *transformingIterator) advance() { if !skip { m.key = outputKey m.value = outputValue + m.valid = true return } m.parent.Next() @@ -108,6 +114,7 @@ func (m *transformingIterator) fail(err error) { m.err = err m.key = nil m.value = nil + m.valid = false if m.parent != nil { _ = m.parent.Close() m.parent = nil @@ -122,6 +129,7 @@ func (m *transformingIterator) Close() error { m.parent = nil m.key = nil m.value = nil + m.valid = false return err } @@ -156,7 +164,7 @@ func (m *transformingIterator) Next() { } func (m *transformingIterator) Valid() bool { - return m.err == nil && m.key != nil + return m.err == nil && m.valid } func (m *transformingIterator) Value() []byte { diff --git a/sei-db/state_db/sc/flatkv/api.go b/sei-db/state_db/sc/flatkv/api.go index fe972b04e8..b007894400 100644 --- a/sei-db/state_db/sc/flatkv/api.go +++ b/sei-db/state_db/sc/flatkv/api.go @@ -61,10 +61,23 @@ type Store interface { // keys across underlying data DBs, merged in global lexicographic order. // Keys are physical format: "evm/" + type_prefix_byte + stripped_key. // Pending writes are not visible. Keys and values are read-only; copy - // before modifying. Caller must Close when done. + // before modifying. + // + // The returned iterator is a stable snapshot taken at construction: it may + // be used concurrently with, and outlive, subsequent ApplyChangeSets/Commit + // calls without observing their effects. The caller must Close it when done; + // an open iterator pins Pebble sstables/memtables and holds back compaction, + // so close promptly rather than relying on it being safe to keep open. RawGlobalIterator() (dbm.Iterator, error) // Create an iterator over a range of keys in a given store. + // + // The returned iterator is a stable snapshot taken at construction (pending + // writes are cloned and the Pebble view is pinned): it may be used + // concurrently with, and outlive, subsequent ApplyChangeSets/Commit calls + // without observing their effects. The caller must Close it when done; an + // open iterator pins Pebble resources and holds back compaction, so close + // promptly rather than relying on it being safe to keep open. Iterator( // The store to iterate over. store string, diff --git a/sei-db/state_db/sc/flatkv/snapshot.go b/sei-db/state_db/sc/flatkv/snapshot.go index b7e9b9fe21..1c2149c100 100644 --- a/sei-db/state_db/sc/flatkv/snapshot.go +++ b/sei-db/state_db/sc/flatkv/snapshot.go @@ -433,6 +433,11 @@ func (s *CommitStore) migrateFlatLayout(flatkvDir string) (string, error) { // The snapshot is written into a versioned subdirectory under the flatkv root // (e.g. flatkv/snapshot-00000000000000000100) and the current symlink is updated. // The dir parameter is ignored; snapshots are always stored alongside the live data. +// +// Concurrency: this MUST NOT acquire s.mu. Commit calls it while already holding +// the write lock (s.mu is not reentrant), and as a lifecycle operation it is +// otherwise expected to be serialized by the caller. It only reads committed +// state and checkpoints the DBs; it does not touch the pending-writes maps. func (s *CommitStore) WriteSnapshot(_ string) (err error) { var pruned int obs := s.observeOp("snapshot", otelMetrics.SnapshotWriteLatency, diff --git a/sei-db/state_db/sc/flatkv/store.go b/sei-db/state_db/sc/flatkv/store.go index c59a690ada..f70027c551 100644 --- a/sei-db/state_db/sc/flatkv/store.go +++ b/sei-db/state_db/sc/flatkv/store.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "runtime" + "sync" "time" "github.com/zbiljic/go-filelock" @@ -91,8 +92,26 @@ func applyPebbleMetricsConfig(c *config.Config) { } // CommitStore implements flatkv.Store for EVM state storage. -// NOT thread-safe; callers must serialize all operations. +// +// Concurrency: writes (ApplyChangeSets, Commit) and the reads that touch the +// pending-writes maps (Get, Has, GetBlockHeightModified) and iterator +// construction (Iterator, RawGlobalIterator) are guarded by mu. Iterators +// snapshot their data at construction time (pending writes are cloned and the +// Pebble view is pinned), so once built they may be used and Closed without +// holding mu and may safely outlive a subsequent ApplyChangeSets/Commit. All +// other lifecycle operations (LoadVersion, Rollback, snapshot/import/export, +// Close) must still be serialized by the caller. type CommitStore struct { + // mu guards the pending-writes maps against concurrent iterator + // construction / reads while ApplyChangeSets and Commit mutate them. + // + // TODO(concurrency): this is a coarse lock taken at the exported entry + // points. Commit in particular holds the write lock across its WAL fsync + // and periodic auto-snapshot. That is acceptable while commits are not + // pipelined with reads; revisit with a finer-grained scheme (guarding only + // the in-memory maps) if/when pipelining is introduced. + mu sync.RWMutex + ctx context.Context cancel context.CancelFunc config config.Config diff --git a/sei-db/state_db/sc/flatkv/store_apply.go b/sei-db/state_db/sc/flatkv/store_apply.go index cd8967628f..a653ff1342 100644 --- a/sei-db/state_db/sc/flatkv/store_apply.go +++ b/sei-db/state_db/sc/flatkv/store_apply.go @@ -22,6 +22,12 @@ func (s *CommitStore) ApplyChangeSets(changeSets []*proto.NamedChangeSet) (err e return errReadOnly } + // Hold the write lock for the whole body: it both reads + // (batchReadOldValues) and mutates (maps.Copy) the pending-writes maps, + // which iterator construction and Get read under a read lock. + s.mu.Lock() + defer s.mu.Unlock() + /////////// // Setup // /////////// diff --git a/sei-db/state_db/sc/flatkv/store_iteration.go b/sei-db/state_db/sc/flatkv/store_iteration.go index 67e2311e41..852fb528dc 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration.go +++ b/sei-db/state_db/sc/flatkv/store_iteration.go @@ -18,6 +18,11 @@ import ( // order. Within each DB, keys are in Pebble order. Per-DB _meta/* keys are // skipped. Pending writes are not visible. metadataDB is not included. func (s *CommitStore) RawGlobalIterator() (dbm.Iterator, error) { + // Read lock for the construction span: the returned iterator pins a Pebble + // view and may then outlive a concurrent ApplyChangeSets/Commit. + s.mu.RLock() + defer s.mu.RUnlock() + dbs := s.dataDBs() children := make([]dbm.Iterator, 0, len(dbs)) for _, db := range dbs { @@ -50,11 +55,26 @@ func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending return nil, fmt.Errorf("store name cannot be empty") } + // Read lock for the construction span: buildEvmIterator/buildLegacyDBLane + // snapshot the pending-writes maps and pin the Pebble view here, so the + // returned iterator may safely outlive a concurrent ApplyChangeSets/Commit. + s.mu.RLock() + defer s.mu.RUnlock() + + var iter dbm.Iterator + var err error if store == keys.EVMStoreKey { - return s.buildEvmIterator(start, end, ascending) + iter, err = s.buildEvmIterator(start, end, ascending) + } else { + lowerBound, upperBound := moduleIteratorBounds(store, start, end) + iter, err = s.buildLegacyDBLane(store, lowerBound, upperBound, ascending) + } + if err != nil { + return nil, err } - lowerBound, upperBound := moduleIteratorBounds(store, start, end) - return s.buildLegacyDBLane(store, lowerBound, upperBound, ascending) + // The underlying lane/merge/transform iterators report physical Pebble + // bounds from Domain(); present the caller's logical [start, end) instead. + return iterators.NewDomainIterator(iter, start, end) } /* Data flow: buildEvmIterator diff --git a/sei-db/state_db/sc/flatkv/store_iteration_test.go b/sei-db/state_db/sc/flatkv/store_iteration_test.go index 7a0c9e2890..286cda6ac9 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration_test.go +++ b/sei-db/state_db/sc/flatkv/store_iteration_test.go @@ -8,6 +8,7 @@ import ( "math/rand" "os" "sort" + "sync" "testing" "github.com/sei-protocol/sei-chain/sei-db/common/keys" @@ -170,6 +171,110 @@ func TestLegacyIteratorNonEVM(t *testing.T) { }, got) } +// TestEvmIteratorDomain verifies that Iterator reports the caller's logical +// [start, end) from Domain() (M3), not the underlying physical Pebble bounds. +func TestEvmIteratorDomain(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + t.Run("evm bounded", func(t *testing.T) { + start := []byte{0x07} + end := []byte{0x09} + iter, err := s.Iterator(keys.EVMStoreKey, start, end, true) + require.NoError(t, err) + defer func() { require.NoError(t, iter.Close()) }() + + gotStart, gotEnd := iter.Domain() + require.Equal(t, start, gotStart) + require.Equal(t, end, gotEnd) + }) + + t.Run("evm unbounded", func(t *testing.T) { + iter, err := s.Iterator(keys.EVMStoreKey, nil, nil, true) + require.NoError(t, err) + defer func() { require.NoError(t, iter.Close()) }() + + gotStart, gotEnd := iter.Domain() + require.Nil(t, gotStart) + require.Nil(t, gotEnd) + }) + + t.Run("non-evm bounded", func(t *testing.T) { + start := []byte("a") + end := []byte("z") + iter, err := s.Iterator("bank", start, end, true) + require.NoError(t, err) + defer func() { require.NoError(t, iter.Close()) }() + + gotStart, gotEnd := iter.Domain() + require.Equal(t, start, gotStart) + require.Equal(t, end, gotEnd) + }) +} + +// TestEvmIteratorSnapshotConcurrentWithWrites exercises the RWMutex (M2): +// iterators are stable snapshots that can be built and drained concurrently +// with ApplyChangeSets/Commit, and a snapshot opened before writes is unaffected +// by later commits. Run with -race to detect unsynchronized access to the +// pending-writes maps. +func TestEvmIteratorSnapshotConcurrentWithWrites(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + base := addrN(0x01) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS( + noncePair(base, 7), + codePair(base, []byte{0xaa}), + storagePair(base, slotN(0x01), []byte{0xbb}), + )})) + commitAndCheck(t, s) + + // Expected committed-only state, captured before any concurrent writes. + wantIter, err := s.Iterator(keys.EVMStoreKey, nil, nil, true) + require.NoError(t, err) + want := collectIterEntries(t, wantIter) + require.NoError(t, wantIter.Close()) + + // A snapshot opened before the writer starts must keep returning `want` + // regardless of the commits that follow. + snap, err := s.Iterator(keys.EVMStoreKey, nil, nil, true) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 50; i++ { + a := addrN(byte(0x20 + i)) + if applyErr := s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS( + noncePair(a, uint64(i+1)), + )}); applyErr != nil { + t.Errorf("ApplyChangeSets: %v", applyErr) + return + } + if _, commitErr := s.Commit(); commitErr != nil { + t.Errorf("Commit: %v", commitErr) + return + } + } + }() + + // Concurrently build and drain fresh iterators (RLock) while the writer + // holds the write lock, to stress the lock under -race. + for i := 0; i < 50; i++ { + it, iterErr := s.Iterator(keys.EVMStoreKey, nil, nil, true) + require.NoError(t, iterErr) + _ = collectIterEntries(t, it) + require.NoError(t, it.Close()) + } + + wg.Wait() + + got := collectIterEntries(t, snap) + require.NoError(t, snap.Close()) + require.Equal(t, want, got, "pre-write snapshot must be unaffected by concurrent commits") +} + // TestEvmLaneBounds exercises every branch of evmLaneBounds in // isolation, for an aligned lane (logical == physical byte) and the misaligned // codehash lane (logical 0x08 -> physical account byte 0x0a). @@ -502,6 +607,15 @@ func buildEvmIteratorFixture(t *testing.T, seed int64) *evmIteratorFixture { // between the legacy lane and the account-derived lanes. gen.addMalformedAccountLegacyKey() + // Malformed storage/code-prefixed legacy keys: correct type byte but wrong + // length, so they route to legacyDB and physically live in the storage + // (evm/0x03...) and code (evm/0x07...) keyspaces. Confirms the legacy lane + // interleaves with the storage and code lanes and that the merge does not + // falsely dedup a legacy key against an optimized-lane key of a different + // length. + gen.addMalformedStoragePrefixLegacyKey() + gen.addMalformedCodePrefixLegacyKey() + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS(batch1...)})) commitAndCheck(t, s) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS(batch2...)})) @@ -548,6 +662,28 @@ func (g *evmIteratorGenerator) addMalformedAccountLegacyKey() { setEvmLatest(g.latest, key, val) } +// addMalformedStoragePrefixLegacyKey writes a 0x03-prefixed key whose length +// does not match a well-formed storage key (1 + 20 + 32), so it routes to +// legacyDB while physically living in the storage keyspace (evm/0x03...). It is +// committed (batch1) so the legacy and storage lanes interleave over pebble. +func (g *evmIteratorGenerator) addMalformedStoragePrefixLegacyKey() { + key := append([]byte{0x03}, bytes.Repeat([]byte{0x7f}, ktype.AddressLen)...) + val := []byte{0x12, 0x34} + *g.batch1 = append(*g.batch1, &proto.KVPair{Key: bytes.Clone(key), Value: bytes.Clone(val)}) + setEvmLatest(g.latest, key, val) +} + +// addMalformedCodePrefixLegacyKey writes a 0x07-prefixed key whose length does +// not match a well-formed code key (1 + 20), so it routes to legacyDB while +// physically living in the code keyspace (evm/0x07...). It is pending-only +// (batch2) so the legacy and code lanes interleave over pending writes too. +func (g *evmIteratorGenerator) addMalformedCodePrefixLegacyKey() { + key := append([]byte{0x07}, bytes.Repeat([]byte{0x5a}, ktype.AddressLen-1)...) + val := []byte{0x56, 0x78} + *g.batch2 = append(*g.batch2, &proto.KVPair{Key: bytes.Clone(key), Value: bytes.Clone(val)}) + setEvmLatest(g.latest, key, val) +} + func (g *evmIteratorGenerator) uniqueSlot() ktype.Slot { var s ktype.Slot g.rng.Read(s[:]) diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index 630955ad3f..42e980bba1 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -18,6 +18,13 @@ import ( // Returns (value, true) if found, (nil, false) if not found. // Panics on I/O errors or unsupported key types. func (s *CommitStore) Get(moduleName string, key []byte) ([]byte, bool) { + // Read lock: the internal getters (getAccountData, getStorageData, + // getCodeData, getLegacyData) read the pending-writes maps, which + // ApplyChangeSets/Commit mutate under the write lock. Has delegates to Get + // and must not take its own lock (RWMutex read locks are not reentrant). + s.mu.RLock() + defer s.mu.RUnlock() + if moduleName != keys.EVMStoreKey { value, err := s.getLegacyValue(moduleName, key) if err != nil { @@ -83,6 +90,11 @@ func (s *CommitStore) Get(moduleName string, key []byte) ([]byte, bool) { // Only supported for EVM keys; non-EVM legacy data does not track block height. // If not found, returns (-1, false, nil). func (s *CommitStore) GetBlockHeightModified(moduleName string, key []byte) (int64, bool, error) { + // Read lock: the internal getters (getStorageData, getAccountData, + // getCodeData) read the pending-writes maps mutated under the write lock. + s.mu.RLock() + defer s.mu.RUnlock() + if moduleName != keys.EVMStoreKey { return -1, false, fmt.Errorf("block height modified not tracked for module %q", moduleName) } diff --git a/sei-db/state_db/sc/flatkv/store_write.go b/sei-db/state_db/sc/flatkv/store_write.go index 8d495b6bb2..5bd6c1bcb2 100644 --- a/sei-db/state_db/sc/flatkv/store_write.go +++ b/sei-db/state_db/sc/flatkv/store_write.go @@ -20,6 +20,16 @@ import ( // On crash, catchup replays WAL to recover incomplete commits. func (s *CommitStore) Commit() (version int64, err error) { start := time.Now() + + // TODO(concurrency): This takes a single coarse write lock for the whole + // commit, so it also blocks readers/iterator construction during the WAL + // fsync and the periodic auto-snapshot. That is fine today because commits + // are not pipelined with reads (there is currently no pipelining at all). + // When commit pipelining is introduced, replace this with a finer-grained + // scheme. + s.mu.Lock() + defer s.mu.Unlock() + pendingAccount := len(s.accountWrites) pendingCode := len(s.codeWrites) pendingStorage := len(s.storageWrites)