diff --git a/sei-db/state_db/sc/flatkv/exporter.go b/sei-db/state_db/sc/flatkv/exporter.go index c57b51070d..60413672e6 100644 --- a/sei-db/state_db/sc/flatkv/exporter.go +++ b/sei-db/state_db/sc/flatkv/exporter.go @@ -89,6 +89,10 @@ func (e *KVExporter) Next() (interface{}, error) { continue } + if isMetaKey(e.currentIter.Key()) { + e.currentIter.Next() + continue + } key := bytes.Clone(e.currentIter.Key()) value := bytes.Clone(e.currentIter.Value()) e.currentIter.Next() @@ -123,10 +127,8 @@ func (e *KVExporter) Close() error { return nil } -// openIterForDB returns an iterator over all user data in the given DB, -// excluding internal metadata. metaKeyLowerBound() returns {0x00, 0x00} which -// skips the single-byte DBLocalMetaKey ({0x00}) while including all user keys -// (minimum 20 bytes for an EVM address). +// openIterForDB returns an iterator over all user data in the given DB. +// Metadata keys are filtered out by isMetaKey() in the iteration loop. func (e *KVExporter) openIterForDB(db exportDBKind) (dbtypes.KeyValueDBIterator, error) { var kvDB dbtypes.KeyValueDB switch db { @@ -144,9 +146,7 @@ func (e *KVExporter) openIterForDB(db exportDBKind) (dbtypes.KeyValueDBIterator, if kvDB == nil { return nil, nil } - return kvDB.NewIter(&dbtypes.IterOptions{ - LowerBound: metaKeyLowerBound(), - }) + return kvDB.NewIter(&dbtypes.IterOptions{}) } func (e *KVExporter) convertToNodes(db exportDBKind, key, value []byte) ([]*types.SnapshotNode, error) { diff --git a/sei-db/state_db/sc/flatkv/iterator.go b/sei-db/state_db/sc/flatkv/iterator.go index eeaa6ab121..6ea8557c86 100644 --- a/sei-db/state_db/sc/flatkv/iterator.go +++ b/sei-db/state_db/sc/flatkv/iterator.go @@ -1,7 +1,6 @@ package flatkv import ( - "bytes" "fmt" "github.com/sei-protocol/sei-chain/sei-db/common/evm" @@ -54,11 +53,6 @@ func newDBIterator(db types.KeyValueDB, kind evm.EVMKeyKind, start, end []byte) return &emptyIterator{} } - // Exclude metadata key (0x00) - if internalStart == nil { - internalStart = metaKeyLowerBound() - } - iter, err := db.NewIter(&types.IterOptions{ LowerBound: internalStart, UpperBound: internalEnd, @@ -79,11 +73,6 @@ func newDBIterator(db types.KeyValueDB, kind evm.EVMKeyKind, start, end []byte) func newDBPrefixIterator(db types.KeyValueDB, kind evm.EVMKeyKind, internalPrefix []byte, externalPrefix []byte) Iterator { internalEnd := PrefixEnd(internalPrefix) - // Exclude metadata key (0x00) - if internalPrefix == nil || bytes.Compare(internalPrefix, metaKeyLowerBound()) < 0 { - internalPrefix = metaKeyLowerBound() - } - iter, err := db.NewIter(&types.IterOptions{ LowerBound: internalPrefix, UpperBound: internalEnd, @@ -132,14 +121,22 @@ func (it *dbIterator) First() bool { if it.closed { return false } - return it.iter.First() + if !it.iter.First() { + return false + } + it.skipMetaForward() + return it.iter.Valid() } func (it *dbIterator) Last() bool { if it.closed { return false } - return it.iter.Last() + if !it.iter.Last() { + return false + } + it.skipMetaBackward() + return it.iter.Valid() } func (it *dbIterator) SeekGE(key []byte) bool { @@ -153,7 +150,11 @@ func (it *dbIterator) SeekGE(key []byte) bool { return false } - return it.iter.SeekGE(internalKey) + if !it.iter.SeekGE(internalKey) { + return false + } + it.skipMetaForward() + return it.iter.Valid() } func (it *dbIterator) SeekLT(key []byte) bool { @@ -167,21 +168,50 @@ func (it *dbIterator) SeekLT(key []byte) bool { return false } - return it.iter.SeekLT(internalKey) + if !it.iter.SeekLT(internalKey) { + return false + } + it.skipMetaBackward() + return it.iter.Valid() } func (it *dbIterator) Next() bool { if it.closed { return false } - return it.iter.Next() + if !it.iter.Next() { + return false + } + it.skipMetaForward() + return it.iter.Valid() } func (it *dbIterator) Prev() bool { if it.closed { return false } - return it.iter.Prev() + if !it.iter.Prev() { + return false + } + it.skipMetaBackward() + return it.iter.Valid() +} + +// skipMetaForward advances past any _meta/ keys. +// On I/O error Valid() becomes false and the loop exits; +// the caller surfaces the error via Error(). +func (it *dbIterator) skipMetaForward() { + for it.iter.Valid() && isMetaKey(it.iter.Key()) { + it.iter.Next() + } +} + +// skipMetaBackward retreats past any _meta/ keys. +// Error handling mirrors skipMetaForward. +func (it *dbIterator) skipMetaBackward() { + for it.iter.Valid() && isMetaKey(it.iter.Key()) { + it.iter.Prev() + } } func (it *dbIterator) Key() []byte { diff --git a/sei-db/state_db/sc/flatkv/keys.go b/sei-db/state_db/sc/flatkv/keys.go index b5ef53963d..070ca104f2 100644 --- a/sei-db/state_db/sc/flatkv/keys.go +++ b/sei-db/state_db/sc/flatkv/keys.go @@ -4,21 +4,32 @@ import ( "bytes" "encoding/binary" "fmt" + + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash" ) -// DBLocalMetaKey is the key for per-DB local metadata. -// It is a single-byte key (0x00), which cannot collide with any valid user key -// because all user keys have minimum length of 20 bytes (EVM address). -// -// Invariant: All user keys are >= 20 bytes (address=20, storage=52). -var DBLocalMetaKey = []byte{0x00} +const metaKeyPrefix = "_meta/" + +const ( + metaVersion = metaKeyPrefix + "version" + metaLtHash = metaKeyPrefix + "hash" +) + +var ( + metaKeyPrefixBytes = []byte(metaKeyPrefix) + metaVersionKey = []byte(metaVersion) + metaLtHashKey = []byte(metaLtHash) +) -// metaKeyLowerBound returns the iterator lower bound that excludes DBLocalMetaKey. -// Lexicographically: 0x00 (1 byte) < 0x00,0x00 (2 bytes) < any user key (>=20 bytes). -// This ensures metadata key is excluded while all user keys (even those starting -// with 0x00) are included. -func metaKeyLowerBound() []byte { - return []byte{0x00, 0x00} +// isMetaKey reports whether key is a per-DB internal metadata key (not user data). +// +// Safety: _meta/ keys are 10–13 bytes; the shortest user key is 20 bytes +// (an EVM address). Prefix collision would require an address starting with +// 0x5F6D657461 ("_meta") — probability ~2^-48 for random addresses and +// negligible even under CREATE2 brute-force. Legacy DB keys must not use +// the _meta/ prefix. +func isMetaKey(key []byte) bool { + return bytes.HasPrefix(key, metaKeyPrefixBytes) } const ( @@ -27,32 +38,13 @@ const ( SlotLen = 32 BalanceLen = 32 NonceLen = 8 - - // localMetaSize is the serialized size of LocalMeta (version = 8 bytes) - localMetaSize = 8 ) // LocalMeta stores per-DB version tracking metadata. -// Stored inside each DB at DBLocalMetaKey (0x00). +// Version is stored at _meta/version, LtHash at _meta/hash. type LocalMeta struct { - CommittedVersion int64 // Current committed version in this DB -} - -// MarshalLocalMeta encodes LocalMeta as fixed 8 bytes (big-endian). -func MarshalLocalMeta(m *LocalMeta) []byte { - buf := make([]byte, localMetaSize) - binary.BigEndian.PutUint64(buf, uint64(m.CommittedVersion)) //nolint:gosec // version is always non-negative - return buf -} - -// UnmarshalLocalMeta decodes LocalMeta from bytes. -func UnmarshalLocalMeta(data []byte) (*LocalMeta, error) { - if len(data) != localMetaSize { - return nil, fmt.Errorf("invalid LocalMeta size: got %d, want %d", len(data), localMetaSize) - } - return &LocalMeta{ - CommittedVersion: int64(binary.BigEndian.Uint64(data)), //nolint:gosec // version won't exceed int64 max - }, nil + CommittedVersion int64 // Current committed version in this DB + LtHash *lthash.LtHash // nil for old format (version-only) } // Address is an EVM address (20 bytes). diff --git a/sei-db/state_db/sc/flatkv/keys_test.go b/sei-db/state_db/sc/flatkv/keys_test.go index b4dc64368b..ad81d5360d 100644 --- a/sei-db/state_db/sc/flatkv/keys_test.go +++ b/sei-db/state_db/sc/flatkv/keys_test.go @@ -237,57 +237,11 @@ func TestFlatKVTypeConversions(t *testing.T) { }) } -func TestLocalMetaSerialization(t *testing.T) { - t.Run("RoundTripZero", func(t *testing.T) { - original := &LocalMeta{CommittedVersion: 0} - encoded := MarshalLocalMeta(original) - require.Equal(t, localMetaSize, len(encoded)) - - decoded, err := UnmarshalLocalMeta(encoded) - require.NoError(t, err) - require.Equal(t, original.CommittedVersion, decoded.CommittedVersion) - }) - - t.Run("RoundTripPositive", func(t *testing.T) { - original := &LocalMeta{CommittedVersion: 12345} - encoded := MarshalLocalMeta(original) - require.Equal(t, localMetaSize, len(encoded)) - - decoded, err := UnmarshalLocalMeta(encoded) - require.NoError(t, err) - require.Equal(t, original.CommittedVersion, decoded.CommittedVersion) - }) - - t.Run("RoundTripMaxInt64", func(t *testing.T) { - original := &LocalMeta{CommittedVersion: math.MaxInt64} - encoded := MarshalLocalMeta(original) - require.Equal(t, localMetaSize, len(encoded)) - - decoded, err := UnmarshalLocalMeta(encoded) - require.NoError(t, err) - require.Equal(t, original.CommittedVersion, decoded.CommittedVersion) - }) - - t.Run("InvalidLength", func(t *testing.T) { - // Too short - _, err := UnmarshalLocalMeta([]byte{0x00}) - require.Error(t, err) - require.Contains(t, err.Error(), "invalid LocalMeta size") - - // Too long - _, err = UnmarshalLocalMeta(make([]byte, localMetaSize+1)) - require.Error(t, err) - require.Contains(t, err.Error(), "invalid LocalMeta size") - }) - - t.Run("BigEndianEncoding", func(t *testing.T) { - // Verify big-endian encoding: version 0x0102030405060708 - meta := &LocalMeta{CommittedVersion: 0x0102030405060708} - encoded := MarshalLocalMeta(meta) - - // Big-endian: most significant byte first - require.Equal(t, byte(0x01), encoded[0]) - require.Equal(t, byte(0x02), encoded[1]) - require.Equal(t, byte(0x08), encoded[7]) - }) +func TestIsMetaKey(t *testing.T) { + require.True(t, isMetaKey(metaVersionKey)) + require.True(t, isMetaKey(metaLtHashKey)) + require.True(t, isMetaKey([]byte("_meta/future"))) + require.False(t, isMetaKey([]byte{0x00})) + require.False(t, isMetaKey(AccountKey(Address{0x01}))) + require.False(t, isMetaKey(StorageKey(Address{0x01}, Slot{0x02}))) } 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 96dd9aa719..1cf7e0fd3c 100644 --- a/sei-db/state_db/sc/flatkv/lthash_correctness_test.go +++ b/sei-db/state_db/sc/flatkv/lthash_correctness_test.go @@ -22,12 +22,13 @@ func fullScanLtHash(t *testing.T, s *CommitStore) *lthash.LtHash { var pairs []lthash.KVPairWithLastValue scanDB := func(db types.KeyValueDB) { - iter, err := db.NewIter(&types.IterOptions{ - LowerBound: metaKeyLowerBound(), - }) + iter, err := db.NewIter(&types.IterOptions{}) require.NoError(t, err) defer iter.Close() for iter.First(); iter.Valid(); iter.Next() { + if isMetaKey(iter.Key()) { + continue + } key := bytes.Clone(iter.Key()) value := bytes.Clone(iter.Value()) pairs = append(pairs, lthash.KVPairWithLastValue{ diff --git a/sei-db/state_db/sc/flatkv/perdb_lthash_test.go b/sei-db/state_db/sc/flatkv/perdb_lthash_test.go new file mode 100644 index 0000000000..6d22b82a16 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/perdb_lthash_test.go @@ -0,0 +1,415 @@ +package flatkv + +import ( + "bytes" + "path/filepath" + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/db_engine/pebbledb" + "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash" + scTypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" + iavl "github.com/sei-protocol/sei-chain/sei-iavl/proto" + "github.com/stretchr/testify/require" +) + +// testFullScanDBLtHash computes the LtHash of a single data DB by iterating +// all KV pairs (excluding _meta/ metadata keys). Test-only helper. +func testFullScanDBLtHash(t *testing.T, db types.KeyValueDB) *lthash.LtHash { + t.Helper() + iter, err := db.NewIter(&types.IterOptions{}) + require.NoError(t, err) + defer iter.Close() + + var pairs []lthash.KVPairWithLastValue + for iter.First(); iter.Valid(); iter.Next() { + if isMetaKey(iter.Key()) { + continue + } + pairs = append(pairs, lthash.KVPairWithLastValue{ + Key: bytes.Clone(iter.Key()), + Value: bytes.Clone(iter.Value()), + }) + } + require.NoError(t, iter.Error()) + result, _ := lthash.ComputeLtHash(nil, pairs) + if result == nil { + return lthash.New() + } + return result +} + +// fullScanPerDBLtHash computes LtHash for each data DB individually via full scan. +func fullScanPerDBLtHash(t *testing.T, s *CommitStore) map[string]*lthash.LtHash { + t.Helper() + result := make(map[string]*lthash.LtHash, 4) + for dbDir, db := range map[string]types.KeyValueDB{ + accountDBDir: s.accountDB, + codeDBDir: s.codeDB, + storageDBDir: s.storageDB, + legacyDBDir: s.legacyDB, + } { + result[dbDir] = testFullScanDBLtHash(t, db) + } + return result +} + +// verifyPerDBLtHash checks that the in-memory per-DB working hashes +// match a full scan of each respective database. +func verifyPerDBLtHash(t *testing.T, s *CommitStore) { + t.Helper() + scanned := fullScanPerDBLtHash(t, s) + for dbDir, scanHash := range scanned { + require.True(t, s.perDBWorkingLtHash[dbDir].Equal(scanHash), + "per-DB LtHash mismatch for %s:\n working: %x\n fullscan: %x", + dbDir, s.perDBWorkingLtHash[dbDir].Checksum(), scanHash.Checksum()) + } +} + +// commitMixedState applies changesets with data across all 4 DB types. +// round must be in [0, 255] since it is used as a byte to derive unique addresses/slots. +func commitMixedState(t *testing.T, s *CommitStore, round byte) { + t.Helper() + addr := addrN(round) + slot := slotN(round) + legacyKey := append([]byte{0x09}, addr[:]...) + + cs1 := namedCS( + noncePair(addr, uint64(round)), + codeHashPair(addr, codeHashN(round)), + codePair(addr, []byte{0x60, 0x80, round}), + storagePair(addr, slot, []byte{round, 0xAA}), + ) + cs2 := makeChangeSet(legacyKey, []byte{round, 0xBB}, false) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1, cs2})) + _, err := s.Commit() + require.NoError(t, err) +} + +// Test: Crash recovery where metadataDB is behind data DBs. +// Simulates a crash after commitBatches (step 2) but before +// commitGlobalMetadata (step 4) by rolling back metadataDB's +// global version. Data DBs and their LocalMeta remain at v2. +func TestPerDBLtHashSkewRecovery(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + s1 := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err := s1.LoadVersion(0, false) + require.NoError(t, err) + + commitMixedState(t, s1, 1) + commitMixedState(t, s1, 2) + verifyPerDBLtHash(t, s1) + require.NoError(t, s1.Close()) + + // Roll back metadataDB global version to 1 to simulate crash + // after commitBatches completed but before commitGlobalMetadata. + snapDir, _, err := currentSnapshotDir(dbDir) + require.NoError(t, err) + + metaDBPath := filepath.Join(snapDir, metadataDir) + db, err := pebbledb.Open(t.Context(), metaDBPath, types.OpenOptions{}, false) + require.NoError(t, err) + require.NoError(t, db.Set(metaVersionKey, versionToBytes(1), types.WriteOptions{Sync: true})) + require.NoError(t, db.Close()) + + // Reopen -- catchup should replay version 2 from WAL + s2 := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err = s2.LoadVersion(0, false) + require.NoError(t, err) + defer s2.Close() + + require.Equal(t, int64(2), s2.Version()) + verifyPerDBLtHash(t, s2) + verifyLtHashAtHeight(t, s2, 2) +} + +// Test: Per-DB full scan verification after restart. +func TestPerDBLtHashPersistenceAfterReopen(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + s1 := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err := s1.LoadVersion(0, false) + require.NoError(t, err) + + for i := byte(1); i <= 10; i++ { + commitMixedState(t, s1, i) + } + verifyPerDBLtHash(t, s1) + require.NoError(t, s1.Close()) + + // Reopen and verify + s2 := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err = s2.LoadVersion(0, false) + require.NoError(t, err) + defer s2.Close() + + require.Equal(t, int64(10), s2.Version()) + verifyPerDBLtHash(t, s2) + verifyLtHashAtHeight(t, s2, 10) + + for _, dbDir := range dataDBDirs { + wh := s2.perDBWorkingLtHash[dbDir] + meta := s2.localMeta[dbDir] + require.NotNil(t, meta.LtHash, + "LocalMeta LtHash should be loaded for %s", dbDir) + require.True(t, wh.Equal(meta.LtHash), + "per-DB working hash should match LocalMeta LtHash on open for %s", dbDir) + } +} + +// Test: Verify per-DB LTHash alongside global in the incremental multi-block test. +func TestPerDBLtHashIncrementalEqualsFullScan(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + for i := 1; i <= 10; i++ { + addr := addrN(byte(i)) + slot := slotN(byte(i)) + legacyKey := append([]byte{0x09}, addr[:]...) + + cs1 := namedCS( + noncePair(addr, uint64(i)), + storagePair(addr, slot, []byte{byte(i), 0xAA}), + ) + cs2 := makeChangeSet(legacyKey, []byte{byte(i)}, false) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1, cs2})) + commitAndCheck(t, s) + } + verifyPerDBLtHash(t, s) + verifyLtHashAtHeight(t, s, 10) + + for i := 11; i <= 15; i++ { + addr := addrN(byte(i - 10)) + ch := codeHashN(byte(i)) + cs := namedCS( + codeHashPair(addr, ch), + codePair(addr, []byte{0x60, 0x80, byte(i)}), + ) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + commitAndCheck(t, s) + } + verifyPerDBLtHash(t, s) + + for i := 16; i <= 18; i++ { + addr := addrN(byte(i - 15)) + slot := slotN(byte(i - 15)) + cs := namedCS(storagePair(addr, slot, []byte{byte(i), 0xBB})) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + commitAndCheck(t, s) + } + for i := 19; i <= 20; i++ { + addr := addrN(byte(i - 15)) + slot := slotN(byte(i - 15)) + cs := namedCS(storageDeletePair(addr, slot)) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + commitAndCheck(t, s) + } + verifyPerDBLtHash(t, s) + verifyLtHashAtHeight(t, s, 20) +} + +// Test: sum of per-DB hashes equals global hash (homomorphic property). +func TestPerDBLtHashSumEqualsGlobal(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + for i := byte(1); i <= 5; i++ { + commitMixedState(t, s, i) + } + + sumHash := lthash.New() + for _, dbDir := range []string{accountDBDir, codeDBDir, storageDBDir, legacyDBDir} { + sumHash.MixIn(s.perDBWorkingLtHash[dbDir]) + } + + require.True(t, s.workingLtHash.Equal(sumHash), + "sum of per-DB LtHashes should equal global LtHash:\n global: %x\n sum: %x", + s.workingLtHash.Checksum(), sumHash.Checksum()) +} + +// Test: per-DB hashes are correct after catchup with WAL replay. +func TestPerDBLtHashCatchupReplay(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + s1 := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err := s1.LoadVersion(0, false) + require.NoError(t, err) + + commitMixedState(t, s1, 1) + commitMixedState(t, s1, 2) + require.NoError(t, s1.WriteSnapshot("")) + + commitMixedState(t, s1, 3) + commitMixedState(t, s1, 4) + commitMixedState(t, s1, 5) + verifyPerDBLtHash(t, s1) + + expectedPerDB := make(map[string][32]byte, 4) + for dbDir, h := range s1.perDBWorkingLtHash { + expectedPerDB[dbDir] = h.Checksum() + } + require.NoError(t, s1.Close()) + + s2 := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err = s2.LoadVersion(0, false) + require.NoError(t, err) + defer s2.Close() + + require.Equal(t, int64(5), s2.Version()) + for dbDir, expectedCS := range expectedPerDB { + actualCS := s2.perDBWorkingLtHash[dbDir].Checksum() + require.Equal(t, expectedCS, actualCS, + "per-DB LtHash mismatch for %s after catchup", dbDir) + } + verifyPerDBLtHash(t, s2) +} + +// Test: per-DB LtHash with empty blocks doesn't drift. +func TestPerDBLtHashEmptyBlocks(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + commitMixedState(t, s, 1) + checksums := make(map[string][32]byte) + for dbDir, h := range s.perDBWorkingLtHash { + checksums[dbDir] = h.Checksum() + } + + for i := 0; i < 5; i++ { + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{namedCS()})) + commitAndCheck(t, s) + } + + for dbDir, expected := range checksums { + actual := s.perDBWorkingLtHash[dbDir].Checksum() + require.Equal(t, expected, actual, + "empty blocks should not change per-DB LtHash for %s", dbDir) + } +} + +// Test: per-DB hashes after import via Importer. +func TestPerDBLtHashAfterImport(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + s := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err := s.LoadVersion(0, false) + require.NoError(t, err) + + imp, err := s.Importer(1) + require.NoError(t, err) + + for i := byte(1); i <= 5; i++ { + addr := addrN(i) + slot := slotN(i) + sp := storagePair(addr, slot, []byte{i, 0xAA}) + np := noncePair(addr, uint64(i)) + imp.AddNode(&scTypes.SnapshotNode{Key: sp.Key, Value: sp.Value}) + imp.AddNode(&scTypes.SnapshotNode{Key: np.Key, Value: np.Value}) + } + require.NoError(t, imp.Close()) + + verifyPerDBLtHash(t, s) + verifyLtHashAtHeight(t, s, 1) + + for _, dbDir := range dataDBDirs { + wh := s.perDBWorkingLtHash[dbDir] + meta := s.localMeta[dbDir] + require.NotNil(t, meta.LtHash, + "LocalMeta LtHash should exist after import for %s", dbDir) + require.True(t, wh.Equal(meta.LtHash), + "per-DB working hash should match LocalMeta LtHash after import for %s", dbDir) + } + require.NoError(t, s.Close()) +} + +// Test: per-DB hashes survive rollback. +func TestPerDBLtHashRollback(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + s := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err := s.LoadVersion(0, false) + require.NoError(t, err) + + commitMixedState(t, s, 1) + commitMixedState(t, s, 2) + commitMixedState(t, s, 3) + require.NoError(t, s.WriteSnapshot("")) + + commitMixedState(t, s, 4) + commitMixedState(t, s, 5) + + require.NoError(t, s.Rollback(3)) + require.Equal(t, int64(3), s.Version()) + verifyPerDBLtHash(t, s) + verifyLtHashAtHeight(t, s, 3) + + require.NoError(t, s.Close()) +} + +// Test: per-DB LtHashes are persisted in each DB's LocalMeta after normal commit cycle. +func TestPerDBLtHashPersistedInLocalMeta(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + s := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err := s.LoadVersion(0, false) + require.NoError(t, err) + + commitMixedState(t, s, 1) + commitMixedState(t, s, 2) + + dbInstances := map[string]types.KeyValueDB{ + accountDBDir: s.accountDB, + codeDBDir: s.codeDB, + storageDBDir: s.storageDB, + legacyDBDir: s.legacyDB, + } + for _, dbDirName := range dataDBDirs { + db := dbInstances[dbDirName] + meta, err := loadLocalMeta(db) + require.NoError(t, err, "LocalMeta should be readable for %s", dbDirName) + require.NotNil(t, meta.LtHash, + "LocalMeta LtHash should be non-nil for %s", dbDirName) + require.True(t, s.perDBWorkingLtHash[dbDirName].Equal(meta.LtHash), + "LocalMeta LtHash should match working hash for %s", dbDirName) + } + + require.NoError(t, s.Close()) +} + +func TestPerDBLtHashAfterDirectImport(t *testing.T) { + dir := t.TempDir() + dbDir := filepath.Join(dir, flatkvRootDir) + + s := NewCommitStore(t.Context(), dbDir, DefaultConfig()) + _, err := s.LoadVersion(0, false) + require.NoError(t, err) + + var pairs []*iavl.KVPair + for i := byte(1); i <= 10; i++ { + addr := addrN(i) + slot := slotN(i) + pairs = append(pairs, + storagePair(addr, slot, []byte{i, 0xAA}), + noncePair(addr, uint64(i)), + ) + } + + cs := &proto.NamedChangeSet{ + Name: "evm", + Changeset: iavl.ChangeSet{Pairs: pairs}, + } + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + commitAndCheck(t, s) + + verifyPerDBLtHash(t, s) + verifyLtHashAtHeight(t, s, 1) + require.NoError(t, s.Close()) +} diff --git a/sei-db/state_db/sc/flatkv/snapshot.go b/sei-db/state_db/sc/flatkv/snapshot.go index 93f0a328e3..d95e2b6453 100644 --- a/sei-db/state_db/sc/flatkv/snapshot.go +++ b/sei-db/state_db/sc/flatkv/snapshot.go @@ -380,7 +380,7 @@ func (s *CommitStore) migrateFlatLayout(flatkvDir string) (string, error) { var version int64 metaPath := filepath.Join(flatkvDir, metadataDir) if tmpMeta, err := pebbledb.Open(s.ctx, metaPath, types.OpenOptions{}, s.config.EnablePebbleMetrics); err == nil { - verData, verErr := tmpMeta.Get([]byte(MetaGlobalVersion)) + verData, verErr := tmpMeta.Get(metaVersionKey) _ = tmpMeta.Close() if verErr == nil && len(verData) == 8 { version = int64(binary.BigEndian.Uint64(verData)) //nolint:gosec // block height, always < MaxInt64 diff --git a/sei-db/state_db/sc/flatkv/snapshot_test.go b/sei-db/state_db/sc/flatkv/snapshot_test.go index 6178490b46..c795084162 100644 --- a/sei-db/state_db/sc/flatkv/snapshot_test.go +++ b/sei-db/state_db/sc/flatkv/snapshot_test.go @@ -314,8 +314,7 @@ func TestOpenVersionValidation(t *testing.T) { accountDBPath := filepath.Join(snapDir, accountDBDir) db, err := pebbledb.Open(t.Context(), accountDBPath, types.OpenOptions{}, false) require.NoError(t, err) - lagMeta := &LocalMeta{CommittedVersion: 1} - require.NoError(t, db.Set(DBLocalMetaKey, MarshalLocalMeta(lagMeta), types.WriteOptions{Sync: true})) + require.NoError(t, db.Set(metaVersionKey, versionToBytes(1), types.WriteOptions{Sync: true})) require.NoError(t, db.Close()) // Phase 3: reopen - should detect skew and catchup @@ -1399,13 +1398,13 @@ func TestGlobalMetadataCorruption(t *testing.T) { workingMeta := filepath.Join(dbDir, "working", metadataDir) db, err := pebbledb.Open(context.Background(), workingMeta, types.OpenOptions{}, false) require.NoError(t, err) - require.NoError(t, db.Set([]byte(MetaGlobalVersion), []byte{0xFF, 0xFF, 0xFF}, types.WriteOptions{Sync: true})) + require.NoError(t, db.Set(metaVersionKey, []byte{0xFF, 0xFF, 0xFF}, types.WriteOptions{Sync: true})) require.NoError(t, db.Close()) snapMeta := filepath.Join(dbDir, snapshotName(1), metadataDir) db2, err := pebbledb.Open(context.Background(), snapMeta, types.OpenOptions{}, false) require.NoError(t, err) - require.NoError(t, db2.Set([]byte(MetaGlobalVersion), []byte{0xFF, 0xFF, 0xFF}, types.WriteOptions{Sync: true})) + require.NoError(t, db2.Set(metaVersionKey, []byte{0xFF, 0xFF, 0xFF}, types.WriteOptions{Sync: true})) require.NoError(t, db2.Close()) _ = os.Remove(filepath.Join(dbDir, "working", snapshotBaseFile)) @@ -1461,18 +1460,18 @@ func TestLocalMetaCorruption(t *testing.T) { require.NoError(t, s.WriteSnapshot("")) require.NoError(t, s.Close()) - // Corrupt accountDB LocalMeta in working dir: write 3 garbage bytes (expected 8). + // Corrupt accountDB meta version in working dir: write 3 garbage bytes (expected 8). workingAccount := filepath.Join(dbDir, "working", accountDBDir) db, err := pebbledb.Open(context.Background(), workingAccount, types.OpenOptions{}, false) require.NoError(t, err) - require.NoError(t, db.Set(DBLocalMetaKey, []byte{0xDE, 0xAD, 0xFF}, types.WriteOptions{Sync: true})) + require.NoError(t, db.Set(metaVersionKey, []byte{0xDE, 0xAD, 0xFF}, types.WriteOptions{Sync: true})) require.NoError(t, db.Close()) // Same corruption in the snapshot dir. snapAccount := filepath.Join(dbDir, snapshotName(1), accountDBDir) db2, err := pebbledb.Open(context.Background(), snapAccount, types.OpenOptions{}, false) require.NoError(t, err) - require.NoError(t, db2.Set(DBLocalMetaKey, []byte{0xDE, 0xAD, 0xFF}, types.WriteOptions{Sync: true})) + require.NoError(t, db2.Set(metaVersionKey, []byte{0xDE, 0xAD, 0xFF}, types.WriteOptions{Sync: true})) require.NoError(t, db2.Close()) // Remove SNAPSHOT_BASE to force re-clone from corrupted snapshot. @@ -1480,8 +1479,8 @@ func TestLocalMetaCorruption(t *testing.T) { s2 := NewCommitStore(context.Background(), dbDir, DefaultConfig()) _, err = s2.LoadVersion(0, false) - require.Error(t, err, "open should fail when LocalMeta is corrupted (invalid size)") - require.Contains(t, err.Error(), "invalid LocalMeta size") + require.Error(t, err, "open should fail when meta version is corrupted") + require.Contains(t, err.Error(), "invalid meta version length") } // TestWALSegmentCorruption simulates WAL data loss caused by segment corruption. @@ -1505,9 +1504,7 @@ func TestWALSegmentCorruption(t *testing.T) { workingMeta := filepath.Join(dbDir, "working", metadataDir) mdb, err := pebbledb.Open(context.Background(), workingMeta, types.OpenOptions{}, false) require.NoError(t, err) - versionBuf := make([]byte, 8) - versionBuf[7] = 1 // version = 1 - require.NoError(t, mdb.Set([]byte(MetaGlobalVersion), versionBuf, types.WriteOptions{Sync: true})) + require.NoError(t, mdb.Set(metaVersionKey, versionToBytes(1), types.WriteOptions{Sync: true})) require.NoError(t, mdb.Close()) // Corrupt WAL segments: tidwall/wal will auto-truncate, losing all entries. diff --git a/sei-db/state_db/sc/flatkv/store.go b/sei-db/state_db/sc/flatkv/store.go index e37cbc8437..c14e0a86b1 100644 --- a/sei-db/state_db/sc/flatkv/store.go +++ b/sei-db/state_db/sc/flatkv/store.go @@ -42,13 +42,12 @@ const ( readOnlyDirPrefix = "readonly-" - // Metadata DB keys - MetaGlobalVersion = "_meta/version" // Global committed version watermark (8 bytes) - MetaGlobalLtHash = "_meta/hash" // Global LtHash (2048 bytes) - flatkvMeterName = "seidb_flatkv" ) +// dataDBDirs lists all data DB directory names (used for per-DB LtHash iteration). +var dataDBDirs = []string{accountDBDir, codeDBDir, storageDBDir, legacyDBDir} + // pendingKVWrite tracks a buffered key-value write for code/storage DBs. type pendingKVWrite struct { key []byte // Internal DB key @@ -89,6 +88,11 @@ type CommitStore struct { committedLtHash *lthash.LtHash workingLtHash *lthash.LtHash + // Per-DB working LTHash tracking. Authoritative copies live in each + // DB's LocalMeta (atomically committed with data). On startup the + // working hashes are loaded from LocalMeta. + perDBWorkingLtHash map[string]*lthash.LtHash + // Pending writes buffer // accountWrites: key = address string (20 bytes), value = AccountValue // codeWrites/storageWrites/legacyWrites: key = internal DB key string, value = raw bytes @@ -123,18 +127,19 @@ func NewCommitStore( meter := otel.Meter(flatkvMeterName) return &CommitStore{ - ctx: ctx, - config: cfg, - dbDir: dbDir, - localMeta: make(map[string]*LocalMeta), - accountWrites: make(map[string]*pendingAccountWrite), - codeWrites: make(map[string]*pendingKVWrite), - storageWrites: make(map[string]*pendingKVWrite), - legacyWrites: make(map[string]*pendingKVWrite), - pendingChangeSets: make([]*proto.NamedChangeSet, 0), - committedLtHash: lthash.New(), - workingLtHash: lthash.New(), - phaseTimer: metrics.NewPhaseTimer(meter, "seidb_main_thread"), + ctx: ctx, + config: cfg, + dbDir: dbDir, + localMeta: make(map[string]*LocalMeta), + accountWrites: make(map[string]*pendingAccountWrite), + codeWrites: make(map[string]*pendingKVWrite), + storageWrites: make(map[string]*pendingKVWrite), + legacyWrites: make(map[string]*pendingKVWrite), + pendingChangeSets: make([]*proto.NamedChangeSet, 0), + committedLtHash: lthash.New(), + workingLtHash: lthash.New(), + perDBWorkingLtHash: newPerDBLtHashMap(), + phaseTimer: metrics.NewPhaseTimer(meter, "seidb_main_thread"), } } @@ -497,6 +502,26 @@ func (s *CommitStore) loadGlobalMetadata() error { s.committedLtHash = lthash.New() s.workingLtHash = lthash.New() } + + // Load per-DB LtHashes from each DB's LocalMeta (already loaded in openDBs). + // If any DB's version is behind the global version (partial commit or + // corruption), lower committedVersion so catchup replays from there. + for _, dbDir := range dataDBDirs { + meta := s.localMeta[dbDir] + if meta != nil && meta.LtHash != nil { + s.perDBWorkingLtHash[dbDir] = meta.LtHash.Clone() + } else { + s.perDBWorkingLtHash[dbDir] = lthash.New() + } + if meta != nil && meta.CommittedVersion < s.committedVersion { + logger.Warn("DB LocalMeta version behind global version, will catchup", + "db", dbDir, + "localVersion", meta.CommittedVersion, + "globalVersion", s.committedVersion) + s.committedVersion = meta.CommittedVersion + } + } + return nil } diff --git a/sei-db/state_db/sc/flatkv/store_meta.go b/sei-db/state_db/sc/flatkv/store_meta.go index dc5bd7f185..dcd6966119 100644 --- a/sei-db/state_db/sc/flatkv/store_meta.go +++ b/sei-db/state_db/sc/flatkv/store_meta.go @@ -10,22 +10,66 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash" ) -// loadLocalMeta loads the local metadata from a DB, or returns default if not present. +// versionToBytes encodes a non-negative version as 8-byte big-endian. +// Panics on negative input to catch programming errors early. +// Only called from internal commit/test paths — never with untrusted input. +func versionToBytes(v int64) []byte { + if v < 0 { + panic(fmt.Sprintf("flatkv: negative version %d", v)) + } + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(v)) //nolint:gosec // guarded above + return b +} + +// loadLocalMeta loads per-DB metadata by reading separate keys. func loadLocalMeta(db types.KeyValueDB) (*LocalMeta, error) { - val, err := db.Get(DBLocalMetaKey) + meta := &LocalMeta{} + + versionData, err := db.Get(metaVersionKey) + if err != nil { + if errorutils.IsNotFound(err) { + return &LocalMeta{CommittedVersion: 0}, nil + } + return nil, fmt.Errorf("could not read meta version: %w", err) + } + if len(versionData) != 8 { + return nil, fmt.Errorf("invalid meta version length: got %d, want 8", len(versionData)) + } + meta.CommittedVersion = int64(binary.BigEndian.Uint64(versionData)) //nolint:gosec // version won't exceed int64 max + + hashData, err := db.Get(metaLtHashKey) if err != nil && !errorutils.IsNotFound(err) { - return nil, fmt.Errorf("could not get DBLocalMetaKey: %w", err) + return nil, fmt.Errorf("could not read meta hash: %w", err) } - if errorutils.IsNotFound(err) || val == nil { - return &LocalMeta{CommittedVersion: 0}, nil + if err == nil && hashData != nil { + h, err := lthash.Unmarshal(hashData) + if err != nil { + return nil, fmt.Errorf("unmarshal meta hash: %w", err) + } + meta.LtHash = h } - return UnmarshalLocalMeta(val) + + return meta, nil +} + +// writeLocalMetaToBatch writes per-DB metadata (version + LtHash) as separate keys. +func writeLocalMetaToBatch(batch types.Batch, version int64, ltHash *lthash.LtHash) error { + if err := batch.Set(metaVersionKey, versionToBytes(version)); err != nil { + return fmt.Errorf("set meta version: %w", err) + } + if ltHash != nil { + if err := batch.Set(metaLtHashKey, ltHash.Marshal()); err != nil { + return fmt.Errorf("set meta hash: %w", err) + } + } + return nil } // loadGlobalVersion reads the global committed version from metadata DB. // Returns 0 if not found (fresh start). func (s *CommitStore) loadGlobalVersion() (int64, error) { - data, err := s.metadataDB.Get([]byte(MetaGlobalVersion)) + data, err := s.metadataDB.Get(metaVersionKey) if errorutils.IsNotFound(err) { return 0, nil } @@ -45,7 +89,7 @@ func (s *CommitStore) loadGlobalVersion() (int64, error) { // loadGlobalLtHash reads the global committed LtHash from metadata DB. // Returns nil if not found (fresh start). func (s *CommitStore) loadGlobalLtHash() (*lthash.LtHash, error) { - data, err := s.metadataDB.Get([]byte(MetaGlobalLtHash)) + data, err := s.metadataDB.Get(metaLtHashKey) if errorutils.IsNotFound(err) { return nil, nil } @@ -55,27 +99,28 @@ func (s *CommitStore) loadGlobalLtHash() (*lthash.LtHash, error) { return lthash.Unmarshal(data) } -// commitGlobalMetadata atomically commits global version and LtHash to metadata DB. -// This is the global watermark written AFTER all per-DB commits succeed. +// commitGlobalMetadata atomically commits global version and global LtHash +// to metadata DB. Per-DB LtHashes are stored in each DB's LocalMeta +// (committed atomically with data in commitBatches). func (s *CommitStore) commitGlobalMetadata(version int64, hash *lthash.LtHash) error { batch := s.metadataDB.NewBatch() defer func() { _ = batch.Close() }() - // Encode version (version should always be non-negative in practice) - versionBuf := make([]byte, 8) - binary.BigEndian.PutUint64(versionBuf, uint64(version)) //nolint:gosec // version is always non-negative - - // Write global metadata - if err := batch.Set([]byte(MetaGlobalVersion), versionBuf); err != nil { + if err := batch.Set(metaVersionKey, versionToBytes(version)); err != nil { return fmt.Errorf("failed to set global version: %w", err) } - - lthashBytes := hash.Marshal() - if err := batch.Set([]byte(MetaGlobalLtHash), lthashBytes); err != nil { + if err := batch.Set(metaLtHashKey, hash.Marshal()); err != nil { return fmt.Errorf("failed to set global lthash: %w", err) } - // Atomic commit with fsync return batch.Commit(types.WriteOptions{Sync: s.config.Fsync}) +} +// newPerDBLtHashMap returns a map with a fresh zero LtHash for each data DB. +func newPerDBLtHashMap() map[string]*lthash.LtHash { + m := make(map[string]*lthash.LtHash, len(dataDBDirs)) + for _, dbDir := range dataDBDirs { + m[dbDir] = lthash.New() + } + return m } diff --git a/sei-db/state_db/sc/flatkv/store_meta_test.go b/sei-db/state_db/sc/flatkv/store_meta_test.go index 425e660828..91597da338 100644 --- a/sei-db/state_db/sc/flatkv/store_meta_test.go +++ b/sei-db/state_db/sc/flatkv/store_meta_test.go @@ -2,6 +2,7 @@ package flatkv import ( "context" + "encoding/binary" "path/filepath" "testing" @@ -31,29 +32,24 @@ func TestLoadLocalMeta(t *testing.T) { db := setupTestDB(t) defer db.Close() - // Write metadata - original := &LocalMeta{CommittedVersion: 42} - err := db.Set(DBLocalMetaKey, MarshalLocalMeta(original), types.WriteOptions{}) - require.NoError(t, err) + require.NoError(t, db.Set(metaVersionKey, versionToBytes(42), types.WriteOptions{})) // Load it back loaded, err := loadLocalMeta(db) require.NoError(t, err) - require.Equal(t, original.CommittedVersion, loaded.CommittedVersion) + require.Equal(t, int64(42), loaded.CommittedVersion) + require.Nil(t, loaded.LtHash) }) - t.Run("CorruptedMeta_ReturnsError", func(t *testing.T) { + t.Run("CorruptedVersion_ReturnsError", func(t *testing.T) { db := setupTestDB(t) defer db.Close() - // Write invalid data (wrong size) - err := db.Set(DBLocalMetaKey, []byte{0x01, 0x02}, types.WriteOptions{}) - require.NoError(t, err) + require.NoError(t, db.Set(metaVersionKey, []byte{0x01, 0x02}, types.WriteOptions{})) - // Should fail to load - _, err = loadLocalMeta(db) + _, err := loadLocalMeta(db) require.Error(t, err) - require.Contains(t, err.Error(), "invalid LocalMeta size") + require.Contains(t, err.Error(), "invalid meta version length") }) } @@ -74,11 +70,9 @@ func TestStoreCommitBatchesUpdatesLocalMeta(t *testing.T) { require.Equal(t, int64(1), s.localMeta[storageDBDir].CommittedVersion) // Verify it's persisted in DB - data, err := s.storageDB.Get(DBLocalMetaKey) - require.NoError(t, err) - meta, err := UnmarshalLocalMeta(data) + data, err := s.storageDB.Get(metaVersionKey) require.NoError(t, err) - require.Equal(t, int64(1), meta.CommittedVersion) + require.Equal(t, int64(1), int64(binary.BigEndian.Uint64(data))) } func TestStoreMetadataOperations(t *testing.T) { @@ -144,7 +138,7 @@ func TestStoreMetadataOperations(t *testing.T) { defer s.Close() // Write invalid data (wrong size) - err := s.metadataDB.Set([]byte(MetaGlobalVersion), []byte{0x01}, types.WriteOptions{}) + err := s.metadataDB.Set(metaVersionKey, []byte{0x01}, types.WriteOptions{}) require.NoError(t, err) // Should return error diff --git a/sei-db/state_db/sc/flatkv/store_write.go b/sei-db/state_db/sc/flatkv/store_write.go index 94ed592f61..400f9a1945 100644 --- a/sei-db/state_db/sc/flatkv/store_write.go +++ b/sei-db/state_db/sc/flatkv/store_write.go @@ -215,15 +215,31 @@ func (s *CommitStore) ApplyChangeSets(cs []*proto.NamedChangeSet) error { s.phaseTimer.SetPhase("apply_change_compute_lt_hash") - // Combine all pairs and update working LtHash - allPairs := append(storagePairs, accountPairs...) - allPairs = append(allPairs, codePairs...) - allPairs = append(allPairs, legacyPairs...) - - if len(allPairs) > 0 { - newLtHash, _ := lthash.ComputeLtHash(s.workingLtHash, allPairs) - s.workingLtHash = newLtHash + // Per-DB LTHash updates + type dbPairs struct { + dir string + pairs []lthash.KVPairWithLastValue } + for _, dp := range [4]dbPairs{ + {storageDBDir, storagePairs}, + {accountDBDir, accountPairs}, + {codeDBDir, codePairs}, + {legacyDBDir, legacyPairs}, + } { + if len(dp.pairs) > 0 { + newHash, _ := lthash.ComputeLtHash(s.perDBWorkingLtHash[dp.dir], dp.pairs) + s.perDBWorkingLtHash[dp.dir] = newHash + } + } + + // Global LTHash = sum of per-DB hashes (homomorphic property). + // Compute into a fresh hash and swap to avoid a transient empty state + // on workingLtHash (safe for future pipelining / async callers). + globalHash := lthash.New() + for _, dir := range dataDBDirs { + globalHash.MixIn(s.perDBWorkingLtHash[dir]) + } + s.workingLtHash = globalHash s.phaseTimer.SetPhase("apply_change_done") return nil @@ -345,12 +361,8 @@ func (s *CommitStore) commitBatches(version int64) error { } } - // Update local meta atomically with data (same batch) - newLocalMeta := &LocalMeta{ - CommittedVersion: version, - } - if err := batch.Set(DBLocalMetaKey, MarshalLocalMeta(newLocalMeta)); err != nil { - return fmt.Errorf("accountDB local meta set: %w", err) + if err := writeLocalMetaToBatch(batch, version, s.perDBWorkingLtHash[accountDBDir]); err != nil { + return fmt.Errorf("accountDB local meta: %w", err) } pending = append(pending, pendingCommit{accountDBDir, batch}) } @@ -373,12 +385,8 @@ func (s *CommitStore) commitBatches(version int64) error { } } - // Update local meta atomically with data (same batch) - newLocalMeta := &LocalMeta{ - CommittedVersion: version, - } - if err := batch.Set(DBLocalMetaKey, MarshalLocalMeta(newLocalMeta)); err != nil { - return fmt.Errorf("codeDB local meta set: %w", err) + if err := writeLocalMetaToBatch(batch, version, s.perDBWorkingLtHash[codeDBDir]); err != nil { + return fmt.Errorf("codeDB local meta: %w", err) } pending = append(pending, pendingCommit{codeDBDir, batch}) } @@ -401,12 +409,8 @@ func (s *CommitStore) commitBatches(version int64) error { } } - // Update local meta atomically with data (same batch) - newLocalMeta := &LocalMeta{ - CommittedVersion: version, - } - if err := batch.Set(DBLocalMetaKey, MarshalLocalMeta(newLocalMeta)); err != nil { - return fmt.Errorf("storageDB local meta set: %w", err) + if err := writeLocalMetaToBatch(batch, version, s.perDBWorkingLtHash[storageDBDir]); err != nil { + return fmt.Errorf("storageDB local meta: %w", err) } pending = append(pending, pendingCommit{storageDBDir, batch}) } @@ -429,11 +433,8 @@ func (s *CommitStore) commitBatches(version int64) error { } } - newLocalMeta := &LocalMeta{ - CommittedVersion: version, - } - if err := batch.Set(DBLocalMetaKey, MarshalLocalMeta(newLocalMeta)); err != nil { - return fmt.Errorf("legacyDB local meta set: %w", err) + if err := writeLocalMetaToBatch(batch, version, s.perDBWorkingLtHash[legacyDBDir]); err != nil { + return fmt.Errorf("legacyDB local meta: %w", err) } pending = append(pending, pendingCommit{legacyDBDir, batch}) } @@ -461,10 +462,12 @@ func (s *CommitStore) commitBatches(version int64) error { } } - // Update in-memory local meta after all commits succeed - newLocalMeta := &LocalMeta{CommittedVersion: version} + // Update in-memory local meta after all commits succeed. for _, p := range pending { - s.localMeta[p.dbDir] = newLocalMeta + s.localMeta[p.dbDir] = &LocalMeta{ + CommittedVersion: version, + LtHash: s.perDBWorkingLtHash[p.dbDir].Clone(), + } } return nil } 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 247fc1626a..f075c7568d 100644 --- a/sei-db/state_db/sc/flatkv/store_write_test.go +++ b/sei-db/state_db/sc/flatkv/store_write_test.go @@ -101,11 +101,9 @@ func TestStoreWriteAllDBs(t *testing.T) { "codeDB": s.codeDB, "legacyDB": s.legacyDB, } { - raw, err := db.Get(DBLocalMetaKey) - require.NoError(t, err, "%s LocalMeta read", name) - meta, err := UnmarshalLocalMeta(raw) - require.NoError(t, err) - require.Equal(t, int64(1), meta.CommittedVersion, "%s persisted LocalMeta", name) + raw, err := db.Get(metaVersionKey) + require.NoError(t, err, "%s meta version read", name) + require.Equal(t, int64(1), int64(binary.BigEndian.Uint64(raw)), "%s persisted version", name) } // Verify storage data was written @@ -1274,14 +1272,15 @@ func TestAccountValueEncodingTransition(t *testing.T) { func countLiveEntries(t *testing.T, db types.KeyValueDB) int { t.Helper() - iter, err := db.NewIter(&types.IterOptions{ - LowerBound: metaKeyLowerBound(), - }) + iter, err := db.NewIter(&types.IterOptions{}) require.NoError(t, err) defer iter.Close() count := 0 for iter.First(); iter.Valid(); iter.Next() { + if isMetaKey(iter.Key()) { + continue + } count++ } require.NoError(t, iter.Error())