diff --git a/sei-db/state_db/bench/wrappers/flatkv_wrapper.go b/sei-db/state_db/bench/wrappers/flatkv_wrapper.go index a1e959c58f..7f6a270bec 100644 --- a/sei-db/state_db/bench/wrappers/flatkv_wrapper.go +++ b/sei-db/state_db/bench/wrappers/flatkv_wrapper.go @@ -61,8 +61,8 @@ func (f *flatKVWrapper) Close() error { } func (f *flatKVWrapper) Read(key []byte) (data []byte, found bool, err error) { - data, found = f.base.Get(key) - return data, found, nil + val, ok := f.base.Get(key) + return val, ok, nil } func (f *flatKVWrapper) GetPhaseTimer() *metrics.PhaseTimer { diff --git a/sei-db/state_db/sc/composite/store_test.go b/sei-db/state_db/sc/composite/store_test.go index 025920eb70..3a8c1beddf 100644 --- a/sei-db/state_db/sc/composite/store_test.go +++ b/sei-db/state_db/sc/composite/store_test.go @@ -27,18 +27,27 @@ func (f *failingEVMStore) LoadVersion(int64, bool) (flatkv.Store, error) { func (f *failingEVMStore) ApplyChangeSets([]*proto.NamedChangeSet) error { return nil } func (f *failingEVMStore) Commit() (int64, error) { return 0, nil } func (f *failingEVMStore) Get([]byte) ([]byte, bool) { return nil, false } -func (f *failingEVMStore) Has([]byte) bool { return false } -func (f *failingEVMStore) Iterator(_, _ []byte) flatkv.Iterator { return nil } -func (f *failingEVMStore) IteratorByPrefix([]byte) flatkv.Iterator { return nil } -func (f *failingEVMStore) RootHash() []byte { return nil } -func (f *failingEVMStore) Version() int64 { return 0 } -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) Close() error { return nil } +func (f *failingEVMStore) GetBlockHeightModified([]byte) (int64, bool, error) { + return -1, false, nil +} +func (f *failingEVMStore) Has([]byte) bool { return false } +func (f *failingEVMStore) Iterator(_, _ []byte) flatkv.Iterator { return nil } +func (f *failingEVMStore) IteratorByPrefix([]byte) flatkv.Iterator { return nil } +func (f *failingEVMStore) RootHash() []byte { return nil } +func (f *failingEVMStore) Version() int64 { return 0 } +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) Close() error { return nil } + +func padLeft32(val ...byte) []byte { + var b [32]byte + copy(b[32-len(val):], val) + return b[:] +} func TestCompositeStoreBasicOperations(t *testing.T) { dir := t.TempDir() @@ -201,7 +210,7 @@ func TestLatticeHashCommitInfo(t *testing.T) { Name: EVMStoreName, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ - {Key: evmStorageKey, Value: []byte{round}}, + {Key: evmStorageKey, Value: padLeft32(round)}, }, }, }, @@ -509,7 +518,7 @@ func TestExportImportSplitWrite(t *testing.T) { slot := flatkv.Slot{0xBB} storageKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, flatkv.StorageKey(addr, slot)) - storageVal := []byte{0x42} + storageVal := padLeft32(0x42) nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) nonceVal := []byte{0, 0, 0, 0, 0, 0, 0, 10} @@ -699,7 +708,7 @@ func TestReconcileVersionsAfterCrash(t *testing.T) { Name: EVMStoreName, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ - {Key: storageKey, Value: []byte{i}}, + {Key: storageKey, Value: padLeft32(i)}, }, }, }, @@ -766,7 +775,7 @@ func TestReconcileVersionsThenContinueCommitting(t *testing.T) { {Key: []byte("bal"), Value: []byte{i}}, }}}, {Name: EVMStoreName, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ - {Key: storageKey, Value: []byte{i}}, + {Key: storageKey, Value: padLeft32(i)}, }}}, })) _, err = cs.Commit() @@ -796,13 +805,13 @@ func TestReconcileVersionsThenContinueCommitting(t *testing.T) { // Continue committing new blocks on top of the reconciled state. // Version 3 is re-created with new data (0xA3 instead of 0x03). for i := byte(0); i < 3; i++ { - v := []byte{0xA0 + i + 3} + v := 0xA0 + i + 3 require.NoError(t, cs2.ApplyChangeSets([]*proto.NamedChangeSet{ {Name: "bank", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ - {Key: []byte("bal"), Value: v}, + {Key: []byte("bal"), Value: []byte{v}}, }}}, {Name: EVMStoreName, Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ - {Key: storageKey, Value: v}, + {Key: storageKey, Value: padLeft32(v)}, }}}, })) ver, err := cs2.Commit() @@ -829,7 +838,7 @@ func TestReconcileVersionsThenContinueCommitting(t *testing.T) { got, found := cs3.evmCommitter.Get(storageKey) require.True(t, found) - require.Equal(t, []byte{0xA5}, got) + require.Equal(t, padLeft32(0xA5), got) } func TestReconcileVersionsCosmosAheadByMultiple(t *testing.T) { @@ -860,7 +869,7 @@ func TestReconcileVersionsCosmosAheadByMultiple(t *testing.T) { Name: EVMStoreName, Changeset: proto.ChangeSet{ Pairs: []*proto.KVPair{ - {Key: storageKey, Value: []byte{i}}, + {Key: storageKey, Value: padLeft32(i)}, }, }, }, diff --git a/sei-db/state_db/sc/flatkv/api.go b/sei-db/state_db/sc/flatkv/api.go index ad21e60b43..4c4537603c 100644 --- a/sei-db/state_db/sc/flatkv/api.go +++ b/sei-db/state_db/sc/flatkv/api.go @@ -33,8 +33,12 @@ type Store interface { // Commit persists buffered writes and advances the version. Commit() (int64, error) - // Get returns the value for the x/evm memiavl key, or (nil, false) if not found. - Get(key []byte) ([]byte, bool) + // Get returns the value for the x/evm memiavl key. If not found, returns (nil, false). + Get(key []byte) (value []byte, found bool) + + // GetBlockHeightModified returns the block height at which the key was last modified. + // If not found, returns (-1, false, nil). + GetBlockHeightModified(key []byte) (int64, bool, error) // Has reports whether the x/evm memiavl key exists. Has(key []byte) bool diff --git a/sei-db/state_db/sc/flatkv/crash_recovery_test.go b/sei-db/state_db/sc/flatkv/crash_recovery_test.go index 28c426b942..d9849c3bfa 100644 --- a/sei-db/state_db/sc/flatkv/crash_recovery_test.go +++ b/sei-db/state_db/sc/flatkv/crash_recovery_test.go @@ -94,7 +94,7 @@ func TestCrashRecoveryGlobalMetadataAheadOfDataDBs(t *testing.T) { addr := addrN(0x02) for i := 1; i <= 5; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))) - cs := makeChangeSet(key, []byte{byte(i * 11)}, false) + cs := makeChangeSet(key, padLeft32(byte(i*11)), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err := s.Commit() require.NoError(t, err) @@ -124,7 +124,7 @@ func TestCrashRecoveryGlobalMetadataAheadOfDataDBs(t *testing.T) { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))) val, found := s2.Get(key) require.True(t, found, "slot %d should exist after recovery", i) - require.Equal(t, []byte{byte(i * 11)}, val) + require.Equal(t, padLeft32(byte(i*11)), val) } } @@ -142,7 +142,7 @@ func TestCrashRecoveryWALReplayLargeGap(t *testing.T) { addr := addrN(0x03) for i := 1; i <= 20; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))) - cs := makeChangeSet(key, []byte{byte(i)}, false) + cs := makeChangeSet(key, padLeft32(byte(i)), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err := s.Commit() require.NoError(t, err) @@ -166,7 +166,7 @@ func TestCrashRecoveryWALReplayLargeGap(t *testing.T) { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))) val, found := s2.Get(key) require.True(t, found, "slot %d should exist", i) - require.Equal(t, []byte{byte(i)}, val) + require.Equal(t, padLeft32(byte(i)), val) } } @@ -182,7 +182,7 @@ func TestCrashRecoveryEmptyWALAfterSnapshot(t *testing.T) { addr := addrN(0x04) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0xAA}, false) + cs := makeChangeSet(key, padLeft32(0xAA), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err = s.Commit() require.NoError(t, err) @@ -207,10 +207,10 @@ func TestCrashRecoveryEmptyWALAfterSnapshot(t *testing.T) { val, found := s2.Get(key) require.True(t, found) - require.Equal(t, []byte{0xAA}, val) + require.Equal(t, padLeft32(0xAA), val) // Can continue committing after recovery from snapshot-only state. - cs2 := makeChangeSet(key, []byte{0xBB}, false) + cs2 := makeChangeSet(key, padLeft32(0xBB), false) require.NoError(t, s2.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) v, err := s2.Commit() require.NoError(t, err) @@ -248,7 +248,7 @@ func TestCrashRecoveryCorruptedAccountValueInDB(t *testing.T) { } err = s.ApplyChangeSets([]*proto.NamedChangeSet{cs2}) require.Error(t, err, "should fail on corrupted AccountValue") - require.Contains(t, err.Error(), "corrupted AccountValue") + require.Contains(t, err.Error(), "unsupported serialization version") } func TestCrashRecoveryCrashAfterWALBeforeDBCommit(t *testing.T) { @@ -265,14 +265,14 @@ func TestCrashRecoveryCrashAfterWALBeforeDBCommit(t *testing.T) { addr := addrN(0x06) slot := slotN(0x01) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err = s.Commit() require.NoError(t, err) hashAfterV1 := s.RootHash() // Now simulate writing v2 to WAL but "crashing" before DB commit. - cs2 := makeChangeSet(key, []byte{0x22}, false) + cs2 := makeChangeSet(key, padLeft32(0x22), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) // Write v2 to WAL manually (like Commit step 1). @@ -300,7 +300,7 @@ func TestCrashRecoveryCrashAfterWALBeforeDBCommit(t *testing.T) { val, found := s2.Get(key) require.True(t, found) - require.Equal(t, []byte{0x22}, val, "v2 value should be present after catchup") + require.Equal(t, padLeft32(0x22), val, "v2 value should be present after catchup") verifyLtHashConsistency(t, s2) } @@ -321,7 +321,7 @@ func TestCrashRecoveryLtHashConsistencyAfterAllPaths(t *testing.T) { noncePair(addr, uint64(i)), { Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))), - Value: []byte{byte(i)}, + Value: padLeft32(byte(i)), }, } cs := &proto.NamedChangeSet{ @@ -381,7 +381,7 @@ func TestCrashRecoveryCorruptLtHashBlobInMetadata(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x01), slotN(0x01))), - []byte{0x11}, false, + padLeft32(0x11), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err = s.Commit() @@ -416,7 +416,7 @@ func TestCrashRecoveryCorruptLtHashBlobInPerDBMeta(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x02), slotN(0x01))), - []byte{0x22}, false, + padLeft32(0x22), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err = s.Commit() @@ -451,7 +451,7 @@ func TestCrashRecoveryGlobalVersionOverflow(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x03), slotN(0x01))), - []byte{0x33}, false, + padLeft32(0x33), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err = s.Commit() diff --git a/sei-db/state_db/sc/flatkv/exporter.go b/sei-db/state_db/sc/flatkv/exporter.go index 60413672e6..8ae0726d3d 100644 --- a/sei-db/state_db/sc/flatkv/exporter.go +++ b/sei-db/state_db/sc/flatkv/exporter.go @@ -8,6 +8,7 @@ import ( errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/evm" dbtypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" ) @@ -154,18 +155,18 @@ func (e *KVExporter) convertToNodes(db exportDBKind, key, value []byte) ([]*type case exportDBAccount: return e.accountToNodes(key, value) case exportDBCode: - return e.codeToNodes(key, value), nil + return e.codeToNodes(key, value) case exportDBStorage: - return e.storageToNodes(key, value), nil + return e.storageToNodes(key, value) case exportDBLegacy: - return e.legacyToNodes(key, value), nil + return e.legacyToNodes(key, value) default: return nil, nil } } func (e *KVExporter) accountToNodes(key, value []byte) ([]*types.SnapshotNode, error) { - av, err := DecodeAccountValue(value) + ad, err := vtype.DeserializeAccountData(value) if err != nil { return nil, fmt.Errorf("corrupt account entry key=%x: %w", key, err) } @@ -173,8 +174,8 @@ func (e *KVExporter) accountToNodes(key, value []byte) ([]*types.SnapshotNode, e var nodes []*types.SnapshotNode nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, key) - nonceValue := make([]byte, NonceLen) - binary.BigEndian.PutUint64(nonceValue, av.Nonce) + nonceValue := make([]byte, vtype.NonceLen) + binary.BigEndian.PutUint64(nonceValue, ad.GetNonce()) nodes = append(nodes, &types.SnapshotNode{ Key: nonceKey, Value: nonceValue, @@ -182,10 +183,12 @@ func (e *KVExporter) accountToNodes(key, value []byte) ([]*types.SnapshotNode, e Height: 0, }) - if av.HasCode() { + codeHash := ad.GetCodeHash() + var zeroHash vtype.CodeHash + if codeHash != nil && *codeHash != zeroHash { codeHashKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, key) - codeHashValue := make([]byte, CodeHashLen) - copy(codeHashValue, av.CodeHash[:]) + codeHashValue := make([]byte, vtype.CodeHashLen) + copy(codeHashValue, codeHash[:]) nodes = append(nodes, &types.SnapshotNode{ Key: codeHashKey, Value: codeHashValue, @@ -197,31 +200,43 @@ func (e *KVExporter) accountToNodes(key, value []byte) ([]*types.SnapshotNode, e return nodes, nil } -func (e *KVExporter) codeToNodes(key, value []byte) []*types.SnapshotNode { +func (e *KVExporter) codeToNodes(key, value []byte) ([]*types.SnapshotNode, error) { + codeData, err := vtype.DeserializeCodeData(value) + if err != nil { + return nil, fmt.Errorf("corrupt code entry key=%x: %w", key, err) + } memiavlKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, key) return []*types.SnapshotNode{{ Key: memiavlKey, - Value: value, + Value: codeData.GetBytecode(), Version: e.version, Height: 0, - }} + }}, nil } -func (e *KVExporter) storageToNodes(key, value []byte) []*types.SnapshotNode { +func (e *KVExporter) storageToNodes(key, value []byte) ([]*types.SnapshotNode, error) { + storageData, err := vtype.DeserializeStorageData(value) + if err != nil { + return nil, fmt.Errorf("corrupt storage entry key=%x: %w", key, err) + } memiavlKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, key) return []*types.SnapshotNode{{ Key: memiavlKey, - Value: value, + Value: storageData.GetValue()[:], Version: e.version, Height: 0, - }} + }}, nil } -func (e *KVExporter) legacyToNodes(key, value []byte) []*types.SnapshotNode { +func (e *KVExporter) legacyToNodes(key, value []byte) ([]*types.SnapshotNode, error) { + legacyData, err := vtype.DeserializeLegacyData(value) + if err != nil { + return nil, fmt.Errorf("corrupt legacy entry key=%x: %w", key, err) + } return []*types.SnapshotNode{{ Key: key, - Value: value, + Value: legacyData.GetValue(), Version: e.version, Height: 0, - }} + }}, nil } diff --git a/sei-db/state_db/sc/flatkv/exporter_test.go b/sei-db/state_db/sc/flatkv/exporter_test.go index b20f12fc94..8a804deb8a 100644 --- a/sei-db/state_db/sc/flatkv/exporter_test.go +++ b/sei-db/state_db/sc/flatkv/exporter_test.go @@ -12,6 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/evm" dbtypes "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/vtype" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" ) @@ -51,8 +52,8 @@ func TestExporterStorageKeys(t *testing.T) { addr := Address{0xAA} slot1 := Slot{0x01} slot2 := Slot{0x02} - val1 := []byte{0x11} - val2 := []byte{0x22} + val1 := padLeft32(0x11) + val2 := padLeft32(0x22) key1 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot1)) key2 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot2)) @@ -88,7 +89,7 @@ func TestExporterAccountKeys(t *testing.T) { nonceVal := []byte{0, 0, 0, 0, 0, 0, 0, 42} codeHashKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]) - codeHashVal := make([]byte, CodeHashLen) + codeHashVal := make([]byte, vtype.CodeHashLen) codeHashVal[0] = 0xDE require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ @@ -159,13 +160,13 @@ func TestExporterRoundTrip(t *testing.T) { slot := Slot{0xEE} storageKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) - storageVal := []byte{0xFF} + storageVal := padLeft32(0xFF) nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) nonceVal := []byte{0, 0, 0, 0, 0, 0, 0, 7} codeKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:]) codeVal := []byte{0x60, 0x80} codeHashKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]) - codeHashVal := make([]byte, CodeHashLen) + codeHashVal := make([]byte, vtype.CodeHashLen) codeHashVal[31] = 0xAB require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ @@ -273,7 +274,7 @@ func TestImportSurvivesReopen(t *testing.T) { slot := Slot{0xEE} storageKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) - storageVal := []byte{0xFF} + storageVal := padLeft32(0xFF) nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) nonceVal := []byte{0, 0, 0, 0, 0, 0, 0, 7} @@ -371,14 +372,14 @@ func TestImportPurgesStaleData(t *testing.T) { codeStale := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addrStale[:]) nonceVal := []byte{0, 0, 0, 0, 0, 0, 0, 1} - codeHashVal := make([]byte, CodeHashLen) + codeHashVal := make([]byte, vtype.CodeHashLen) codeHashVal[31] = 0xAB codeVal := []byte{0x60, 0x80} require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ {Name: "evm", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ - {Key: storageA, Value: []byte{0x0A}}, - {Key: storageStale, Value: []byte{0x0C}}, + {Key: storageA, Value: padLeft32(0x0A)}, + {Key: storageStale, Value: padLeft32(0x0C)}, {Key: nonceA, Value: nonceVal}, {Key: nonceStale, Value: nonceVal}, {Key: codeHashB, Value: codeHashVal}, @@ -391,8 +392,9 @@ func TestImportPurgesStaleData(t *testing.T) { staleKeys := [][]byte{storageStale, nonceStale, codeHashStale, codeStale} + var found bool for _, k := range staleKeys { - _, found := s.Get(k) + _, found = s.Get(k) require.True(t, found, "pre-import: key should exist") } @@ -400,9 +402,9 @@ func TestImportPurgesStaleData(t *testing.T) { src := setupTestStore(t) defer src.Close() - newStorageVal := []byte{0xA1} + newStorageVal := padLeft32(0xA1) newNonceVal := []byte{0, 0, 0, 0, 0, 0, 0, 5} - newCodeHashVal := make([]byte, CodeHashLen) + newCodeHashVal := make([]byte, vtype.CodeHashLen) newCodeHashVal[31] = 0xCD newCodeVal := []byte{0x60, 0x40, 0x52} @@ -439,7 +441,8 @@ func TestImportPurgesStaleData(t *testing.T) { require.NoError(t, imp.Close()) // --- Phase 4: verify stale keys are gone across all DB types --- - got, found := s.Get(storageA) + var got []byte + got, found = s.Get(storageA) require.True(t, found, "storage key A should exist") require.Equal(t, newStorageVal, got) @@ -515,7 +518,7 @@ func TestImporterOnReadOnlyStore(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x01), slotN(0x01))), - []byte{0x11}, false, + padLeft32(0x11), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -546,7 +549,7 @@ func TestImporterHeightNonZeroSkipped(t *testing.T) { // Non-leaf nodes (Height != 0) are silently skipped. imp.AddNode(&types.SnapshotNode{ Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x01), slotN(0x01))), - Value: []byte{0x11}, + Value: padLeft32(0x11), Height: 1, // non-leaf }) @@ -620,7 +623,7 @@ func TestImporterCorruptKeyDataPropagatesError(t *testing.T) { // Add a valid storage node first. imp.AddNode(&types.SnapshotNode{ Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x01), slotN(0x01))), - Value: []byte{0x11}, + Value: padLeft32(0x11), }) // Add a node with a nonce key but invalid nonce value length. @@ -651,21 +654,21 @@ func TestImporterDoubleImport(t *testing.T) { require.NoError(t, err) imp1.AddNode(&types.SnapshotNode{ Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x01), slotN(0x01))), - Value: []byte{0x11}, + Value: padLeft32(0x11), }) require.NoError(t, imp1.Close()) key1 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x01), slotN(0x01))) val, found := s.Get(key1) require.True(t, found) - require.Equal(t, []byte{0x11}, val) + require.Equal(t, padLeft32(0x11), val) // Second import: should wipe prior state (resetForImport). imp2, err := s.Importer(2) require.NoError(t, err) imp2.AddNode(&types.SnapshotNode{ Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x02), slotN(0x02))), - Value: []byte{0x22}, + Value: padLeft32(0x22), }) require.NoError(t, imp2.Close()) @@ -678,7 +681,7 @@ func TestImporterDoubleImport(t *testing.T) { key2 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x02), slotN(0x02))) val, found = s.Get(key2) require.True(t, found) - require.Equal(t, []byte{0x22}, val) + require.Equal(t, padLeft32(0x22), val) require.NoError(t, s.Close()) } @@ -692,17 +695,17 @@ func TestExporterAtHistoricalVersion(t *testing.T) { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) // v1: write 0x11 - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) // v2: write 0x22 - cs2 := makeChangeSet(key, []byte{0x22}, false) + cs2 := makeChangeSet(key, padLeft32(0x22), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) commitAndCheck(t, s) // v3: write 0x33 - cs3 := makeChangeSet(key, []byte{0x33}, false) + cs3 := makeChangeSet(key, padLeft32(0x33), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs3})) commitAndCheck(t, s) @@ -726,38 +729,41 @@ func TestExporterAtHistoricalVersion(t *testing.T) { require.NoError(t, exp.Close()) require.Len(t, storageNodes, 1) - require.Equal(t, []byte{0x11}, storageNodes[0].Value, "historical export should have v1 value") + require.Equal(t, padLeft32(0x11), storageNodes[0].Value, "historical export should have v1 value") } func TestExportImportLargerDataset(t *testing.T) { cfg := DefaultTestConfig(t) - cfg.SnapshotInterval = 5 s := setupTestStoreWithConfig(t, cfg) defer s.Close() - // Write multiple key types across multiple addresses. + // Write multiple key types across multiple addresses in a single block + // so that all rows share the same block height. The importer commits + // everything at a single version, so block heights must match for the + // LtHash round-trip to be identical. + var allPairs []*proto.KVPair for i := byte(1); i <= 10; i++ { addr := addrN(i) - pairs := []*proto.KVPair{ + allPairs = append(allPairs, noncePair(addr, uint64(i)), - { + &proto.KVPair{ Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(i))), - Value: []byte{i, i, i}, + Value: padLeft32(i, i, i), }, - } + ) if i%3 == 0 { - pairs = append(pairs, + allPairs = append(allPairs, codeHashPair(addr, codeHashN(i)), codePair(addr, []byte{0x60, i}), ) } - cs := &proto.NamedChangeSet{ - Name: "evm", - Changeset: proto.ChangeSet{Pairs: pairs}, - } - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) - commitAndCheck(t, s) } + cs := &proto.NamedChangeSet{ + Name: "evm", + Changeset: proto.ChangeSet{Pairs: allPairs}, + } + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) + commitAndCheck(t, s) originalHash := s.RootHash() // Export. @@ -776,14 +782,14 @@ func TestExportImportLargerDataset(t *testing.T) { _, err = s2.LoadVersion(0, false) require.NoError(t, err) - imp, err := s2.Importer(10) + imp, err := s2.Importer(1) require.NoError(t, err) for _, n := range nodes { imp.AddNode(n) } require.NoError(t, imp.Close()) - require.Equal(t, int64(10), s2.Version()) + require.Equal(t, int64(1), s2.Version()) require.Equal(t, originalHash, s2.RootHash(), "imported store should have identical RootHash") require.NoError(t, s2.Close()) } diff --git a/sei-db/state_db/sc/flatkv/iterator.go b/sei-db/state_db/sc/flatkv/iterator.go index 6ea8557c86..44a90e6fc0 100644 --- a/sei-db/state_db/sc/flatkv/iterator.go +++ b/sei-db/state_db/sc/flatkv/iterator.go @@ -5,6 +5,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/evm" "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" ) // dbIterator is a generic iterator that wraps a PebbleDB iterator @@ -226,7 +227,18 @@ func (it *dbIterator) Value() []byte { if !it.Valid() { return nil } - return it.iter.Value() + raw := it.iter.Value() + switch it.kind { + case evm.EVMKeyStorage: + sd, err := vtype.DeserializeStorageData(raw) + if err != nil { + it.err = fmt.Errorf("deserialize storage value: %w", err) + return nil + } + return sd.GetValue()[:] + default: + return raw + } } // CommitStore factory methods for creating iterators diff --git a/sei-db/state_db/sc/flatkv/keys.go b/sei-db/state_db/sc/flatkv/keys.go index 3070296aef..391763051e 100644 --- a/sei-db/state_db/sc/flatkv/keys.go +++ b/sei-db/state_db/sc/flatkv/keys.go @@ -2,9 +2,8 @@ package flatkv import ( "bytes" - "encoding/binary" - "fmt" + "github.com/sei-protocol/sei-chain/sei-db/common/evm" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash" ) @@ -32,12 +31,25 @@ func isMetaKey(key []byte) bool { return bytes.HasPrefix(key, metaKeyPrefixBytes) } +// Supported EVM key types for FlatKV. +// TODO: add balance key when that is eventually supported +var supportedKeyTypes = map[evm.EVMKeyKind]struct{}{ + evm.EVMKeyStorage: {}, + evm.EVMKeyNonce: {}, + evm.EVMKeyCodeHash: {}, + evm.EVMKeyCode: {}, + evm.EVMKeyLegacy: {}, +} + +// IsSupportedKeyType reports whether the given key kind is handled by FlatKV. +func IsSupportedKeyType(kind evm.EVMKeyKind) bool { + _, ok := supportedKeyTypes[kind] + return ok +} + const ( - AddressLen = 20 - CodeHashLen = 32 - SlotLen = 32 - BalanceLen = 32 - NonceLen = 8 + AddressLen = 20 + SlotLen = 32 ) // LocalMeta stores per-DB version tracking metadata. @@ -50,15 +62,9 @@ type LocalMeta struct { // Address is an EVM address (20 bytes). type Address [AddressLen]byte -// CodeHash is a contract code hash (32 bytes). -type CodeHash [CodeHashLen]byte - // Slot is a storage slot key (32 bytes). type Slot [SlotLen]byte -// Balance is an EVM balance (32 bytes, big-endian uint256). -type Balance [BalanceLen]byte - func AddressFromBytes(b []byte) (Address, bool) { if len(b) != AddressLen { return Address{}, false @@ -68,15 +74,6 @@ func AddressFromBytes(b []byte) (Address, bool) { return a, true } -func SlotFromBytes(b []byte) (Slot, bool) { - if len(b) != SlotLen { - return Slot{}, false - } - var s Slot - copy(s[:], b) - return s, true -} - // ============================================================================= // DB Key Builders // ============================================================================= @@ -96,6 +93,15 @@ func StorageKey(addr Address, slot Slot) []byte { return key } +func SlotFromBytes(b []byte) (Slot, bool) { + if len(b) != SlotLen { + return Slot{}, false + } + var s Slot + copy(s[:], b) + return s, true +} + // PrefixEnd returns the exclusive upper bound for prefix iteration (or nil). func PrefixEnd(prefix []byte) []byte { if len(prefix) == 0 { @@ -110,83 +116,3 @@ func PrefixEnd(prefix []byte) []byte { } return nil } - -// AccountValue is the account record. -// -// Encoding is variable-length to save space for EOA accounts: -// - EOA (no code): balance(32) || nonce(8) = 40 bytes -// - Contract (has code): balance(32) || nonce(8) || codehash(32) = 72 bytes -// -// CodeHash == CodeHash{} (all zeros) means the account has no code (EOA). -// Note: empty code contracts have CodeHash = keccak256("") which is non-zero. -type AccountValue struct { - Balance Balance - Nonce uint64 - CodeHash CodeHash -} - -const ( - // accountValueEOALen is the encoded length for EOA accounts (no code). - accountValueEOALen = BalanceLen + NonceLen // 40 bytes - - // accountValueContractLen is the encoded length for contract accounts. - accountValueContractLen = BalanceLen + NonceLen + CodeHashLen // 72 bytes -) - -// HasCode returns true if the account has code (is a contract). -func (v AccountValue) HasCode() bool { - return v.CodeHash != CodeHash{} -} - -// IsEmpty returns true when all fields are zero-valued, indicating the -// account can be physically deleted from accountDB. -func (v AccountValue) IsEmpty() bool { - return v.Balance == (Balance{}) && v.Nonce == 0 && v.CodeHash == (CodeHash{}) -} - -// Encode encodes the AccountValue to bytes. -func (v AccountValue) Encode() []byte { - return EncodeAccountValue(v) -} - -// EncodeAccountValue encodes v into a variable-length slice. -// EOA accounts (no code) are encoded as 40 bytes, contracts as 72 bytes. -func EncodeAccountValue(v AccountValue) []byte { - size := accountValueEOALen - if v.HasCode() { - size = accountValueContractLen - } - b := make([]byte, size) - copy(b, v.Balance[:]) - binary.BigEndian.PutUint64(b[BalanceLen:], v.Nonce) - if v.HasCode() { - copy(b[BalanceLen+NonceLen:], v.CodeHash[:]) - } - return b -} - -// DecodeAccountValue decodes a variable-length account record. -// Returns an error if the length is neither 40 (EOA) nor 72 (contract) bytes. -func DecodeAccountValue(b []byte) (AccountValue, error) { - switch len(b) { - case accountValueEOALen: - // EOA: balance(32) || nonce(8) - var v AccountValue - copy(v.Balance[:], b[:BalanceLen]) - v.Nonce = binary.BigEndian.Uint64(b[BalanceLen:]) - // CodeHash remains zero (no code) - return v, nil - - case accountValueContractLen: - // Contract: balance(32) || nonce(8) || codehash(32) - var v AccountValue - copy(v.Balance[:], b[:BalanceLen]) - v.Nonce = binary.BigEndian.Uint64(b[BalanceLen : BalanceLen+NonceLen]) - copy(v.CodeHash[:], b[BalanceLen+NonceLen:]) - return v, nil - - default: - return AccountValue{}, fmt.Errorf("invalid account value length: got %d, want %d (EOA) or %d (contract)", - len(b), accountValueEOALen, accountValueContractLen) - } -} diff --git a/sei-db/state_db/sc/flatkv/keys_test.go b/sei-db/state_db/sc/flatkv/keys_test.go index 263101c20d..b77814b369 100644 --- a/sei-db/state_db/sc/flatkv/keys_test.go +++ b/sei-db/state_db/sc/flatkv/keys_test.go @@ -1,8 +1,6 @@ package flatkv import ( - "math" - "math/rand" "testing" "github.com/stretchr/testify/require" @@ -31,144 +29,6 @@ func TestFlatKVPrefixEnd(t *testing.T) { } } -func TestFlatKVAccountValueEncoding(t *testing.T) { - // Deterministic seed so failures are reproducible. - const seed = int64(1) - rng := rand.New(rand.NewSource(seed)) - - randomBytes := func(n int) []byte { - b := make([]byte, n) - rng.Read(b) - return b - } - - t.Run("RoundTripContract", func(t *testing.T) { - var balance Balance - copy(balance[:], randomBytes(BalanceLen)) - var codeHash CodeHash - copy(codeHash[:], randomBytes(CodeHashLen)) - - original := AccountValue{ - Balance: balance, - Nonce: rng.Uint64(), - CodeHash: codeHash, - } - - require.True(t, original.HasCode(), "contract should have code") - - encoded := EncodeAccountValue(original) - require.Equal(t, accountValueContractLen, len(encoded), "contract should be 72 bytes") - - decoded, err := DecodeAccountValue(encoded) - require.NoError(t, err) - require.Equal(t, original, decoded) - }) - - t.Run("RoundTripEOA", func(t *testing.T) { - var balance Balance - copy(balance[:], randomBytes(BalanceLen)) - - original := AccountValue{ - Balance: balance, - Nonce: rng.Uint64(), - CodeHash: CodeHash{}, // EOA has no code - } - - require.False(t, original.HasCode(), "EOA should not have code") - - encoded := EncodeAccountValue(original) - require.Equal(t, accountValueEOALen, len(encoded), "EOA should be 40 bytes") - - decoded, err := DecodeAccountValue(encoded) - require.NoError(t, err) - require.Equal(t, original, decoded) - }) - - t.Run("RoundTripZeroEOA", func(t *testing.T) { - // Completely empty account (zero balance, zero nonce, no code) - original := AccountValue{ - Balance: Balance{}, - Nonce: 0, - CodeHash: CodeHash{}, - } - - require.False(t, original.HasCode()) - - encoded := EncodeAccountValue(original) - require.Equal(t, accountValueEOALen, len(encoded), "zero EOA should be 40 bytes") - - decoded, err := DecodeAccountValue(encoded) - require.NoError(t, err) - require.Equal(t, original, decoded) - }) - - t.Run("InvalidLength", func(t *testing.T) { - // Too short - _, err := DecodeAccountValue([]byte{0x00}) - require.Error(t, err) - require.Contains(t, err.Error(), "invalid account value length") - - // In between EOA and Contract lengths - _, err = DecodeAccountValue(make([]byte, 50)) - require.Error(t, err) - require.Contains(t, err.Error(), "invalid account value length") - - // Too long - _, err = DecodeAccountValue(make([]byte, 100)) - require.Error(t, err) - require.Contains(t, err.Error(), "invalid account value length") - }) - - t.Run("NonceIsBigEndianUint64", func(t *testing.T) { - // Test with EOA - original := AccountValue{ - Nonce: math.MaxUint64, - } - encoded := EncodeAccountValue(original) - decoded, err := DecodeAccountValue(encoded) - require.NoError(t, err) - require.Equal(t, original.Nonce, decoded.Nonce) - - // Test with Contract - var codeHash CodeHash - copy(codeHash[:], randomBytes(CodeHashLen)) - originalContract := AccountValue{ - Nonce: math.MaxUint64, - CodeHash: codeHash, - } - encodedContract := EncodeAccountValue(originalContract) - decodedContract, err := DecodeAccountValue(encodedContract) - require.NoError(t, err) - require.Equal(t, originalContract.Nonce, decodedContract.Nonce) - }) - - t.Run("HasCodeMethod", func(t *testing.T) { - // EOA - no code - eoa := AccountValue{CodeHash: CodeHash{}} - require.False(t, eoa.HasCode()) - - // Contract - has code (any non-zero hash) - var codeHash CodeHash - codeHash[0] = 0x01 // Just one non-zero byte is enough - contract := AccountValue{CodeHash: codeHash} - require.True(t, contract.HasCode()) - }) -} - -func TestAccountValueIsEmpty(t *testing.T) { - require.True(t, AccountValue{}.IsEmpty(), "zero-value AccountValue should be empty") - - require.False(t, AccountValue{Nonce: 1}.IsEmpty(), "non-zero nonce") - require.False(t, AccountValue{CodeHash: CodeHash{0x01}}.IsEmpty(), "non-zero codehash") - require.False(t, AccountValue{Balance: Balance{0x01}}.IsEmpty(), "non-zero balance") - - require.False(t, AccountValue{ - Balance: Balance{0x01}, - Nonce: 42, - CodeHash: CodeHash{0xFF}, - }.IsEmpty(), "all non-zero fields") -} - func TestFlatKVTypeConversions(t *testing.T) { t.Run("AddressFromBytes", func(t *testing.T) { valid := make([]byte, AddressLen) 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 400e543b3e..e78ac3370b 100644 --- a/sei-db/state_db/sc/flatkv/lthash_correctness_test.go +++ b/sei-db/state_db/sc/flatkv/lthash_correctness_test.go @@ -13,6 +13,7 @@ import ( "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" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" scTypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" "github.com/stretchr/testify/require" ) @@ -54,7 +55,7 @@ func fullScanLtHash(t *testing.T, s *CommitStore) *lthash.LtHash { // ---------- helpers to build memiavl-format changeset pairs ---------- func nonceBytes(n uint64) []byte { - b := make([]byte, NonceLen) + b := make([]byte, vtype.NonceLen) binary.BigEndian.PutUint64(b, n) return b } @@ -71,8 +72,8 @@ func slotN(n byte) Slot { return s } -func codeHashN(n byte) CodeHash { - var h CodeHash +func codeHashN(n byte) vtype.CodeHash { + var h vtype.CodeHash for i := range h { h[i] = n } @@ -86,7 +87,7 @@ func noncePair(addr Address, nonce uint64) *proto.KVPair { } } -func codeHashPair(addr Address, ch CodeHash) *proto.KVPair { +func codeHashPair(addr Address, ch vtype.CodeHash) *proto.KVPair { return &proto.KVPair{ Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]), Value: ch[:], @@ -110,7 +111,7 @@ func codeDeletePair(addr Address) *proto.KVPair { func storagePair(addr Address, slot Slot, val []byte) *proto.KVPair { return &proto.KVPair{ Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), - Value: val, + Value: padLeft32(val...), } } @@ -770,7 +771,7 @@ func TestLtHashCrossApplyStorageOverwrite(t *testing.T) { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) val, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{0x33}, val) + require.Equal(t, padLeft32(0x33), val) } // TestLtHashCrossApplyCodeOverwrite verifies that overwriting the same code @@ -915,7 +916,7 @@ func TestLtHashCrossApplyMixedOverwrite(t *testing.T) { storageKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) storageVal, found := s.Get(storageKey) require.True(t, found) - require.Equal(t, []byte{0x33}, storageVal) + require.Equal(t, padLeft32(0x33), storageVal) legacyVal, found := s.Get(legacyKey) require.True(t, found) @@ -989,7 +990,11 @@ func TestLtHashAccountDeleteThenRecreate(t *testing.T) { raw, err := s.accountDB.Get(AccountKey(addr)) require.NoError(t, err) - require.Equal(t, accountValueEOALen, len(raw), "row should be 40-byte EOA encoding") + ad, err := vtype.DeserializeAccountData(raw) + require.NoError(t, err) + require.Equal(t, uint64(99), ad.GetNonce()) + var zeroHash vtype.CodeHash + require.Equal(t, &zeroHash, ad.GetCodeHash(), "codehash should be zero (EOA)") } func TestLtHashAccountPartialDeletePreservesRow(t *testing.T) { @@ -1012,7 +1017,11 @@ func TestLtHashAccountPartialDeletePreservesRow(t *testing.T) { raw, err := s.accountDB.Get(AccountKey(addr)) require.NoError(t, err, "row should still exist after partial delete") - require.Equal(t, accountValueEOALen, len(raw), "should shrink to EOA encoding") + ad, err := vtype.DeserializeAccountData(raw) + require.NoError(t, err) + require.Equal(t, uint64(3), ad.GetNonce(), "nonce should be preserved") + var zeroHash vtype.CodeHash + require.Equal(t, &zeroHash, ad.GetCodeHash(), "codehash should be zero after delete") } // TestAccountPendingReadPartialDelete verifies that the isDelete guard in @@ -1046,7 +1055,7 @@ func TestAccountPendingReadPartialDelete(t *testing.T) { paw := s.accountWrites[string(addr[:])] require.NotNil(t, paw) - require.False(t, paw.isDelete, "row should NOT be marked for deletion (partial delete)") + require.False(t, paw.IsDelete(), "row should NOT be marked for deletion (partial delete)") } // TestAccountRowDeleteGetBeforeCommit verifies the core behavioral change: @@ -1089,13 +1098,15 @@ func TestAccountRowDeleteGetBeforeCommit(t *testing.T) { require.False(t, found, "codehash should not be found after pending full-delete") require.Nil(t, chVal) - require.False(t, s.Has(nonceKey), "Has(nonce) should be false after pending full-delete") - require.False(t, s.Has(chKey), "Has(codehash) should be false after pending full-delete") + hasNonce := s.Has(nonceKey) + require.False(t, hasNonce, "Has(nonce) should be false after pending full-delete") + hasCodeHash := s.Has(chKey) + require.False(t, hasCodeHash, "Has(codehash) should be false after pending full-delete") // Verify isDelete is set paw := s.accountWrites[string(addr[:])] require.NotNil(t, paw) - require.True(t, paw.isDelete, "row should be marked for deletion (all fields zero)") + require.True(t, paw.IsDelete(), "row should be marked for deletion (all fields zero)") } // TestLtHashAccountWriteZeroGC verifies that writing a zero value (not a @@ -1277,22 +1288,27 @@ func TestLtHashExportImportRoundTrip(t *testing.T) { s := setupTestStore(t) defer s.Close() - // Build state across multiple blocks + // Build state in a single block so that all rows share the same block + // height. The importer commits everything at a single version, so block + // heights must match for the LtHash round-trip to be identical. + var evmPairs []*proto.KVPair + var legacyCS []*proto.NamedChangeSet for i := byte(1); i <= 5; i++ { addr := addrN(i) + evmPairs = append(evmPairs, + noncePair(addr, uint64(i)*10), + codeHashPair(addr, codeHashN(i)), + codePair(addr, []byte{0x60, 0x80, i}), + storagePair(addr, slotN(i), []byte{i, 0xBB}), + ) legacyKey := append([]byte{0x09}, addr[:]...) - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ - namedCS( - noncePair(addr, uint64(i)*10), - codeHashPair(addr, codeHashN(i)), - codePair(addr, []byte{0x60, 0x80, i}), - storagePair(addr, slotN(i), []byte{i, 0xBB}), - ), - makeChangeSet(legacyKey, []byte{i, 0xCC}, false), - })) - commitAndCheck(t, s) + legacyCS = append(legacyCS, makeChangeSet(legacyKey, []byte{i, 0xCC}, false)) } - verifyLtHashAtHeight(t, s, 5) + allCS := append([]*proto.NamedChangeSet{namedCS(evmPairs...)}, legacyCS...) + require.NoError(t, s.ApplyChangeSets(allCS)) + commitAndCheck(t, s) + + verifyLtHashAtHeight(t, s, 1) srcHash := s.RootHash() // Export @@ -1314,7 +1330,7 @@ func TestLtHashExportImportRoundTrip(t *testing.T) { // Import into fresh store s2 := setupTestStore(t) - imp, err := s2.Importer(5) + imp, err := s2.Importer(1) require.NoError(t, err) require.NoError(t, imp.AddModule(evm.EVMFlatKVStoreKey)) for _, n := range nodes { @@ -1322,10 +1338,10 @@ func TestLtHashExportImportRoundTrip(t *testing.T) { } require.NoError(t, imp.Close()) - require.Equal(t, int64(5), s2.Version()) + require.Equal(t, int64(1), s2.Version()) require.Equal(t, srcHash, s2.RootHash(), "imported store RootHash should match source") - verifyLtHashAtHeight(t, s2, 5) + verifyLtHashAtHeight(t, s2, 1) require.NoError(t, s2.Close()) } 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 a7259bd348..1ca29d06d9 100644 --- a/sei-db/state_db/sc/flatkv/perdb_lthash_test.go +++ b/sei-db/state_db/sc/flatkv/perdb_lthash_test.go @@ -465,7 +465,7 @@ func TestPerDBLtHashPartialKeyTypeOperations(t *testing.T) { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) // Write only storage keys: other DBs' per-DB LtHash should remain zero. - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -487,7 +487,7 @@ func TestPerDBLtHashDeleteLastKeyZerosHash(t *testing.T) { addr := addrN(0x02) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x22}, false) + cs := makeChangeSet(key, padLeft32(0x22), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -527,7 +527,7 @@ func TestPerDBLtHashSumInvariantAcrossAllOperations(t *testing.T) { // Operation 1: Add storage key. storageKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(storageKey, []byte{0x33}, false) + cs := makeChangeSet(storageKey, padLeft32(0x33), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) verifySumInvariant("after storage add") @@ -556,7 +556,7 @@ func TestPerDBLtHashSumInvariantAcrossAllOperations(t *testing.T) { verifySumInvariant("after code add") // Operation 4: Update storage. - cs4 := makeChangeSet(storageKey, []byte{0x44}, false) + cs4 := makeChangeSet(storageKey, padLeft32(0x44), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs4})) commitAndCheck(t, s) verifySumInvariant("after storage update") diff --git a/sei-db/state_db/sc/flatkv/snapshot_test.go b/sei-db/state_db/sc/flatkv/snapshot_test.go index 1b541a4ce8..6e0c861d0b 100644 --- a/sei-db/state_db/sc/flatkv/snapshot_test.go +++ b/sei-db/state_db/sc/flatkv/snapshot_test.go @@ -11,16 +11,19 @@ import ( "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/vtype" "github.com/stretchr/testify/require" ) func commitStorageEntry(t *testing.T, s *CommitStore, addr Address, slot Slot, value []byte) int64 { t.Helper() + padded := make([]byte, 32) + copy(padded[32-len(value):], value) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) cs := &proto.NamedChangeSet{ Name: "evm", Changeset: proto.ChangeSet{ - Pairs: []*proto.KVPair{{Key: key, Value: value}}, + Pairs: []*proto.KVPair{{Key: key, Value: padded}}, }, } require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) @@ -120,10 +123,10 @@ func TestOpenFromSnapshot(t *testing.T) { key3 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(Address{0x10}, Slot{0x03})) v, ok := s2.Get(key1) require.True(t, ok) - require.Equal(t, []byte{0x01}, v) + require.Equal(t, padLeft32(0x01), v) v, ok = s2.Get(key3) require.True(t, ok) - require.Equal(t, []byte{0x03}, v) + require.Equal(t, padLeft32(0x03), v) } func TestCatchupUpdatesLtHash(t *testing.T) { @@ -196,7 +199,7 @@ func TestRollbackRewindsState(t *testing.T) { key4 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(Address{0x30}, Slot{0x04})) v, ok := s.Get(key4) require.True(t, ok) - require.Equal(t, []byte{0x04}, v) + require.Equal(t, padLeft32(0x04), v) require.NoError(t, s.Close()) } @@ -473,7 +476,7 @@ func TestSnapshotThenCatchupThenVerifyCorrectness(t *testing.T) { // Record baseline value at v2 for the same key. vAtV2, ok := s1.Get(key) require.True(t, ok) - require.Equal(t, []byte{0x01}, vAtV2) + require.Equal(t, padLeft32(0x01), vAtV2) // Phase 2: advance state beyond the snapshot (v3..v4). commitStorageEntry(t, s1, addr, slot, []byte{0x03}) // v3 @@ -491,7 +494,7 @@ func TestSnapshotThenCatchupThenVerifyCorrectness(t *testing.T) { require.NoError(t, err) gotV2, ok := s2.Get(key) require.True(t, ok) - require.Equal(t, []byte{0x01}, gotV2, "snapshot baseline should remain stable") + require.Equal(t, padLeft32(0x01), gotV2, "snapshot baseline should remain stable") require.NoError(t, s2.Close()) // Phase 4: reopen latest again to ensure catchup/replay still reaches v4. @@ -506,7 +509,7 @@ func TestSnapshotThenCatchupThenVerifyCorrectness(t *testing.T) { require.Equal(t, int64(4), s3.Version()) gotLatest, ok := s3.Get(key) require.True(t, ok) - require.Equal(t, []byte{0x04}, gotLatest) + require.Equal(t, padLeft32(0x04), gotLatest) } // TestLoadVersionMixedSequence: load-old -> load-latest -> load-old-again. @@ -546,7 +549,7 @@ func TestLoadVersionMixedSequence(t *testing.T) { require.Equal(t, hashAtV2, s1.RootHash()) v, ok := s1.Get(key) require.True(t, ok) - require.Equal(t, []byte{0x02}, v) + require.Equal(t, padLeft32(0x02), v) require.NoError(t, s1.Close()) // Round 2: load latest (catches up through v3, v4) @@ -560,7 +563,7 @@ func TestLoadVersionMixedSequence(t *testing.T) { require.Equal(t, hashAtV4, s2.RootHash()) v, ok = s2.Get(key) require.True(t, ok) - require.Equal(t, []byte{0x04}, v) + require.Equal(t, padLeft32(0x04), v) require.NoError(t, s2.Close()) // Round 3: load v2 AGAIN — snapshot must still be clean. @@ -574,7 +577,7 @@ func TestLoadVersionMixedSequence(t *testing.T) { require.Equal(t, hashAtV2, s3.RootHash()) v, ok = s3.Get(key) require.True(t, ok) - require.Equal(t, []byte{0x02}, v) + require.Equal(t, padLeft32(0x02), v) require.NoError(t, s3.Close()) } @@ -1230,7 +1233,7 @@ func TestSnapshotPreservesAllKeyTypes(t *testing.T) { slot := Slot{0xCD} pairs := []*proto.KVPair{ - {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), Value: []byte{0x11}}, + {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), Value: padLeft32(0x11)}, {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]), Value: []byte{0, 0, 0, 0, 0, 0, 0, 7}}, {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:]), Value: []byte{0x60, 0x80}}, } @@ -1257,7 +1260,7 @@ func TestSnapshotPreservesAllKeyTypes(t *testing.T) { storageKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) v, ok := s2.Get(storageKey) require.True(t, ok) - require.Equal(t, []byte{0x11}, v) + require.Equal(t, padLeft32(0x11), v) nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) v, ok = s2.Get(nonceKey) @@ -1329,7 +1332,7 @@ func TestReopenAfterDeletes(t *testing.T) { cs := &proto.NamedChangeSet{ Name: "evm", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ - {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), Value: []byte{0x11}}, + {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), Value: padLeft32(0x11)}, {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]), Value: []byte{0, 0, 0, 0, 0, 0, 0, 42}}, {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]), Value: ch[:]}, {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:]), Value: []byte{0x60, 0x80}}, @@ -1409,14 +1412,17 @@ func TestWALTruncationThenRollback(t *testing.T) { for i := 1; i <= 5; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(byte(i)), slotN(byte(i)))) - val, found := s.Get(key) + var val []byte + var found bool + val, found = s.Get(key) require.True(t, found, "key at block %d should exist after rollback to v5", i) - require.Equal(t, []byte{byte(i)}, val) + require.Equal(t, padLeft32(byte(i)), val) } for i := 6; i <= 10; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(byte(i)), slotN(byte(i)))) - _, found := s.Get(key) + var found bool + _, found = s.Get(key) require.False(t, found, "key at block %d should NOT exist after rollback to v5", i) } @@ -1456,9 +1462,11 @@ func TestReopenAfterSnapshotAndTruncation(t *testing.T) { for i := 1; i <= 10; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(byte(i)), slotN(byte(i)))) - val, found := s2.Get(key) + var val []byte + var found bool + val, found = s2.Get(key) require.True(t, found, "key at block %d should exist after reopen", i) - require.Equal(t, []byte{byte(i)}, val) + require.Equal(t, padLeft32(byte(i)), val) } } @@ -1584,7 +1592,7 @@ func TestWALDirectoryDeleted(t *testing.T) { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(Address{0x03}, Slot{0x03})) val, found := s2.Get(key) require.True(t, found) - require.Equal(t, []byte{0xCC}, val) + require.Equal(t, padLeft32(0xCC), val) } func TestLocalMetaCorruption(t *testing.T) { @@ -1714,10 +1722,10 @@ func TestAccountRowDeletePersistsAfterReopen(t *testing.T) { Name: "evm", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]), Value: []byte{0, 0, 0, 0, 0, 0, 0, 5}}, - {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]), Value: make([]byte, CodeHashLen)}, + {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]), Value: make([]byte, vtype.CodeHashLength)}, }}, } - ch := CodeHash{0xAA} + ch := vtype.CodeHash{0xAA} cs1.Changeset.Pairs[1].Value = ch[:] require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) _, err = s.Commit() @@ -1869,7 +1877,7 @@ func TestRollbackOnReadOnlyStore(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x01), slotN(0x01))), - []byte{0x11}, false, + padLeft32(0x11), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1892,7 +1900,7 @@ func TestRollbackToCurrentVersion(t *testing.T) { addr := addrN(0x02) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x22}, false) + cs := makeChangeSet(key, padLeft32(0x22), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) // v1 + snapshot @@ -1905,7 +1913,7 @@ func TestRollbackToCurrentVersion(t *testing.T) { val, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{0x22}, val) + require.Equal(t, padLeft32(0x22), val) } func TestRollbackToFutureVersionFails(t *testing.T) { @@ -1916,7 +1924,7 @@ func TestRollbackToFutureVersionFails(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x03), slotN(0x01))), - []byte{0x33}, false, + padLeft32(0x33), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) // v1 @@ -1933,13 +1941,13 @@ func TestRollbackDiscardsUncommittedPendingWrites(t *testing.T) { addr := addrN(0x04) key1 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs1 := makeChangeSet(key1, []byte{0x44}, false) + cs1 := makeChangeSet(key1, padLeft32(0x44), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) commitAndCheck(t, s) // v1 // Apply but do NOT commit. key2 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x02))) - cs2 := makeChangeSet(key2, []byte{0x55}, false) + cs2 := makeChangeSet(key2, padLeft32(0x55), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) require.NoError(t, s.Rollback(1)) @@ -1947,7 +1955,7 @@ func TestRollbackDiscardsUncommittedPendingWrites(t *testing.T) { val, found := s.Get(key1) require.True(t, found) - require.Equal(t, []byte{0x44}, val) + require.Equal(t, padLeft32(0x44), val) _, found = s.Get(key2) require.False(t, found, "uncommitted pending write should be discarded after rollback") @@ -1962,11 +1970,11 @@ func TestRollbackThenNewTimeline(t *testing.T) { addr := addrN(0x05) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs1 := makeChangeSet(key, []byte{0x11}, false) + cs1 := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) commitAndCheck(t, s) // v1 - cs2 := makeChangeSet(key, []byte{0x22}, false) + cs2 := makeChangeSet(key, padLeft32(0x22), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) commitAndCheck(t, s) // v2 @@ -1974,7 +1982,7 @@ func TestRollbackThenNewTimeline(t *testing.T) { require.Equal(t, int64(1), s.Version()) // Write new data in the alternate timeline. - cs3 := makeChangeSet(key, []byte{0xFF}, false) + cs3 := makeChangeSet(key, padLeft32(0xFF), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs3})) v, err := s.Commit() require.NoError(t, err) @@ -1982,7 +1990,7 @@ func TestRollbackThenNewTimeline(t *testing.T) { val, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{0xFF}, val) + require.Equal(t, padLeft32(0xFF), val) } func TestRollbackPreservesWALContinuity(t *testing.T) { @@ -1999,7 +2007,7 @@ func TestRollbackPreservesWALContinuity(t *testing.T) { addr := addrN(0x06) for i := 1; i <= 4; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))) - cs := makeChangeSet(key, []byte{byte(i)}, false) + cs := makeChangeSet(key, padLeft32(byte(i)), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err := s.Commit() require.NoError(t, err) @@ -2010,7 +2018,7 @@ func TestRollbackPreservesWALContinuity(t *testing.T) { // Continue committing. for i := 5; i <= 6; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))) - cs := makeChangeSet(key, []byte{byte(i)}, false) + cs := makeChangeSet(key, padLeft32(byte(i)), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err := s.Commit() require.NoError(t, err) @@ -2034,7 +2042,7 @@ func TestWriteSnapshotOnReadOnlyStore(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x01), slotN(0x01))), - []byte{0x11}, false, + padLeft32(0x11), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -2063,7 +2071,7 @@ func TestWriteSnapshotWhileReadOnlyCloneActive(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x07), slotN(0x01))), - []byte{0x77}, false, + padLeft32(0x77), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -2078,7 +2086,7 @@ func TestWriteSnapshotWhileReadOnlyCloneActive(t *testing.T) { // RO clone should still work. val, found := ro.Get(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x07), slotN(0x01)))) require.True(t, found) - require.Equal(t, []byte{0x77}, val) + require.Equal(t, padLeft32(0x77), val) require.NoError(t, s.Close()) } @@ -2088,7 +2096,7 @@ func TestWriteSnapshotDirParameterIgnored(t *testing.T) { cs := makeChangeSet( evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x08), slotN(0x01))), - []byte{0x88}, false, + padLeft32(0x88), false, ) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -2099,5 +2107,5 @@ func TestWriteSnapshotDirParameterIgnored(t *testing.T) { // Verify snapshot was created in the correct location (not the passed dir). val, found := s.Get(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0x08), slotN(0x01)))) require.True(t, found) - require.Equal(t, []byte{0x88}, val) + require.Equal(t, padLeft32(0x88), val) } diff --git a/sei-db/state_db/sc/flatkv/store.go b/sei-db/state_db/sc/flatkv/store.go index 632382635a..bb655a6cb0 100644 --- a/sei-db/state_db/sc/flatkv/store.go +++ b/sei-db/state_db/sc/flatkv/store.go @@ -18,6 +18,7 @@ import ( seidbtypes "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" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" "github.com/sei-protocol/sei-chain/sei-db/wal" "github.com/sei-protocol/seilog" @@ -52,27 +53,6 @@ const ( // 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 - value []byte - isDelete bool -} - -// pendingAccountWrite tracks a buffered account write. -// Uses AccountValue structure: balance(32) || nonce(8) || codehash(32) -// -// Account-field deletes (KVPair.Delete for nonce or codehash) reset the -// individual field within value. When all fields become zero after resets, -// isDelete is set to true and the accountDB row is physically deleted at -// commit time. Any subsequent write to the same address within the same -// block clears isDelete back to false (row is recreated). -type pendingAccountWrite struct { - addr Address - value AccountValue - isDelete bool // true = row will be physically deleted (all fields zero) -} - // CommitStore implements flatkv.Store for EVM state storage. // NOT thread-safe; callers must serialize all operations. type CommitStore struct { @@ -83,10 +63,10 @@ type CommitStore struct { // Five separate PebbleDB instances metadataDB seidbtypes.KeyValueDB // Global version + LtHash watermark - accountDB seidbtypes.KeyValueDB // addr(20) → AccountValue (40 or 72 bytes) - codeDB seidbtypes.KeyValueDB // addr(20) → bytecode - storageDB seidbtypes.KeyValueDB // addr(20)||slot(32) → value(32) - legacyDB seidbtypes.KeyValueDB // Legacy data for backward compatibility + accountDB seidbtypes.KeyValueDB // addr(20) → vtype.AccountData + codeDB seidbtypes.KeyValueDB // addr(20) → vtype.CodeData + storageDB seidbtypes.KeyValueDB // addr(20)||slot(32) → vtype.StorageData + legacyDB seidbtypes.KeyValueDB // key → vtype.LegacyValue // Per-DB committed version, keyed by DB dir name (e.g. accountDBDir). localMeta map[string]*LocalMeta @@ -102,14 +82,14 @@ type CommitStore struct { 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 - accountWrites map[string]*pendingAccountWrite - codeWrites map[string]*pendingKVWrite - storageWrites map[string]*pendingKVWrite - legacyWrites map[string]*pendingKVWrite - - changelog wal.ChangelogWAL + accountWrites map[string]*vtype.AccountData + codeWrites map[string]*vtype.CodeData + storageWrites map[string]*vtype.StorageData + legacyWrites map[string]*vtype.LegacyData + + changelog wal.ChangelogWAL + + // Changes to feed into the WAL at the next commit. pendingChangeSets []*proto.NamedChangeSet lastSnapshotTime time.Time @@ -168,10 +148,10 @@ func NewCommitStore( cancel: cancel, config: *cfg, 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), + accountWrites: make(map[string]*vtype.AccountData), + codeWrites: make(map[string]*vtype.CodeData), + storageWrites: make(map[string]*vtype.StorageData), + legacyWrites: make(map[string]*vtype.LegacyData), pendingChangeSets: make([]*proto.NamedChangeSet, 0), committedLtHash: lthash.New(), workingLtHash: lthash.New(), diff --git a/sei-db/state_db/sc/flatkv/store_apply.go b/sei-db/state_db/sc/flatkv/store_apply.go new file mode 100644 index 0000000000..df13fe4d3a --- /dev/null +++ b/sei-db/state_db/sc/flatkv/store_apply.go @@ -0,0 +1,331 @@ +package flatkv + +import ( + "fmt" + + "github.com/sei-protocol/sei-chain/sei-db/common/evm" + "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" +) + +// ApplyChangeSets buffers EVM changesets and updates LtHash. +func (s *CommitStore) ApplyChangeSets(changeSets []*proto.NamedChangeSet) error { + if s.readOnly { + return errReadOnly + } + + /////////// + // Setup // + /////////// + s.phaseTimer.SetPhase("apply_change_sets_prepare") + + changesByType, err := sortChangeSets(changeSets) + if err != nil { + return fmt.Errorf("failed to sort change sets: %w", err) + } + + blockHeight := s.committedVersion + 1 + + //////////////////// + // Batch Read Old // + //////////////////// + s.phaseTimer.SetPhase("apply_change_sets_batch_read") + + storageOld, accountOld, codeOld, legacyOld, err := s.batchReadOldValues(changesByType) + if err != nil { + return fmt.Errorf("failed to batch read old values: %w", err) + } + + ////////////////// + // Gather Pairs // + ////////////////// + s.phaseTimer.SetPhase("apply_change_sets_gather_pairs") + + // Gather account pairs + accountWrites, err := mergeAccountUpdates( + changesByType[evm.EVMKeyNonce], + changesByType[evm.EVMKeyCodeHash], + nil, // TODO: update this when we add a balance key! + ) + if err != nil { + return fmt.Errorf("failed to gather account updates: %w", err) + } + newAccountValues := deriveNewAccountValues(accountWrites, accountOld, blockHeight) + accountPairs := gatherLTHashPairs(newAccountValues, accountOld) + storeWrites(s.accountWrites, newAccountValues) + + // Gather storage pairs + storageChanges, err := processStorageChanges(changesByType[evm.EVMKeyStorage], blockHeight) + if err != nil { + return fmt.Errorf("failed to parse storage changes: %w", err) + } + storagePairs := gatherLTHashPairs(storageChanges, storageOld) + storeWrites(s.storageWrites, storageChanges) + + // Gather code pairs + codeChanges, err := processCodeChanges(changesByType[evm.EVMKeyCode], blockHeight) + if err != nil { + return fmt.Errorf("failed to parse code changes: %w", err) + } + codePairs := gatherLTHashPairs(codeChanges, codeOld) + storeWrites(s.codeWrites, codeChanges) + + // Gather legacy pairs + legacyChanges, err := processLegacyChanges(changesByType[evm.EVMKeyLegacy]) + if err != nil { + return fmt.Errorf("failed to parse legacy changes: %w", err) + } + legacyPairs := gatherLTHashPairs(legacyChanges, legacyOld) + storeWrites(s.legacyWrites, legacyChanges) + + //////////////////// + // Compute LTHash // + //////////////////// + s.phaseTimer.SetPhase("apply_change_compute_lt_hash") + + 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 + + ////////////// + // Finalize // + ////////////// + + // Now that we've made it through the batch without errors, we can add the change sets to the pending change sets. + s.pendingChangeSets = append(s.pendingChangeSets, changeSets...) + + s.phaseTimer.SetPhase("apply_change_done") + return nil +} + +// Store a map of writes into a map of pending writes. +func storeWrites[T vtype.VType]( + // the map that is accumulating writes + pendingWrites map[string]T, + // new writes that need to be applied to the pendingWrites map + newValues map[string]T, +) { + for keyStr, newValue := range newValues { + pendingWrites[keyStr] = newValue + } +} + +// Sort the change sets by type. This method only returns an error if +func sortChangeSets(changeSets []*proto.NamedChangeSet) (map[evm.EVMKeyKind]map[string][]byte, error) { + result := make(map[evm.EVMKeyKind]map[string][]byte) + + for _, cs := range changeSets { + if cs.Changeset.Pairs == nil { + continue + } + for _, pair := range cs.Changeset.Pairs { + + kind, keyBytes := evm.ParseEVMKey(pair.Key) + + // evm.ParseEVMKey() should return a valid key type 100% of the time, unless we add a new key but + // forget to update IsSupportedKeyType() and associated code. This is a sanity check. + if !IsSupportedKeyType(kind) { + return nil, fmt.Errorf("unsupported key type: %v", kind) + } + + keyStr := string(keyBytes) + + kindMap, ok := result[kind] + if !ok { + kindMap = make(map[string][]byte) + result[kind] = kindMap + } + + if pair.Delete { + kindMap[keyStr] = nil + } else { + kindMap[keyStr] = pair.Value + } + } + } + + return result, nil +} + +// Process incoming storage changes into a form appropriate for hashing and insertion into the DB. +func processStorageChanges( + rawChanges map[string][]byte, + blockHeight int64, +) (map[string]*vtype.StorageData, error) { + result := make(map[string]*vtype.StorageData) + + for keyStr, rawChange := range rawChanges { + if rawChange == nil { + // Deletion is equivalent to setting the storage value to a zero value + result[keyStr] = vtype.NewStorageData().SetBlockHeight(blockHeight).SetValue(&[32]byte{}) + } else { + value, err := vtype.ParseStorageValue(rawChange) + if err != nil { + return nil, fmt.Errorf("failed to parse storage value: %w", err) + } + result[keyStr] = vtype.NewStorageData().SetBlockHeight(blockHeight).SetValue(value) + } + } + + return result, nil +} + +// Process incoming code changes into a form appropriate for hashing and insertion into the DB. +func processCodeChanges( + rawChanges map[string][]byte, + blockHeight int64, +) (map[string]*vtype.CodeData, error) { + result := make(map[string]*vtype.CodeData) + + for keyStr, rawChange := range rawChanges { + if rawChange == nil { + // Deletion is equivalent to setting the code to a zero value + result[keyStr] = vtype.NewCodeData().SetBlockHeight(blockHeight).SetBytecode(nil) + } else { + result[keyStr] = vtype.NewCodeData().SetBlockHeight(blockHeight).SetBytecode(rawChange) + } + } + return result, nil +} + +// Process incoming legacy changes into a form appropriate for hashing and insertion into the DB. +func processLegacyChanges(rawChanges map[string][]byte) (map[string]*vtype.LegacyData, error) { + result := make(map[string]*vtype.LegacyData) + + for keyStr, rawChange := range rawChanges { + if rawChange == nil { + // Deletion is equivalent to setting the legacy value to a zero value + result[keyStr] = vtype.NewLegacyData().SetValue(nil) + } else { + result[keyStr] = vtype.NewLegacyData().SetValue(rawChange) + } + } + return result, nil +} + +// Gather LtHash pairs for a DB. +func gatherLTHashPairs[T vtype.VType]( + newValues map[string]T, + oldValues map[string]T, +) []lthash.KVPairWithLastValue { + + pairs := make([]lthash.KVPairWithLastValue, 0, len(newValues)) + + for keyStr, newValue := range newValues { + var oldValue = oldValues[keyStr] + + var newBytes []byte + if !newValue.IsDelete() { + newBytes = newValue.Serialize() + } + + var oldBytes []byte + if !oldValue.IsDelete() { + oldBytes = oldValue.Serialize() + } + + pairs = append(pairs, lthash.KVPairWithLastValue{ + Key: []byte(keyStr), + Value: newBytes, + LastValue: oldBytes, + Delete: newValue.IsDelete(), + }) + } + + return pairs +} + +// Merge account updates down into a single update per account. +func mergeAccountUpdates( + nonceChanges map[string][]byte, + codeHashChanges map[string][]byte, + balanceChanges map[string][]byte, +) (map[string]*vtype.PendingAccountWrite, error) { + + // PendingAccountWrite objects are well behaved when nil, no need to bootstrap map entries. + updates := make(map[string]*vtype.PendingAccountWrite) + + for key, nonceChange := range nonceChanges { + if nonceChange == nil { + // Deletion is equivalent to setting the nonce to 0 + updates[key] = updates[key].SetNonce(0) + } else { + nonce, err := vtype.ParseNonce(nonceChange) + if err != nil { + return nil, fmt.Errorf("invalid nonce value: %w", err) + } + updates[key] = updates[key].SetNonce(nonce) + } + } + + for key, codeHashChange := range codeHashChanges { + if codeHashChange == nil { + // Deletion is equivalent to setting the code hash to a zero hash + var zero vtype.CodeHash + updates[key] = updates[key].SetCodeHash(&zero) + } else { + codeHash, err := vtype.ParseCodeHash(codeHashChange) + if err != nil { + return nil, fmt.Errorf("invalid codehash value: %w", err) + } + updates[key] = updates[key].SetCodeHash(codeHash) + } + } + + for key, balanceChange := range balanceChanges { + if balanceChange == nil { + // Deletion is equivalent to setting the balance to a zero balance + var zero vtype.Balance + updates[key] = updates[key].SetBalance(&zero) + } else { + balance, err := vtype.ParseBalance(balanceChange) + if err != nil { + return nil, fmt.Errorf("invalid balance value: %w", err) + } + updates[key] = updates[key].SetBalance(balance) + } + } + return updates, nil +} + +// Combine the pending account writes with prior values to determine the new account values. +// +// We need to take this step because accounts are split into multiple fields, and its possible to overwrite just a +// single field (thus requring us to copy the unmodified fields from the prior value). +func deriveNewAccountValues( + pendingWrites map[string]*vtype.PendingAccountWrite, + oldValues map[string]*vtype.AccountData, + blockHeight int64, +) map[string]*vtype.AccountData { + result := make(map[string]*vtype.AccountData) + + for addrStr, pendingWrite := range pendingWrites { + oldValue := oldValues[addrStr] + + newValue := pendingWrite.Merge(oldValue, blockHeight) + result[addrStr] = newValue + } + return result +} 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 840f9bc968..c51db83b12 100644 --- a/sei-db/state_db/sc/flatkv/store_meta_test.go +++ b/sei-db/state_db/sc/flatkv/store_meta_test.go @@ -61,7 +61,7 @@ func TestStoreCommitBatchesUpdatesLocalMeta(t *testing.T) { slot := Slot{0x34} key := memiavlStorageKey(addr, slot) - cs := makeChangeSet(key, []byte{0x56}, false) + cs := makeChangeSet(key, padLeft32(0x56), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) v := commitAndCheck(t, s) require.Equal(t, int64(1), v) diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index fcf6bfc052..c562731b70 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -7,82 +7,59 @@ import ( errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/evm" - seidbtypes "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" ) // Get returns the value for the given memiavl key. // Returns (value, true) if found, (nil, false) if not found. +// Panics on I/O errors or unsupported key types. func (s *CommitStore) Get(key []byte) ([]byte, bool) { kind, keyBytes := evm.ParseEVMKey(key) - if kind == evm.EVMKeyUnknown { - return nil, false + if !IsSupportedKeyType(kind) { + panic(fmt.Sprintf("flatkv: unsupported key type: %v", kind)) } switch kind { case evm.EVMKeyStorage: value, err := s.getStorageValue(keyBytes) if err != nil { - return nil, false + panic(fmt.Sprintf("flatkv: Get storage key %x: %v", key, err)) } return value, value != nil case evm.EVMKeyNonce, evm.EVMKeyCodeHash: - // Account data: keyBytes = addr(20) - // accountDB stores AccountValue at key=addr(20) - addr, ok := AddressFromBytes(keyBytes) - if !ok { - return nil, false - } - - // Check pending writes first - if paw, found := s.accountWrites[string(addr[:])]; found { - if paw.isDelete { - return nil, false - } - if kind == evm.EVMKeyNonce { - nonce := make([]byte, NonceLen) - binary.BigEndian.PutUint64(nonce, paw.value.Nonce) - return nonce, true - } - // CodeHash - if paw.value.CodeHash == (CodeHash{}) { - return nil, false - } - return paw.value.CodeHash[:], true - } - - // Read from accountDB - encoded, err := s.accountDB.Get(AccountKey(addr)) + accountData, err := s.getAccountData(keyBytes) if err != nil { - return nil, false + panic(fmt.Sprintf("flatkv: Get account key %x: %v", key, err)) } - av, err := DecodeAccountValue(encoded) - if err != nil { + if accountData == nil || accountData.IsDelete() { return nil, false } if kind == evm.EVMKeyNonce { - nonce := make([]byte, NonceLen) - binary.BigEndian.PutUint64(nonce, av.Nonce) - return nonce, true + nonceBytes := make([]byte, vtype.NonceLen) + binary.BigEndian.PutUint64(nonceBytes, accountData.GetNonce()) + return nonceBytes, true } // CodeHash - if av.CodeHash == (CodeHash{}) { + codeHash := accountData.GetCodeHash() + var zeroCodeHash vtype.CodeHash + if *codeHash == zeroCodeHash { return nil, false } - return av.CodeHash[:], true + return codeHash[:], true case evm.EVMKeyCode: value, err := s.getCodeValue(keyBytes) if err != nil { - return nil, false + panic(fmt.Sprintf("flatkv: Get code key %x: %v", key, err)) } return value, value != nil case evm.EVMKeyLegacy: value, err := s.getLegacyValue(keyBytes) if err != nil { - return nil, false + panic(fmt.Sprintf("flatkv: Get legacy key %x: %v", key, err)) } return value, value != nil @@ -91,7 +68,52 @@ func (s *CommitStore) Get(key []byte) ([]byte, bool) { } } +// GetBlockHeightModified returns the block height at which the key was last modified. +// If not found, returns (-1, false, nil). +func (s *CommitStore) GetBlockHeightModified(key []byte) (int64, bool, error) { + kind, keyBytes := evm.ParseEVMKey(key) + if !IsSupportedKeyType(kind) { + // Only possible if a new type is added to evm.ParseEVMKey() without updating code to handle that type. + return -1, false, fmt.Errorf("unsupported key type: %v", kind) + } + + switch kind { + case evm.EVMKeyStorage: + sd, err := s.getStorageData(keyBytes) + if err != nil { + return -1, false, err + } + if sd == nil || sd.IsDelete() { + return -1, false, nil + } + return sd.GetBlockHeight(), true, nil + + case evm.EVMKeyNonce, evm.EVMKeyCodeHash: + accountData, err := s.getAccountData(keyBytes) + if err != nil { + return -1, false, err + } + if accountData == nil || accountData.IsDelete() { + return -1, false, nil + } + return accountData.GetBlockHeight(), true, nil + + case evm.EVMKeyCode: + cd, err := s.getCodeData(keyBytes) + if err != nil { + return -1, false, err + } + if cd == nil || cd.IsDelete() { + return -1, false, nil + } + return cd.GetBlockHeight(), true, nil + default: + return -1, false, fmt.Errorf("block height modified not tracked for key type: %v", kind) + } +} + // Has reports whether the given memiavl key exists. +// Panics on I/O errors or unsupported key types. func (s *CommitStore) Has(key []byte) bool { _, found := s.Get(key) return found @@ -177,67 +199,103 @@ func (s *CommitStore) IteratorByPrefix(prefix []byte) Iterator { // Internal Getters (used by ApplyChangeSets for LtHash computation) // ============================================================================= -// getAccountValue loads AccountValue from pending writes or DB. -// Returns zero AccountValue if not found (new account) or if the pending -// write is marked for deletion (row logically absent). -// Returns error if existing data is corrupted (decode fails) or I/O error occurs. -func (s *CommitStore) getAccountValue(addr Address) (AccountValue, error) { - // Check pending writes first - if paw, ok := s.accountWrites[string(addr[:])]; ok { - if paw.isDelete { - return AccountValue{}, nil +func (s *CommitStore) getAccountData(keyBytes []byte) (*vtype.AccountData, error) { + addr, ok := AddressFromBytes(keyBytes) + if !ok { + return nil, nil + } + + if accountValue, found := s.accountWrites[string(addr[:])]; found { + return accountValue, nil + } + + encoded, err := s.accountDB.Get(AccountKey(addr)) + if err != nil { + if errorutils.IsNotFound(err) { + return nil, nil } - return paw.value, nil + return nil, fmt.Errorf("accountDB I/O error for key %x: %w", addr, err) + } + return vtype.DeserializeAccountData(encoded) +} + +func (s *CommitStore) getStorageData(keyBytes []byte) (*vtype.StorageData, error) { + pendingWrite, hasPending := s.storageWrites[string(keyBytes)] + if hasPending { + return pendingWrite, nil } - // Read from accountDB - value, err := s.accountDB.Get(AccountKey(addr)) + value, err := s.storageDB.Get(keyBytes) if err != nil { if errorutils.IsNotFound(err) { - return AccountValue{}, nil // New account + return nil, nil } - return AccountValue{}, fmt.Errorf("accountDB I/O error for addr %x: %w", addr, err) + return nil, fmt.Errorf("storageDB I/O error for key %x: %w", keyBytes, err) } + return vtype.DeserializeStorageData(value) +} - av, err := DecodeAccountValue(value) +func (s *CommitStore) getStorageValue(key []byte) ([]byte, error) { + sd, err := s.getStorageData(key) if err != nil { - return AccountValue{}, fmt.Errorf("corrupted AccountValue for addr %x: %w", addr, err) + return nil, err } - return av, nil + if sd == nil || sd.IsDelete() { + return nil, nil + } + return sd.GetValue()[:], nil } -// getKVValue returns the value from pending writes or the backing DB. -// Returns (nil, nil) if not found. Returns (nil, error) on I/O error. -func (s *CommitStore) getKVValue( - key []byte, - writes map[string]*pendingKVWrite, - db seidbtypes.KeyValueDB, - dbName string, -) ([]byte, error) { - if pw, ok := writes[string(key)]; ok { - if pw.isDelete { - return nil, nil - } - return pw.value, nil +func (s *CommitStore) getCodeData(keyBytes []byte) (*vtype.CodeData, error) { + pendingWrite, hasPending := s.codeWrites[string(keyBytes)] + if hasPending { + return pendingWrite, nil } - value, err := db.Get(key) + + value, err := s.codeDB.Get(keyBytes) if err != nil { if errorutils.IsNotFound(err) { return nil, nil } - return nil, fmt.Errorf("%s I/O error for key %x: %w", dbName, key, err) + return nil, fmt.Errorf("codeDB I/O error for key %x: %w", keyBytes, err) } - return value, nil + return vtype.DeserializeCodeData(value) } -func (s *CommitStore) getStorageValue(key []byte) ([]byte, error) { - return s.getKVValue(key, s.storageWrites, s.storageDB, "storageDB") +func (s *CommitStore) getCodeValue(key []byte) ([]byte, error) { + cd, err := s.getCodeData(key) + if err != nil { + return nil, err + } + if cd == nil || cd.IsDelete() { + return nil, nil + } + return cd.GetBytecode(), nil } -func (s *CommitStore) getCodeValue(key []byte) ([]byte, error) { - return s.getKVValue(key, s.codeWrites, s.codeDB, "codeDB") +func (s *CommitStore) getLegacyData(keyBytes []byte) (*vtype.LegacyData, error) { + pendingWrite, hasPending := s.legacyWrites[string(keyBytes)] + if hasPending { + return pendingWrite, nil + } + + value, err := s.legacyDB.Get(keyBytes) + if err != nil { + if errorutils.IsNotFound(err) { + return nil, nil + } + return nil, fmt.Errorf("legacyDB I/O error for key %x: %w", keyBytes, err) + } + return vtype.DeserializeLegacyData(value) } func (s *CommitStore) getLegacyValue(key []byte) ([]byte, error) { - return s.getKVValue(key, s.legacyWrites, s.legacyDB, "legacyDB") + ld, err := s.getLegacyData(key) + if err != nil { + return nil, err + } + if ld == nil || ld.IsDelete() { + return nil, nil + } + return ld.GetValue(), nil } 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 958e73a801..39a056fd2f 100644 --- a/sei-db/state_db/sc/flatkv/store_read_test.go +++ b/sei-db/state_db/sc/flatkv/store_read_test.go @@ -8,6 +8,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/evm" "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/vtype" "github.com/stretchr/testify/require" ) @@ -21,7 +22,7 @@ func TestStoreGetPendingWrites(t *testing.T) { addr := Address{0x11} slot := Slot{0x22} - value := []byte{0x33} + value := padLeft32(0x33) key := memiavlStorageKey(addr, slot) // No data initially @@ -55,7 +56,7 @@ func TestStoreGetPendingDelete(t *testing.T) { key := memiavlStorageKey(addr, slot) // Write and commit - cs1 := makeChangeSet(key, []byte{0x66}, false) + cs1 := makeChangeSet(key, padLeft32(0x66), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) commitAndCheck(t, s) @@ -92,8 +93,9 @@ func TestStoreGetNonStorageKeys(t *testing.T) { evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:]), } + var found bool for _, key := range nonStorageKeys { - _, found := s.Get(key) + _, found = s.Get(key) require.False(t, found, "non-storage keys should not be found before write") } } @@ -107,15 +109,17 @@ func TestStoreHas(t *testing.T) { key := memiavlStorageKey(addr, slot) // Initially not found - require.False(t, s.Has(key)) + found := s.Has(key) + require.False(t, found) // Write and commit - cs := makeChangeSet(key, []byte{0xAA}, false) + cs := makeChangeSet(key, padLeft32(0xAA), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) // Now should exist - require.True(t, s.Has(key)) + found = s.Has(key) + require.True(t, found) } // ============================================================================= @@ -191,14 +195,14 @@ func TestStoreDelete(t *testing.T) { key := memiavlStorageKey(addr, slot) // Write - cs1 := makeChangeSet(key, []byte{0x77}, false) + cs1 := makeChangeSet(key, padLeft32(0x77), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) commitAndCheck(t, s) // Verify exists got, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{0x77}, got) + require.Equal(t, padLeft32(0x77), got) // Delete cs2 := makeChangeSet(key, nil, true) @@ -231,7 +235,7 @@ func TestStoreIteratorSingleKey(t *testing.T) { addr := Address{0xAA} slot := Slot{0xBB} - value := []byte{0xCC} + value := padLeft32(0xCC) memiavlKey := memiavlStorageKey(addr, slot) internalKey := StorageKey(addr, slot) // addr(20) || slot(32) @@ -272,7 +276,7 @@ func TestStoreIteratorMultipleKeys(t *testing.T) { pairs := make([]*proto.KVPair, len(entries)) for i, e := range entries { key := memiavlStorageKey(addr, e.slot) - pairs[i] = &proto.KVPair{Key: key, Value: []byte{e.value}} + pairs[i] = &proto.KVPair{Key: key, Value: padLeft32(e.value)} } cs := &proto.NamedChangeSet{ @@ -326,7 +330,7 @@ func TestStoreStoragePrefixIteration(t *testing.T) { for i := byte(1); i <= 3; i++ { slot := Slot{i} key := memiavlStorageKey(addr, slot) - cs := makeChangeSet(key, []byte{i * 10}, false) + cs := makeChangeSet(key, padLeft32(i*10), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) } commitAndCheck(t, s) @@ -356,7 +360,7 @@ func TestStoreIteratorByPrefixAddress(t *testing.T) { for i := byte(1); i <= 3; i++ { slot := Slot{i} key := memiavlStorageKey(addr1, slot) - cs := makeChangeSet(key, []byte{i * 10}, false) + cs := makeChangeSet(key, padLeft32(i*10), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) } @@ -364,7 +368,7 @@ func TestStoreIteratorByPrefixAddress(t *testing.T) { for i := byte(1); i <= 2; i++ { slot := Slot{i} key := memiavlStorageKey(addr2, slot) - cs := makeChangeSet(key, []byte{i * 20}, false) + cs := makeChangeSet(key, padLeft32(i*20), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) } @@ -423,7 +427,7 @@ func TestGetAllKeyTypesFromCommittedDB(t *testing.T) { // Storage got, found := s.Get(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot))) require.True(t, found, "storage should be found") - require.Equal(t, storageVal, got) + require.Equal(t, padLeft32(0x42), got) // Nonce got, found = s.Get(evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:])) @@ -446,11 +450,16 @@ func TestGetAllKeyTypesFromCommittedDB(t *testing.T) { require.Equal(t, legacyVal, got) // Has should match - require.True(t, s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)))) - require.True(t, s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]))) - require.True(t, s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]))) - require.True(t, s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:]))) - require.True(t, s.Has(legacyKey)) + found = s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot))) + require.True(t, found) + found = s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:])) + require.True(t, found) + found = s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:])) + require.True(t, found) + found = s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:])) + require.True(t, found) + found = s.Has(legacyKey) + require.True(t, found) } func TestGetNonceFromCommittedEOA(t *testing.T) { @@ -473,8 +482,10 @@ func TestGetNonceFromCommittedEOA(t *testing.T) { _, found = s.Get(chKey) require.False(t, found, "codehash should NOT be found for EOA") - require.True(t, s.Has(nonceKey)) - require.False(t, s.Has(chKey)) + found = s.Has(nonceKey) + require.True(t, found) + found = s.Has(chKey) + require.False(t, found) } func TestGetCodeHashFromCommittedContract(t *testing.T) { @@ -499,8 +510,10 @@ func TestGetCodeHashFromCommittedContract(t *testing.T) { require.True(t, found) require.Equal(t, uint64(1), binary.BigEndian.Uint64(got)) - require.True(t, s.Has(chKey)) - require.True(t, s.Has(nonceKey)) + found = s.Has(chKey) + require.True(t, found) + found = s.Has(nonceKey) + require.True(t, found) } func TestGetCodeFromCommittedDB(t *testing.T) { @@ -542,21 +555,37 @@ func TestGetUnknownKeyTypes(t *testing.T) { s := setupTestStore(t) defer s.Close() - cases := []struct { + // Nil and empty keys map to EVMKeyEmpty/EVMKeyUnknown, which is + // unsupported and panics under StrictKeyTypeCheck. + for _, tc := range []struct { name string key []byte }{ {"nil key", nil}, {"empty key", []byte{}}, + } { + t.Run(tc.name, func(t *testing.T) { + require.Panics(t, func() { s.Get(tc.key) }) + require.Panics(t, func() { s.Has(tc.key) }) + }) + } + + // Non-empty keys that don't match a known prefix are classified as + // EVMKeyLegacy, which is a supported type — Get/Has should not panic. + for _, tc := range []struct { + name string + key []byte + }{ {"single byte", []byte{0xFF}}, {"random bytes", []byte{0xDE, 0xAD, 0xBE, 0xEF}}, {"short nonce-like (2 bytes)", []byte{0x04, 0x01}}, - } - for _, tc := range cases { + } { t.Run(tc.name, func(t *testing.T) { - _, found := s.Get(tc.key) + val, found := s.Get(tc.key) + require.False(t, found) + require.Nil(t, val) + found = s.Has(tc.key) require.False(t, found) - require.False(t, s.Has(tc.key)) }) } } @@ -591,8 +620,10 @@ func TestGetAccountAfterFullDeletePending(t *testing.T) { _, chFound := s.Get(chKey) require.False(t, chFound, "codehash should not be found after full delete (isDelete=true)") - require.False(t, s.Has(nonceKey)) - require.False(t, s.Has(chKey)) + found := s.Has(nonceKey) + require.False(t, found) + found = s.Has(chKey) + require.False(t, found) } func TestGetAccountAfterFullDeleteCommitted(t *testing.T) { @@ -621,8 +652,10 @@ func TestGetAccountAfterFullDeleteCommitted(t *testing.T) { _, chFound := s.Get(chKey) require.False(t, chFound, "codehash should not be found after full delete + commit") - require.False(t, s.Has(nonceKey)) - require.False(t, s.Has(chKey)) + found := s.Has(nonceKey) + require.False(t, found) + found = s.Has(chKey) + require.False(t, found) } func TestGetAccountAfterPartialDelete(t *testing.T) { @@ -654,7 +687,8 @@ func TestGetAccountAfterPartialDelete(t *testing.T) { // Account row should still exist (EOA encoding) raw, err := s.accountDB.Get(AccountKey(addr)) require.NoError(t, err) - require.Equal(t, accountValueEOALen, len(raw)) + expectedEOALen := vtype.VersionLength + vtype.BlockHeightLength + vtype.BalanceLength + vtype.NonceLength + require.Equal(t, expectedEOALen, len(raw)) } // ============================================================================= @@ -674,17 +708,18 @@ func TestGetAfterOverwrite(t *testing.T) { })) commitAndCheck(t, s) - got, _ := s.Get(key) - require.Equal(t, []byte{0x11}, got) + got, found := s.Get(key) + require.True(t, found) + require.Equal(t, padLeft32(0x11), got) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ namedCS(storagePair(addr, slot, []byte{0x22, 0x33})), })) commitAndCheck(t, s) - got, found := s.Get(key) + got, found = s.Get(key) require.True(t, found) - require.Equal(t, []byte{0x22, 0x33}, got, "should return v2 value after overwrite") + require.Equal(t, padLeft32(0x22, 0x33), got, "should return v2 value after overwrite") } func TestGetAfterDeleteAndRecreate(t *testing.T) { @@ -718,7 +753,7 @@ func TestGetAfterDeleteAndRecreate(t *testing.T) { got, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{0xBB, 0xCC}, got, "should return v3 value after re-create") + require.Equal(t, padLeft32(0xBB, 0xCC), got, "should return v3 value after re-create") } func TestGetAfterReopenAllKeyTypes(t *testing.T) { @@ -763,7 +798,7 @@ func TestGetAfterReopenAllKeyTypes(t *testing.T) { got, found := s2.Get(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot))) require.True(t, found, "storage should survive reopen") - require.Equal(t, []byte{0x42}, got) + require.Equal(t, padLeft32(0x42), got) got, found = s2.Get(evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:])) require.True(t, found, "nonce should survive reopen") @@ -1223,7 +1258,7 @@ func TestReadOnlyGetAllKeyTypes(t *testing.T) { got, found := ro.Get(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot))) require.True(t, found) - require.Equal(t, []byte{0x42}, got) + require.Equal(t, padLeft32(0x42), got) got, found = ro.Get(evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:])) require.True(t, found) @@ -1298,30 +1333,26 @@ func TestGetNilKey(t *testing.T) { s := setupTestStore(t) defer s.Close() - val, found := s.Get(nil) - require.False(t, found) - require.Nil(t, val) + require.Panics(t, func() { s.Get(nil) }) } func TestGetEmptyKey(t *testing.T) { s := setupTestStore(t) defer s.Close() - val, found := s.Get([]byte{}) - require.False(t, found) - require.Nil(t, val) + require.Panics(t, func() { s.Get([]byte{}) }) } func TestHasNilKey(t *testing.T) { s := setupTestStore(t) defer s.Close() - require.False(t, s.Has(nil)) + require.Panics(t, func() { s.Has(nil) }) } func TestHasEmptyKey(t *testing.T) { s := setupTestStore(t) defer s.Close() - require.False(t, s.Has([]byte{})) + require.Panics(t, func() { s.Has([]byte{}) }) } func TestHasForAllKeyTypes(t *testing.T) { @@ -1333,7 +1364,7 @@ func TestHasForAllKeyTypes(t *testing.T) { ch := codeHashN(0xAB) pairs := []*proto.KVPair{ - {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), Value: []byte{0x11}}, + {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), Value: padLeft32(0x11)}, noncePair(addr, 42), codeHashPair(addr, ch), codePair(addr, []byte{0x60, 0x60}), @@ -1345,10 +1376,14 @@ func TestHasForAllKeyTypes(t *testing.T) { require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) - require.True(t, s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)))) - require.True(t, s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]))) - require.True(t, s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]))) - require.True(t, s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:]))) + found := s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot))) + require.True(t, found) + found = s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:])) + require.True(t, found) + found = s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:])) + require.True(t, found) + found = s.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:])) + require.True(t, found) } func TestHasOnPendingDeletes(t *testing.T) { @@ -1359,14 +1394,16 @@ func TestHasOnPendingDeletes(t *testing.T) { slot := slotN(0x01) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) - cs := makeChangeSet(key, []byte{0xAA}, false) + cs := makeChangeSet(key, padLeft32(0xAA), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) - require.True(t, s.Has(key)) + found := s.Has(key) + require.True(t, found) delCS := makeChangeSet(key, nil, true) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{delCS})) - require.False(t, s.Has(key), "Has should return false for pending-deleted key") + found = s.Has(key) + require.False(t, found, "Has should return false for pending-deleted key") } func TestHasOnReadOnlyStore(t *testing.T) { @@ -1376,7 +1413,7 @@ func TestHasOnReadOnlyStore(t *testing.T) { slot := slotN(0x01) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) - cs := makeChangeSet(key, []byte{0xBB}, false) + cs := makeChangeSet(key, padLeft32(0xBB), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1384,8 +1421,10 @@ func TestHasOnReadOnlyStore(t *testing.T) { require.NoError(t, err) defer ro.Close() - require.True(t, ro.Has(key)) - require.False(t, ro.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0xFF), slotN(0xFF))))) + found := ro.Has(key) + require.True(t, found) + found = ro.Has(evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addrN(0xFF), slotN(0xFF)))) + require.False(t, found) require.NoError(t, s.Close()) } @@ -1413,7 +1452,7 @@ func TestGetAfterRollback(t *testing.T) { slot := slotN(0x01) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) - cs1 := makeChangeSet(key, []byte{0x11}, false) + cs1 := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) commitAndCheck(t, s) // v1 @@ -1421,13 +1460,13 @@ func TestGetAfterRollback(t *testing.T) { require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) commitAndCheck(t, s) // v2 - snapshot triggers - cs3 := makeChangeSet(key, []byte{0x33}, false) + cs3 := makeChangeSet(key, padLeft32(0x33), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs3})) commitAndCheck(t, s) // v3 val, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{0x33}, val) + require.Equal(t, padLeft32(0x33), val) require.NoError(t, s.Rollback(2)) require.Equal(t, int64(2), s.Version()) @@ -1454,7 +1493,7 @@ func TestIteratorStartEqualsEnd(t *testing.T) { addr := addrN(0x20) key := memiavlStorageKey(addr, slotN(0x01)) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1472,7 +1511,7 @@ func TestIteratorInterleavedNextPrev(t *testing.T) { addr := addrN(0x21) for i := byte(1); i <= 5; i++ { key := memiavlStorageKey(addr, slotN(i)) - cs := makeChangeSet(key, []byte{i}, false) + cs := makeChangeSet(key, padLeft32(i), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) } commitAndCheck(t, s) @@ -1499,7 +1538,7 @@ func TestIteratorMultipleFirstLastCalls(t *testing.T) { addr := addrN(0x22) for i := byte(1); i <= 3; i++ { key := memiavlStorageKey(addr, slotN(i)) - cs := makeChangeSet(key, []byte{i}, false) + cs := makeChangeSet(key, padLeft32(i), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) } commitAndCheck(t, s) @@ -1529,7 +1568,7 @@ func TestIteratorByPrefixAfterDeletions(t *testing.T) { addr := addrN(0x23) for i := byte(1); i <= 3; i++ { key := memiavlStorageKey(addr, slotN(i)) - cs := makeChangeSet(key, []byte{i * 10}, false) + cs := makeChangeSet(key, padLeft32(i*10), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) } commitAndCheck(t, s) @@ -1558,7 +1597,7 @@ func TestIteratorByPrefixOnReadOnlyStore(t *testing.T) { addr := addrN(0x24) for i := byte(1); i <= 3; i++ { key := memiavlStorageKey(addr, slotN(i)) - cs := makeChangeSet(key, []byte{i}, false) + cs := makeChangeSet(key, padLeft32(i), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) } commitAndCheck(t, s) @@ -1585,7 +1624,7 @@ func TestIteratorByPrefixNilPrefix(t *testing.T) { addr := addrN(0x25) key := memiavlStorageKey(addr, slotN(0x01)) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1605,7 +1644,7 @@ func TestIteratorOnClosedStore(t *testing.T) { addr := addrN(0x26) key := memiavlStorageKey(addr, slotN(0x01)) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) diff --git a/sei-db/state_db/sc/flatkv/store_test.go b/sei-db/state_db/sc/flatkv/store_test.go index 62d5d97fde..59157c87e6 100644 --- a/sei-db/state_db/sc/flatkv/store_test.go +++ b/sei-db/state_db/sc/flatkv/store_test.go @@ -43,6 +43,13 @@ func memiavlStorageKey(addr Address, slot Slot) []byte { return evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, internal) } +// padLeft32 returns a 32-byte big-endian value with the given bytes right-aligned. +func padLeft32(val ...byte) []byte { + var b [32]byte + copy(b[32-len(val):], val) + return b[:] +} + // makeChangeSet creates a changeset func makeChangeSet(key, value []byte, delete bool) *proto.NamedChangeSet { return &proto.NamedChangeSet{ @@ -136,7 +143,7 @@ func TestStoreCommitVersionAutoIncrement(t *testing.T) { slot := Slot{0xBB} key := memiavlStorageKey(addr, slot) - cs := makeChangeSet(key, []byte{0xCC}, false) + cs := makeChangeSet(key, padLeft32(0xCC), false) // Initial version is 0 require.Equal(t, int64(0), s.Version()) @@ -169,7 +176,7 @@ func TestStoreApplyAndCommit(t *testing.T) { addr := Address{0x11} slot := Slot{0x22} - value := []byte{0x33} + value := padLeft32(0x33) key := memiavlStorageKey(addr, slot) cs := makeChangeSet(key, value, false) @@ -207,7 +214,7 @@ func TestStoreMultipleWrites(t *testing.T) { pairs := make([]*proto.KVPair, len(entries)) for i, e := range entries { key := memiavlStorageKey(addr, e.slot) - pairs[i] = &proto.KVPair{Key: key, Value: []byte{e.value}} + pairs[i] = &proto.KVPair{Key: key, Value: padLeft32(e.value)} } cs := &proto.NamedChangeSet{ @@ -225,7 +232,7 @@ func TestStoreMultipleWrites(t *testing.T) { key := memiavlStorageKey(addr, e.slot) got, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{e.value}, got) + require.Equal(t, padLeft32(e.value), got) } } @@ -253,7 +260,7 @@ func TestStoreClearsPendingAfterCommit(t *testing.T) { slot := Slot{0xBB} key := memiavlStorageKey(addr, slot) - cs := makeChangeSet(key, []byte{0xCC}, false) + cs := makeChangeSet(key, padLeft32(0xCC), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) // Should have pending writes @@ -280,14 +287,14 @@ func TestStoreVersioning(t *testing.T) { key := memiavlStorageKey(addr, slot) // Version 1 - cs1 := makeChangeSet(key, []byte{0x01}, false) + cs1 := makeChangeSet(key, padLeft32(0x01), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) commitAndCheck(t, s) require.Equal(t, int64(1), s.Version()) // Version 2 with updated value - cs2 := makeChangeSet(key, []byte{0x02}, false) + cs2 := makeChangeSet(key, padLeft32(0x02), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) commitAndCheck(t, s) @@ -296,7 +303,7 @@ func TestStoreVersioning(t *testing.T) { // Latest value should be from version 2 got, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{0x02}, got) + require.Equal(t, padLeft32(0x02), got) } func TestStorePersistence(t *testing.T) { @@ -304,7 +311,7 @@ func TestStorePersistence(t *testing.T) { addr := Address{0xDD} slot := Slot{0xEE} - value := []byte{0xFF} + value := padLeft32(0xFF) key := memiavlStorageKey(addr, slot) // Write and close @@ -354,7 +361,7 @@ func TestStoreRootHashChanges(t *testing.T) { slot := Slot{0xCD} key := memiavlStorageKey(addr, slot) - cs := makeChangeSet(key, []byte{0xEF}, false) + cs := makeChangeSet(key, padLeft32(0xEF), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) // Working hash should change @@ -382,7 +389,7 @@ func TestStoreRootHashChangesOnApply(t *testing.T) { slot := Slot{0xFF} key := memiavlStorageKey(addr, slot) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) // Working hash should change @@ -398,7 +405,7 @@ func TestStoreRootHashStableAfterCommit(t *testing.T) { slot := Slot{0x34} key := memiavlStorageKey(addr, slot) - cs := makeChangeSet(key, []byte{0x56}, false) + cs := makeChangeSet(key, padLeft32(0x56), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) // Get working hash @@ -678,10 +685,10 @@ func TestRootHashIsBlake3_256(t *testing.T) { } // ============================================================================= -// Get returns nil for unknown keys +// Get returns nil for missing keys, errors for unsupported key types // ============================================================================= -func TestGetUnknownKeyReturnsNil(t *testing.T) { +func TestGetMissingKeyReturnsNil(t *testing.T) { s := setupTestStore(t) defer s.Close() @@ -690,6 +697,21 @@ func TestGetUnknownKeyReturnsNil(t *testing.T) { require.Nil(t, v) } +func TestGetUnsupportedKeyType_Strict(t *testing.T) { + s := setupTestStore(t) + defer s.Close() + + require.Panics(t, func() { s.Get([]byte{}) }) +} + +func TestGetUnsupportedKeyType_NonStrict(t *testing.T) { + cfg := DefaultTestConfig(t) + s := setupTestStoreWithConfig(t, cfg) + defer s.Close() + + require.Panics(t, func() { s.Get([]byte{}) }) +} + // ============================================================================= // Persistence across close/reopen // ============================================================================= @@ -711,7 +733,7 @@ func TestPersistenceAllKeyTypes(t *testing.T) { nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) codeKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:]) - cs := makeChangeSet(storageKey, []byte{0x11}, false) + cs := makeChangeSet(storageKey, padLeft32(0x11), false) require.NoError(t, s1.ApplyChangeSets([]*proto.NamedChangeSet{cs})) cs2 := makeChangeSet(nonceKey, []byte{0, 0, 0, 0, 0, 0, 0, 5}, false) require.NoError(t, s1.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) @@ -735,7 +757,7 @@ func TestPersistenceAllKeyTypes(t *testing.T) { v, ok := s2.Get(storageKey) require.True(t, ok) - require.Equal(t, []byte{0x11}, v) + require.Equal(t, padLeft32(0x11), v) v, ok = s2.Get(nonceKey) require.True(t, ok) @@ -759,7 +781,7 @@ func TestReadOnlyBasicLoadAndRead(t *testing.T) { addr := Address{0xAA} slot := Slot{0xBB} key := memiavlStorageKey(addr, slot) - value := []byte{0xCC} + value := padLeft32(0xCC) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, value, false)})) commitAndCheck(t, s) @@ -786,7 +808,7 @@ func TestReadOnlyLoadFromUnopenedStore(t *testing.T) { addr := Address{0xCC} slot := Slot{0xDD} key := memiavlStorageKey(addr, slot) - value := []byte{0xEE} + value := padLeft32(0xEE) require.NoError(t, writer.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, value, false)})) commitAndCheck(t, writer) @@ -817,7 +839,7 @@ func TestReadOnlyAtSpecificVersion(t *testing.T) { for i := byte(1); i <= 5; i++ { require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ - makeChangeSet(key, []byte{i}, false), + makeChangeSet(key, padLeft32(i), false), })) commitAndCheck(t, s) } @@ -829,7 +851,7 @@ func TestReadOnlyAtSpecificVersion(t *testing.T) { require.Equal(t, int64(3), ro.Version()) got, found := ro.Get(key) require.True(t, found) - require.Equal(t, []byte{3}, got) + require.Equal(t, padLeft32(3), got) } func TestReadOnlyWriteGuards(t *testing.T) { @@ -842,7 +864,7 @@ func TestReadOnlyWriteGuards(t *testing.T) { addr := Address{0xAA} slot := Slot{0xBB} key := memiavlStorageKey(addr, slot) - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, []byte{1}, false)})) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, padLeft32(1), false)})) commitAndCheck(t, s) ro, err := s.LoadVersion(0, true) @@ -871,16 +893,16 @@ func TestReadOnlyParentWritesDuringReadOnly(t *testing.T) { addr := Address{0xAA} slot := Slot{0xBB} key := memiavlStorageKey(addr, slot) - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, []byte{1}, false)})) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, padLeft32(1), false)})) commitAndCheck(t, s) ro, err := s.LoadVersion(0, true) require.NoError(t, err) defer ro.Close() - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, []byte{2}, false)})) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, padLeft32(2), false)})) commitAndCheck(t, s) - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, []byte{3}, false)})) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, padLeft32(3), false)})) commitAndCheck(t, s) require.Equal(t, int64(3), s.Version()) @@ -888,7 +910,7 @@ func TestReadOnlyParentWritesDuringReadOnly(t *testing.T) { require.Equal(t, int64(1), ro.Version()) got, found := ro.Get(key) require.True(t, found) - require.Equal(t, []byte{1}, got) + require.Equal(t, padLeft32(1), got) } func TestReadOnlyConcurrentInstances(t *testing.T) { @@ -906,7 +928,7 @@ func TestReadOnlyConcurrentInstances(t *testing.T) { for i := byte(1); i <= 4; i++ { require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{ - makeChangeSet(key, []byte{i}, false), + makeChangeSet(key, padLeft32(i), false), })) commitAndCheck(t, s) } @@ -926,8 +948,8 @@ func TestReadOnlyConcurrentInstances(t *testing.T) { g2, ok2 := ro2.Get(key) require.True(t, ok1) require.True(t, ok2) - require.Equal(t, []byte{4}, g1) - require.Equal(t, []byte{4}, g2) + require.Equal(t, padLeft32(4), g1) + require.Equal(t, padLeft32(4), g2) } func TestReadOnlyFailureDoesNotAffectParent(t *testing.T) { @@ -940,20 +962,20 @@ func TestReadOnlyFailureDoesNotAffectParent(t *testing.T) { addr := Address{0xAA} slot := Slot{0xBB} key := memiavlStorageKey(addr, slot) - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, []byte{1}, false)})) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, padLeft32(1), false)})) commitAndCheck(t, s) _, err = s.LoadVersion(999, true) require.Error(t, err) - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, []byte{2}, false)})) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, padLeft32(2), false)})) v, err := s.Commit() require.NoError(t, err) require.Equal(t, int64(2), v) got, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{2}, got) + require.Equal(t, padLeft32(2), got) } func TestReadOnlyCloseRemovesTempDir(t *testing.T) { @@ -966,7 +988,7 @@ func TestReadOnlyCloseRemovesTempDir(t *testing.T) { addr := Address{0xAA} slot := Slot{0xBB} key := memiavlStorageKey(addr, slot) - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, []byte{1}, false)})) + require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{makeChangeSet(key, padLeft32(1), false)})) commitAndCheck(t, s) roStore, err := s.LoadVersion(0, true) @@ -1033,7 +1055,7 @@ func TestLoadVersionReload(t *testing.T) { addr := addrN(0x01) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1048,7 +1070,7 @@ func TestLoadVersionReload(t *testing.T) { val, found := s.Get(key) require.True(t, found) - require.Equal(t, []byte{0x11}, val) + require.Equal(t, padLeft32(0x11), val) require.NoError(t, s.Close()) } @@ -1057,7 +1079,7 @@ func TestLoadVersionReadOnlyVersion0(t *testing.T) { addr := addrN(0x02) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x22}, false) + cs := makeChangeSet(key, padLeft32(0x22), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1069,7 +1091,7 @@ func TestLoadVersionReadOnlyVersion0(t *testing.T) { require.Equal(t, int64(1), ro.Version()) val, found := ro.Get(key) require.True(t, found) - require.Equal(t, []byte{0x22}, val) + require.Equal(t, padLeft32(0x22), val) require.NoError(t, s.Close()) } @@ -1078,13 +1100,13 @@ func TestLoadVersionReadOnlyDoesNotSeePending(t *testing.T) { addr := addrN(0x03) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x33}, false) + cs := makeChangeSet(key, padLeft32(0x33), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) // Apply a new changeset without committing. key2 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x02))) - cs2 := makeChangeSet(key2, []byte{0x44}, false) + cs2 := makeChangeSet(key2, padLeft32(0x44), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) // RO should not see the uncommitted write. @@ -1098,7 +1120,7 @@ func TestLoadVersionReadOnlyDoesNotSeePending(t *testing.T) { // But committed data should be visible. val, found := ro.Get(key) require.True(t, found) - require.Equal(t, []byte{0x33}, val) + require.Equal(t, padLeft32(0x33), val) require.NoError(t, s.Close()) } @@ -1131,13 +1153,13 @@ func TestCloseWithPendingUncommittedWrites(t *testing.T) { addr := addrN(0x10) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) // Apply but do NOT commit. key2 := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x02))) - cs2 := makeChangeSet(key2, []byte{0x22}, false) + cs2 := makeChangeSet(key2, padLeft32(0x22), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) // Close should succeed even with pending writes. @@ -1154,7 +1176,7 @@ func TestCloseWithPendingUncommittedWrites(t *testing.T) { val, found := s2.Get(key) require.True(t, found, "committed data should persist") - require.Equal(t, []byte{0x11}, val) + require.Equal(t, padLeft32(0x11), val) _, found = s2.Get(key2) require.False(t, found, "uncommitted data should be lost") @@ -1165,7 +1187,7 @@ func TestCloseDuringConcurrentReadOnlyClone(t *testing.T) { addr := addrN(0x11) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0xAA}, false) + cs := makeChangeSet(key, padLeft32(0xAA), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1178,7 +1200,7 @@ func TestCloseDuringConcurrentReadOnlyClone(t *testing.T) { // RO should still function. val, found := ro.Get(key) require.True(t, found, "RO clone should remain functional after parent close") - require.Equal(t, []byte{0xAA}, val) + require.Equal(t, padLeft32(0xAA), val) require.NoError(t, ro.Close()) } @@ -1198,7 +1220,7 @@ func TestRootHashAndVersionAfterClose(t *testing.T) { addr := addrN(0x12) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0xBB}, false) + cs := makeChangeSet(key, padLeft32(0xBB), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1233,7 +1255,7 @@ func TestCatchupSkipsAlreadyCommittedEntries(t *testing.T) { addr := addrN(0x20) for i := 1; i <= 5; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))) - cs := makeChangeSet(key, []byte{byte(i)}, false) + cs := makeChangeSet(key, padLeft32(byte(i)), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err := s.Commit() require.NoError(t, err) @@ -1268,7 +1290,7 @@ func TestCatchupTargetVersionMiddleOfWAL(t *testing.T) { var hashes [6][]byte for i := 1; i <= 5; i++ { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(byte(i)))) - cs := makeChangeSet(key, []byte{byte(i)}, false) + cs := makeChangeSet(key, padLeft32(byte(i)), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) _, err := s.Commit() require.NoError(t, err) diff --git a/sei-db/state_db/sc/flatkv/store_write.go b/sei-db/state_db/sc/flatkv/store_write.go index 08a2122543..584fec3f3f 100644 --- a/sei-db/state_db/sc/flatkv/store_write.go +++ b/sei-db/state_db/sc/flatkv/store_write.go @@ -1,7 +1,6 @@ package flatkv import ( - "encoding/binary" "errors" "fmt" "sync" @@ -9,257 +8,9 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/evm" "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" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" ) -// ApplyChangeSets buffers EVM changesets and updates LtHash. -// -// LtHash is computed based on actual storage format (internal keys): -// - storageDB: key=addr||slot, value=storage_value -// - accountDB: key=addr, value=AccountValue (balance(32)||nonce(8)||codehash(32) -// - codeDB: key=addr, value=bytecode -// - legacyDB: key=full original key (with prefix), value=raw value -func (s *CommitStore) ApplyChangeSets(cs []*proto.NamedChangeSet) error { - if s.readOnly { - return errReadOnly - } - - s.phaseTimer.SetPhase("apply_change_sets_batch_read") - - // Batch read all old values from DBs in parallel. - storageOld, accountOld, codeOld, legacyOld, err := s.batchReadOldValues(cs) - if err != nil { - return fmt.Errorf("failed to batch read old values: %w", err) - } - - s.phaseTimer.SetPhase("apply_change_sets_prepare") - s.pendingChangeSets = append(s.pendingChangeSets, cs...) - - // Collect LtHash pairs per DB (using internal key format) - var storagePairs []lthash.KVPairWithLastValue - var codePairs []lthash.KVPairWithLastValue - var legacyPairs []lthash.KVPairWithLastValue - // Account pairs are collected at the end after all account changes are processed - - // Pre-capture raw encoded account bytes so LtHash delta uses the correct - // baseline across multiple ApplyChangeSets calls before Commit. - // nil means the account didn't exist (no phantom MixOut for new accounts). - oldAccountRawValues := make(map[string][]byte) - - for _, namedCS := range cs { - if namedCS.Changeset.Pairs == nil { - continue - } - - for _, pair := range namedCS.Changeset.Pairs { - // Parse memiavl key to determine type - kind, keyBytes := evm.ParseEVMKey(pair.Key) - if kind == evm.EVMKeyUnknown { - // Skip non-EVM keys silently - continue - } - - // Route to appropriate DB based on key type - switch kind { - case evm.EVMKeyStorage: - // Storage: keyBytes = addr(20) || slot(32) - keyStr := string(keyBytes) - oldValue := storageOld[keyStr].Value - - if pair.Delete { - s.storageWrites[keyStr] = &pendingKVWrite{ - key: keyBytes, - isDelete: true, - } - storageOld[keyStr] = types.BatchGetResult{Value: nil} - } else { - s.storageWrites[keyStr] = &pendingKVWrite{ - key: keyBytes, - value: pair.Value, - } - storageOld[keyStr] = types.BatchGetResult{Value: pair.Value} - } - - // LtHash pair: internal key directly - storagePairs = append(storagePairs, lthash.KVPairWithLastValue{ - Key: keyBytes, - Value: pair.Value, - LastValue: oldValue, - Delete: pair.Delete, - }) - - case evm.EVMKeyNonce, evm.EVMKeyCodeHash: - // Account data: keyBytes = addr(20) - addr, ok := AddressFromBytes(keyBytes) - if !ok { - return fmt.Errorf("invalid address length %d for key kind %d", len(keyBytes), kind) - } - addrKey := string(AccountKey(addr)) - - if _, seen := oldAccountRawValues[addrKey]; !seen { - if paw, ok := s.accountWrites[addrKey]; ok { - if paw.isDelete { - oldAccountRawValues[addrKey] = nil - } else { - oldAccountRawValues[addrKey] = paw.value.Encode() - } - } else if result, ok := accountOld[addrKey]; ok { - oldAccountRawValues[addrKey] = result.Value - } else { - oldAccountRawValues[addrKey] = nil - } - } - - paw := s.accountWrites[addrKey] - if paw == nil { - var existingValue AccountValue - result := accountOld[addrKey] - if result.IsFound() && result.Value != nil { - av, err := DecodeAccountValue(result.Value) - if err != nil { - return fmt.Errorf("corrupted AccountValue for addr %x: %w", addr, err) - } - existingValue = av - } - paw = &pendingAccountWrite{ - addr: addr, - value: existingValue, - } - s.accountWrites[addrKey] = paw - } - - if pair.Delete { - if kind == evm.EVMKeyNonce { - paw.value.Nonce = 0 - } else { - paw.value.CodeHash = CodeHash{} - } - paw.isDelete = paw.value.IsEmpty() - } else { - if kind == evm.EVMKeyNonce { - if len(pair.Value) != NonceLen { - return fmt.Errorf("invalid nonce value length: got %d, expected %d", - len(pair.Value), NonceLen) - } - paw.value.Nonce = binary.BigEndian.Uint64(pair.Value) - } else { - if len(pair.Value) != CodeHashLen { - return fmt.Errorf("invalid codehash value length: got %d, expected %d", - len(pair.Value), CodeHashLen) - } - copy(paw.value.CodeHash[:], pair.Value) - } - paw.isDelete = paw.value.IsEmpty() - } - - case evm.EVMKeyCode: - // Code: keyBytes = addr(20) - per x/evm/types/keys.go - keyStr := string(keyBytes) - oldValue := codeOld[keyStr].Value - - if pair.Delete { - s.codeWrites[keyStr] = &pendingKVWrite{ - key: keyBytes, - isDelete: true, - } - codeOld[keyStr] = types.BatchGetResult{Value: nil} - } else { - s.codeWrites[keyStr] = &pendingKVWrite{ - key: keyBytes, - value: pair.Value, - } - codeOld[keyStr] = types.BatchGetResult{Value: pair.Value} - } - - // LtHash pair: internal key directly - codePairs = append(codePairs, lthash.KVPairWithLastValue{ - Key: keyBytes, - Value: pair.Value, - LastValue: oldValue, - Delete: pair.Delete, - }) - - case evm.EVMKeyLegacy: - keyStr := string(keyBytes) - oldValue := legacyOld[keyStr].Value - - if pair.Delete { - s.legacyWrites[keyStr] = &pendingKVWrite{ - key: keyBytes, - isDelete: true, - } - legacyOld[keyStr] = types.BatchGetResult{Value: nil} - } else { - s.legacyWrites[keyStr] = &pendingKVWrite{ - key: keyBytes, - value: pair.Value, - } - legacyOld[keyStr] = types.BatchGetResult{Value: pair.Value} - } - - legacyPairs = append(legacyPairs, lthash.KVPairWithLastValue{ - Key: keyBytes, - Value: pair.Value, - LastValue: oldValue, - Delete: pair.Delete, - }) - } - } - } - - s.phaseTimer.SetPhase("apply_change_sets_collect_account_pairs") - - accountPairs := make([]lthash.KVPairWithLastValue, 0, len(oldAccountRawValues)) - for addrStr, oldRaw := range oldAccountRawValues { - paw, ok := s.accountWrites[addrStr] - if !ok { - continue - } - - var encodedValue []byte - if !paw.isDelete { - encodedValue = paw.value.Encode() - } - accountPairs = append(accountPairs, lthash.KVPairWithLastValue{ - Key: AccountKey(paw.addr), - Value: encodedValue, - LastValue: oldRaw, - Delete: paw.isDelete, - }) - } - - s.phaseTimer.SetPhase("apply_change_compute_lt_hash") - - // 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 -} - // Commit persists buffered writes and advances the version. // Protocol: WAL → per-DB batch (with LocalMeta) → flush → update metaDB. // On crash, catchup replays WAL to recover incomplete commits. @@ -341,10 +92,10 @@ func (s *CommitStore) flushAllDBs() error { // clearPendingWrites clears all pending write buffers func (s *CommitStore) clearPendingWrites() { - s.accountWrites = make(map[string]*pendingAccountWrite) - s.codeWrites = make(map[string]*pendingKVWrite) - s.storageWrites = make(map[string]*pendingKVWrite) - s.legacyWrites = make(map[string]*pendingKVWrite) + s.accountWrites = make(map[string]*vtype.AccountData) + s.codeWrites = make(map[string]*vtype.CodeData) + s.storageWrites = make(map[string]*vtype.StorageData) + s.legacyWrites = make(map[string]*vtype.LegacyData) s.pendingChangeSets = make([]*proto.NamedChangeSet, 0) } @@ -368,15 +119,14 @@ func (s *CommitStore) commitBatches(version int64) error { batch := s.accountDB.NewBatch() defer func() { _ = batch.Close() }() - for _, paw := range s.accountWrites { - key := AccountKey(paw.addr) - if paw.isDelete { + for keyStr, accountWrite := range s.accountWrites { + key := []byte(keyStr) + if accountWrite.IsDelete() { if err := batch.Delete(key); err != nil { return fmt.Errorf("accountDB delete: %w", err) } } else { - encoded := EncodeAccountValue(paw.value) - if err := batch.Set(key, encoded); err != nil { + if err := batch.Set(key, accountWrite.Serialize()); err != nil { return fmt.Errorf("accountDB set: %w", err) } } @@ -388,41 +138,28 @@ func (s *CommitStore) commitBatches(version int64) error { pending = append(pending, pendingCommit{accountDBDir, batch}) } - // Commit to codeDB, storageDB, legacyDB (identical logic per KV DB). - kvDBs := [...]struct { - dir string - phase string - writes map[string]*pendingKVWrite - db types.KeyValueDB - }{ - {codeDBDir, "commit_code_db_prepare", s.codeWrites, s.codeDB}, - {storageDBDir, "commit_storage_db_prepare", s.storageWrites, s.storageDB}, - {legacyDBDir, "commit_legacy_db_prepare", s.legacyWrites, s.legacyDB}, - } - for _, spec := range kvDBs { - if len(spec.writes) == 0 && version <= s.localMeta[spec.dir].CommittedVersion { - continue - } - s.phaseTimer.SetPhase(spec.phase) - batch := spec.db.NewBatch() - defer func(b types.Batch) { _ = b.Close() }(batch) - - for _, pw := range spec.writes { - if pw.isDelete { - if err := batch.Delete(pw.key); err != nil { - return fmt.Errorf("%s delete: %w", spec.dir, err) - } - } else { - if err := batch.Set(pw.key, pw.value); err != nil { - return fmt.Errorf("%s set: %w", spec.dir, err) - } - } - } + batch, err := s.prepareBatchCodeDB(version) + if err != nil { + return fmt.Errorf("codeDB commit: %w", err) + } + if batch != nil { + pending = append(pending, pendingCommit{codeDBDir, batch}) + } - if err := writeLocalMetaToBatch(batch, version, s.perDBWorkingLtHash[spec.dir]); err != nil { - return fmt.Errorf("%s local meta: %w", spec.dir, err) - } - pending = append(pending, pendingCommit{spec.dir, batch}) + batch, err = s.prepareBatchStorageDB(version) + if err != nil { + return fmt.Errorf("storageDB commit: %w", err) + } + if batch != nil { + pending = append(pending, pendingCommit{storageDBDir, batch}) + } + + batch, err = s.prepareBatchLegacyDB(version) + if err != nil { + return fmt.Errorf("legacyDB commit: %w", err) + } + if batch != nil { + pending = append(pending, pendingCommit{legacyDBDir, batch}) } if len(pending) == 0 { @@ -458,101 +195,137 @@ func (s *CommitStore) commitBatches(version int64) error { return nil } -// batchReadOldValues scans all changeset pairs and returns one result map per -// DB containing the "old value" for each key. Keys that already have uncommitted -// pending writes (from a prior ApplyChangeSets call in the same block) are -// resolved from those pending writes directly and excluded from the DB batch -// read, avoiding unnecessary I/O and cache pollution. -func (s *CommitStore) batchReadOldValues(cs []*proto.NamedChangeSet) ( - storageOld map[string]types.BatchGetResult, - accountOld map[string]types.BatchGetResult, - codeOld map[string]types.BatchGetResult, - legacyOld map[string]types.BatchGetResult, - err error, -) { - storageOld = make(map[string]types.BatchGetResult) - accountOld = make(map[string]types.BatchGetResult) - codeOld = make(map[string]types.BatchGetResult) - legacyOld = make(map[string]types.BatchGetResult) +// Prepare a batch of writes for the codeDB. +func (s *CommitStore) prepareBatchCodeDB(version int64) (types.Batch, error) { + if len(s.codeWrites) == 0 && version <= s.localMeta[codeDBDir].CommittedVersion { + return nil, nil + } - // Separate maps for keys that need a DB read (no pending write). - storageBatch := make(map[string]types.BatchGetResult) - accountBatch := make(map[string]types.BatchGetResult) - codeBatch := make(map[string]types.BatchGetResult) - legacyBatch := make(map[string]types.BatchGetResult) + s.phaseTimer.SetPhase("commit_code_db_prepare") + + batch := s.codeDB.NewBatch() - pendingKVResult := func(pw *pendingKVWrite) types.BatchGetResult { - if pw.isDelete { - return types.BatchGetResult{Value: nil} + for keyStr, cw := range s.codeWrites { + key := []byte(keyStr) + if cw.IsDelete() { + if err := batch.Delete(key); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("codeDB delete: %w", err) + } + } else { + if err := batch.Set(key, cw.Serialize()); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("codeDB set: %w", err) + } } - return types.BatchGetResult{Value: pw.value} } - // Partition changeset keys: resolve from pending writes when available - // (prior ApplyChangeSets call in the same block), otherwise queue for - // a DB batch read. - for _, namedCS := range cs { - if namedCS.Changeset.Pairs == nil { - continue + if err := writeLocalMetaToBatch(batch, version, s.perDBWorkingLtHash[codeDBDir]); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("codeDB local meta: %w", err) + } + + return batch, nil +} + +// Prepare a batch of writes for the storageDB. +func (s *CommitStore) prepareBatchStorageDB(version int64) (types.Batch, error) { + if len(s.storageWrites) == 0 && version <= s.localMeta[storageDBDir].CommittedVersion { + return nil, nil + } + + s.phaseTimer.SetPhase("commit_storage_db_prepare") + + batch := s.storageDB.NewBatch() + + for keyStr, sw := range s.storageWrites { + key := []byte(keyStr) + if sw.IsDelete() { + if err := batch.Delete(key); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("storageDB delete: %w", err) + } + } else { + if err := batch.Set(key, sw.Serialize()); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("storageDB set: %w", err) + } } - for _, pair := range namedCS.Changeset.Pairs { - kind, keyBytes := evm.ParseEVMKey(pair.Key) - switch kind { - case evm.EVMKeyStorage: - k := string(keyBytes) - if _, done := storageOld[k]; done { - continue - } - if pw, ok := s.storageWrites[k]; ok { - storageOld[k] = pendingKVResult(pw) - } else { - storageBatch[k] = types.BatchGetResult{} - } + } - case evm.EVMKeyNonce, evm.EVMKeyCodeHash: - addr, ok := AddressFromBytes(keyBytes) - if !ok { - continue - } - k := string(addr[:]) - if _, done := accountOld[k]; done { - continue - } - if paw, ok := s.accountWrites[k]; ok { - accountOld[k] = types.BatchGetResult{Value: EncodeAccountValue(paw.value)} - } else { - accountBatch[k] = types.BatchGetResult{} - } + if err := writeLocalMetaToBatch(batch, version, s.perDBWorkingLtHash[storageDBDir]); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("storageDB local meta: %w", err) + } - case evm.EVMKeyCode: - k := string(keyBytes) - if _, done := codeOld[k]; done { - continue - } - if pw, ok := s.codeWrites[k]; ok { - codeOld[k] = pendingKVResult(pw) - } else { - codeBatch[k] = types.BatchGetResult{} - } + return batch, nil +} - case evm.EVMKeyLegacy: - k := string(keyBytes) - if _, done := legacyOld[k]; done { - continue - } - if pw, ok := s.legacyWrites[k]; ok { - legacyOld[k] = pendingKVResult(pw) - } else { - legacyBatch[k] = types.BatchGetResult{} - } +// Prepare a batch of writes for the legacyDB. +func (s *CommitStore) prepareBatchLegacyDB(version int64) (types.Batch, error) { + if len(s.legacyWrites) == 0 && version <= s.localMeta[legacyDBDir].CommittedVersion { + return nil, nil + } + + s.phaseTimer.SetPhase("commit_legacy_db_prepare") + + batch := s.legacyDB.NewBatch() + + for keyStr, lw := range s.legacyWrites { + key := []byte(keyStr) + if lw.IsDelete() { + if err := batch.Delete(key); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("legacyDB delete: %w", err) + } + } else { + if err := batch.Set(key, lw.Serialize()); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("legacyDB set: %w", err) } } } - // Issue parallel BatchGet calls only for keys that need a DB read. + if err := writeLocalMetaToBatch(batch, version, s.perDBWorkingLtHash[legacyDBDir]); err != nil { + _ = batch.Close() + return nil, fmt.Errorf("legacyDB local meta: %w", err) + } + + return batch, nil +} + +// batchReadOldValues scans all changeset pairs and returns one result map per +// DB containing the "old value" for each key. Keys that already have uncommitted +// pending writes (from a prior ApplyChangeSets call in the same block) are +// resolved from those pending writes directly and excluded from the DB batch +// read, avoiding unnecessary I/O and cache pollution. +func (s *CommitStore) batchReadOldValues(changesByType map[evm.EVMKeyKind]map[string][]byte) ( + storageOld map[string]*vtype.StorageData, + accountOld map[string]*vtype.AccountData, + codeOld map[string]*vtype.CodeData, + legacyOld map[string]*vtype.LegacyData, + err error, +) { + storageOld = make(map[string]*vtype.StorageData) + accountOld = make(map[string]*vtype.AccountData) + codeOld = make(map[string]*vtype.CodeData) + legacyOld = make(map[string]*vtype.LegacyData) + + // Issue reads to each DB if we don't already have the old value in memory. var wg sync.WaitGroup var storageErr, accountErr, codeErr, legacyErr error + // EVM storage + storageBatch := make(map[string]types.BatchGetResult) + for key := range changesByType[evm.EVMKeyStorage] { + if _, ok := s.storageWrites[key]; ok { + // We've got the old value in the pending writes buffer. + storageOld[key] = s.storageWrites[key] + } else { + // Schedule a read for this key. + storageBatch[key] = types.BatchGetResult{} + } + } if len(storageBatch) > 0 { wg.Add(1) s.miscPool.Submit(func() { @@ -561,6 +334,27 @@ func (s *CommitStore) batchReadOldValues(cs []*proto.NamedChangeSet) ( }) } + // Accounts + accountBatch := make(map[string]types.BatchGetResult) + for key := range changesByType[evm.EVMKeyNonce] { + if _, ok := s.accountWrites[key]; ok { + // We've got the old value in the pending writes buffer. + accountOld[key] = s.accountWrites[key] + } else { + // Schedule a read for this key. + accountBatch[key] = types.BatchGetResult{} + } + } + for key := range changesByType[evm.EVMKeyCodeHash] { + if _, ok := s.accountWrites[key]; ok { + // We've got the old value in the pending writes buffer. + accountOld[key] = s.accountWrites[key] + } else { + // Schedule a read for this key. + accountBatch[key] = types.BatchGetResult{} + } + } + // TODO: when we eventually add a balance key, we will need to add it to the accountBatch map here. if len(accountBatch) > 0 { wg.Add(1) s.miscPool.Submit(func() { @@ -569,6 +363,17 @@ func (s *CommitStore) batchReadOldValues(cs []*proto.NamedChangeSet) ( }) } + // EVM bytecode + codeBatch := make(map[string]types.BatchGetResult) + for key := range changesByType[evm.EVMKeyCode] { + if _, ok := s.codeWrites[key]; ok { + // We've got the old value in the pending writes buffer. + codeOld[key] = s.codeWrites[key] + } else { + // Schedule a read for this key. + codeBatch[key] = types.BatchGetResult{} + } + } if len(codeBatch) > 0 { wg.Add(1) s.miscPool.Submit(func() { @@ -577,6 +382,17 @@ func (s *CommitStore) batchReadOldValues(cs []*proto.NamedChangeSet) ( }) } + // Legacy data + legacyBatch := make(map[string]types.BatchGetResult) + for key := range changesByType[evm.EVMKeyLegacy] { + if _, ok := s.legacyWrites[key]; ok { + // We've got the old value in the pending writes buffer. + legacyOld[key] = s.legacyWrites[key] + } else { + // Schedule a read for this key. + legacyBatch[key] = types.BatchGetResult{} + } + } if len(legacyBatch) > 0 { wg.Add(1) s.miscPool.Submit(func() { @@ -585,38 +401,65 @@ func (s *CommitStore) batchReadOldValues(cs []*proto.NamedChangeSet) ( }) } + // Wait for all reads to complete. wg.Wait() if err = errors.Join(storageErr, accountErr, codeErr, legacyErr); err != nil { - return + return nil, nil, nil, nil, fmt.Errorf("failed to batch read old values: %w", err) } - // Merge DB results into the result maps, failing on any per-key errors. - // BatchGet converts ErrNotFound into nil Value (no error), but surfaces - // real read errors. + // Merge DB results into the result maps. + + // Storage for k, v := range storageBatch { if v.Error != nil { return nil, nil, nil, nil, fmt.Errorf("storageDB batch read error for key %x: %w", k, v.Error) } - storageOld[k] = v + if v.IsFound() { + storageOld[k], err = vtype.DeserializeStorageData(v.Value) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed to deserialize storage data: %w", err) + } + } } + + // Accounts for k, v := range accountBatch { if v.Error != nil { return nil, nil, nil, nil, fmt.Errorf("accountDB batch read error for key %x: %w", k, v.Error) } - accountOld[k] = v + if v.IsFound() { + accountOld[k], err = vtype.DeserializeAccountData(v.Value) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed to deserialize account data: %w", err) + } + } } + + // EVM bytecode for k, v := range codeBatch { if v.Error != nil { return nil, nil, nil, nil, fmt.Errorf("codeDB batch read error for key %x: %w", k, v.Error) } - codeOld[k] = v + if v.IsFound() { + codeOld[k], err = vtype.DeserializeCodeData(v.Value) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed to deserialize code data: %w", err) + } + } } + + // Legacy data for k, v := range legacyBatch { if v.Error != nil { return nil, nil, nil, nil, fmt.Errorf("legacyDB batch read error for key %x: %w", k, v.Error) } - legacyOld[k] = v + if v.IsFound() { + legacyOld[k], err = vtype.DeserializeLegacyData(v.Value) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed to deserialize legacy data: %w", err) + } + } } - return + return storageOld, accountOld, codeOld, legacyOld, 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 563b9cbea8..25240466a7 100644 --- a/sei-db/state_db/sc/flatkv/store_write_test.go +++ b/sei-db/state_db/sc/flatkv/store_write_test.go @@ -8,6 +8,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/evm" "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/vtype" "github.com/stretchr/testify/require" ) @@ -20,7 +21,7 @@ func TestStoreNonStorageKeys(t *testing.T) { defer s.Close() addr := Address{0x99} - codeHash := CodeHash{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + codeHash := vtype.CodeHash{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00} @@ -63,7 +64,7 @@ func TestStoreWriteAllDBs(t *testing.T) { // Storage key { Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), - Value: []byte{0x11, 0x22}, + Value: padLeft32(0x11, 0x22), }, // Account nonce key { @@ -104,13 +105,13 @@ func TestStoreWriteAllDBs(t *testing.T) { require.Equal(t, int64(1), int64(binary.BigEndian.Uint64(raw)), "%s persisted version", name) } - // Verify storage data was written - storageData, err := s.storageDB.Get(StorageKey(addr, slot)) - require.NoError(t, err) - require.Equal(t, []byte{0x11, 0x22}, storageData) + // Verify storage data was written (via Store.Get which deserializes) + storageMemiavlKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) + storageValue, found := s.Get(storageMemiavlKey) + require.True(t, found, "Storage should be found") + require.Equal(t, padLeft32(0x11, 0x22), storageValue) // Verify account and code data was written - // Use Store.Get method which handles the kind prefix correctly nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) nonceValue, found := s.Get(nonceKey) require.True(t, found, "Nonce should be found") @@ -121,14 +122,9 @@ func TestStoreWriteAllDBs(t *testing.T) { require.True(t, found, "Code should be found") require.Equal(t, []byte{0x60, 0x60, 0x60}, codeValue) - // Verify bytecode stored directly in codeDB (raw key = addr) - codeRaw, err := s.codeDB.Get(addr[:]) - require.NoError(t, err) - require.Equal(t, []byte{0x60, 0x60, 0x60}, codeRaw) - - // Verify legacy data persisted in legacyDB (full key preserved) - legacyVal, err := s.legacyDB.Get(legacyKey) - require.NoError(t, err) + // Verify legacy data persisted (via Store.Get which deserializes) + legacyVal, found := s.Get(legacyKey) + require.True(t, found, "Legacy should be found") require.Equal(t, []byte{0x00, 0x03}, legacyVal) } @@ -150,7 +146,7 @@ func TestStoreWriteEmptyCommit(t *testing.T) { addr := Address{0x99} slot := Slot{0x88} key := memiavlStorageKey(addr, slot) - cs := makeChangeSet(key, []byte{0x77}, false) + cs := makeChangeSet(key, padLeft32(0x77), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -237,7 +233,7 @@ func TestStoreWriteDelete(t *testing.T) { pairs := []*proto.KVPair{ { Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), - Value: []byte{0x11}, + Value: padLeft32(0x11), }, { Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]), @@ -304,7 +300,7 @@ func TestAccountValueStorage(t *testing.T) { defer s.Close() addr := Address{0xFF, 0xFF} - expectedCodeHash := CodeHash{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB} + expectedCodeHash := vtype.CodeHash{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB} // Write both Nonce and CodeHash for the same address // AccountValue stores: balance(32) || nonce(8) || codehash(32) @@ -338,11 +334,12 @@ func TestAccountValueStorage(t *testing.T) { require.NotNil(t, stored) // Decode and verify - av, err := DecodeAccountValue(stored) + ad, err := vtype.DeserializeAccountData(stored) require.NoError(t, err) - require.Equal(t, uint64(42), av.Nonce, "Nonce should be 42") - require.Equal(t, expectedCodeHash, av.CodeHash, "CodeHash should match") - require.Equal(t, Balance{}, av.Balance, "Balance should be zero") + require.Equal(t, uint64(42), ad.GetNonce(), "Nonce should be 42") + require.Equal(t, &expectedCodeHash, ad.GetCodeHash(), "CodeHash should match") + var zeroBalance vtype.Balance + require.Equal(t, &zeroBalance, ad.GetBalance(), "Balance should be zero") // Get method should return individual fields nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) @@ -381,10 +378,10 @@ func TestStoreWriteLegacyKeys(t *testing.T) { // Verify legacyDB LocalMeta is updated require.Equal(t, int64(1), s.localMeta[legacyDBDir].CommittedVersion) - // Verify data persisted in legacyDB (full key preserved) - stored, err := s.legacyDB.Get(codeSizeKey) - require.NoError(t, err) - require.Equal(t, codeSizeValue, stored) + // Verify data persisted (via Store.Get which deserializes) + got, found := s.Get(codeSizeKey) + require.True(t, found) + require.Equal(t, codeSizeValue, got) } func TestStoreWriteLegacyAndOptimizedKeys(t *testing.T) { @@ -398,7 +395,7 @@ func TestStoreWriteLegacyAndOptimizedKeys(t *testing.T) { // Storage (optimized) { Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)), - Value: []byte{0x11, 0x22}, + Value: padLeft32(0x11, 0x22), }, // Nonce (optimized) { @@ -427,11 +424,11 @@ func TestStoreWriteLegacyAndOptimizedKeys(t *testing.T) { requireAllLocalMetaAt(t, s, 1) - // Verify legacy data persisted + // Verify legacy data persisted (via Store.Get which deserializes) codeSizeKey := append([]byte{0x09}, addr[:]...) - stored, err := s.legacyDB.Get(codeSizeKey) - require.NoError(t, err) - require.Equal(t, []byte{0x00, 0x03}, stored) + got, found := s.Get(codeSizeKey) + require.True(t, found) + require.Equal(t, []byte{0x00, 0x03}, got) } func TestStoreWriteDeleteLegacyKey(t *testing.T) { @@ -532,14 +529,14 @@ func TestStoreFsyncConfig(t *testing.T) { key := memiavlStorageKey(addr, slot) // Write and commit with fsync disabled - cs := makeChangeSet(key, []byte{0xCC}, false) + cs := makeChangeSet(key, padLeft32(0xCC), false) require.NoError(t, store.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, store) // Data should be readable got, found := store.Get(key) require.True(t, found) - require.Equal(t, []byte{0xCC}, got) + require.Equal(t, padLeft32(0xCC), got) // Version should be updated require.Equal(t, int64(1), store.Version()) @@ -645,21 +642,21 @@ func TestMultipleApplyChangeSetsBeforeCommit(t *testing.T) { key1 := memiavlStorageKey(addr, slot1) key2 := memiavlStorageKey(addr, slot2) - cs1 := makeChangeSet(key1, []byte{0x11}, false) + cs1 := makeChangeSet(key1, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) - cs2 := makeChangeSet(key2, []byte{0x22}, false) + cs2 := makeChangeSet(key2, padLeft32(0x22), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) commitAndCheck(t, s) v1, ok := s.Get(key1) require.True(t, ok) - require.Equal(t, []byte{0x11}, v1) + require.Equal(t, padLeft32(0x11), v1) v2, ok := s.Get(key2) require.True(t, ok) - require.Equal(t, []byte{0x22}, v2) + require.Equal(t, padLeft32(0x22), v2) } func TestMultipleApplyAccountFieldsPreservesOther(t *testing.T) { @@ -669,7 +666,7 @@ func TestMultipleApplyAccountFieldsPreservesOther(t *testing.T) { addr := Address{0xBB} nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) codeHashKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]) - codeHash := CodeHash{0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, + codeHash := vtype.CodeHash{0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01} @@ -725,7 +722,7 @@ func TestLtHashUpdatedByDelete(t *testing.T) { slot := Slot{0xEE} key := memiavlStorageKey(addr, slot) - cs1 := makeChangeSet(key, []byte{0xFF}, false) + cs1 := makeChangeSet(key, padLeft32(0xFF), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) commitAndCheck(t, s) hashAfterWrite := s.RootHash() @@ -745,7 +742,7 @@ func TestLtHashAccountFieldMerge(t *testing.T) { addr := Address{0xCC} nonceKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]) codeHashKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, addr[:]) - codeHash := CodeHash{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + codeHash := vtype.CodeHash{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20} @@ -763,10 +760,10 @@ func TestLtHashAccountFieldMerge(t *testing.T) { require.Len(t, s.accountWrites, 1, "both nonce and codehash should merge into one AccountValue") - paw := s.accountWrites[string(addr[:])] - require.NotNil(t, paw) - require.Equal(t, uint64(10), paw.value.Nonce) - require.Equal(t, codeHash, paw.value.CodeHash) + accountWrite := s.accountWrites[string(addr[:])] + require.NotNil(t, accountWrite) + require.Equal(t, uint64(10), accountWrite.GetNonce()) + require.Equal(t, &codeHash, accountWrite.GetCodeHash()) } // ============================================================================= @@ -782,8 +779,8 @@ func TestOverwriteSameKeyInSingleBlock(t *testing.T) { key := memiavlStorageKey(addr, slot) pairs := []*proto.KVPair{ - {Key: key, Value: []byte{0x01}}, - {Key: key, Value: []byte{0x02}}, + {Key: key, Value: padLeft32(0x01)}, + {Key: key, Value: padLeft32(0x02)}, } cs := &proto.NamedChangeSet{ Name: "evm", @@ -794,7 +791,7 @@ func TestOverwriteSameKeyInSingleBlock(t *testing.T) { v, ok := s.Get(key) require.True(t, ok) - require.Equal(t, []byte{0x02}, v, "last write should win") + require.Equal(t, padLeft32(0x02), v, "last write should win") } // ============================================================================= @@ -836,7 +833,7 @@ func TestStoreFsyncEnabled(t *testing.T) { v, ok := s.Get(memiavlStorageKey(Address{0x01}, Slot{0x01})) require.True(t, ok) - require.Equal(t, []byte{0x01}, v) + require.Equal(t, padLeft32(0x01), v) } // ============================================================================= @@ -928,8 +925,10 @@ func TestDeleteSemanticsCodehashAsymmetry(t *testing.T) { require.False(t, found, "codehash should not be found after row deletion") require.Nil(t, chVal) - require.False(t, s.Has(chKey), "Has(codehash) should be false after delete") - require.False(t, s.Has(nonceKey), "Has(nonce) should be false after row deletion") + hasCodeHash := s.Has(chKey) + require.False(t, hasCodeHash, "Has(codehash) should be false after delete") + hasNonce := s.Has(nonceKey) + require.False(t, hasNonce, "Has(nonce) should be false after row deletion") codeKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, addr[:]) _, found = s.Get(codeKey) @@ -986,7 +985,7 @@ func TestCrossApplyChangeSetsOrdering(t *testing.T) { key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slot)) val, found := s.Get(key) require.True(t, found, "delete-then-write: key should exist") - require.Equal(t, []byte{0xBB}, val) + require.Equal(t, padLeft32(0xBB), val) }) } @@ -1215,7 +1214,7 @@ func TestCrossApplyChangeSetsAccountOrdering(t *testing.T) { } func bytesToNonce(b []byte) uint64 { - if len(b) != NonceLen { + if len(b) != vtype.NonceLen { return 0 } return binary.BigEndian.Uint64(b) @@ -1231,42 +1230,43 @@ func TestAccountValueEncodingTransition(t *testing.T) { addr := addrN(0x01) - // Step 1: Write nonce only → EOA encoding (40 bytes) + // Step 1: Write nonce only (AccountData always 81 bytes) cs1 := namedCS(noncePair(addr, 7)) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs1})) commitAndCheck(t, s) raw1, err := s.accountDB.Get(AccountKey(addr)) require.NoError(t, err) - require.Equal(t, accountValueEOALen, len(raw1), "nonce-only should produce EOA encoding (40 bytes)") + ad1, err := vtype.DeserializeAccountData(raw1) + require.NoError(t, err) + require.Equal(t, uint64(7), ad1.GetNonce()) + var zeroHash vtype.CodeHash + require.Equal(t, &zeroHash, ad1.GetCodeHash(), "nonce-only should have zero codehash") - // Step 2: Add codehash → contract encoding (72 bytes) + // Step 2: Add codehash cs2 := namedCS(codeHashPair(addr, codeHashN(0xAB))) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs2})) commitAndCheck(t, s) raw2, err := s.accountDB.Get(AccountKey(addr)) require.NoError(t, err) - require.Equal(t, accountValueContractLen, len(raw2), "nonce+codehash should produce contract encoding (72 bytes)") - - av2, err := DecodeAccountValue(raw2) + ad2, err := vtype.DeserializeAccountData(raw2) require.NoError(t, err) - require.Equal(t, uint64(7), av2.Nonce, "nonce should be preserved after codehash write") - require.Equal(t, codeHashN(0xAB), av2.CodeHash) + require.Equal(t, uint64(7), ad2.GetNonce(), "nonce should be preserved after codehash write") + expectedCH := codeHashN(0xAB) + require.Equal(t, &expectedCH, ad2.GetCodeHash()) - // Step 3: Delete codehash → back to EOA encoding (40 bytes) + // Step 3: Delete codehash → back to zero codehash cs3 := namedCS(codeHashDeletePair(addr)) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs3})) commitAndCheck(t, s) raw3, err := s.accountDB.Get(AccountKey(addr)) require.NoError(t, err) - require.Equal(t, accountValueEOALen, len(raw3), "codehash delete should shrink back to EOA encoding (40 bytes)") - - av3, err := DecodeAccountValue(raw3) + ad3, err := vtype.DeserializeAccountData(raw3) require.NoError(t, err) - require.Equal(t, uint64(7), av3.Nonce, "nonce should survive codehash deletion") - require.Equal(t, CodeHash{}, av3.CodeHash, "codehash should be zero after delete") + require.Equal(t, uint64(7), ad3.GetNonce(), "nonce should survive codehash deletion") + require.Equal(t, &zeroHash, ad3.GetCodeHash(), "codehash should be zero after delete") } // ============================================================================= @@ -1545,7 +1545,7 @@ func TestApplyChangeSetsMixedEVMAndNonEVM(t *testing.T) { evmCS := &proto.NamedChangeSet{ Name: "evm", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ - {Key: storageKey, Value: []byte{0x42}}, + {Key: storageKey, Value: padLeft32(0x42)}, }}, } bankCS := &proto.NamedChangeSet{ @@ -1563,7 +1563,7 @@ func TestApplyChangeSetsMixedEVMAndNonEVM(t *testing.T) { // The EVM value should be readable via pending writes. val, found := s.Get(storageKey) require.True(t, found) - require.Equal(t, []byte{0x42}, val) + require.Equal(t, padLeft32(0x42), val) } func TestApplyChangeSetsEmptyPairsVsNilPairs(t *testing.T) { @@ -1592,7 +1592,7 @@ func TestApplyChangeSetsOnReadOnlyStore(t *testing.T) { addr := addrN(0x01) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) @@ -1649,7 +1649,7 @@ func TestApplyChangeSetsErrorRecoveryPartialState(t *testing.T) { cs := &proto.NamedChangeSet{ Name: "evm", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ - {Key: storageKey, Value: []byte{0xAA}}, + {Key: storageKey, Value: padLeft32(0xAA)}, {Key: evm.BuildMemIAVLEVMKey(evm.EVMKeyNonce, addr[:]), Value: []byte{0x01, 0x02}}, // wrong length }}, } @@ -1660,7 +1660,7 @@ func TestApplyChangeSetsErrorRecoveryPartialState(t *testing.T) { // The storage write may have been buffered before the error. // Verify the store doesn't panic and can still accept new operations. - validCS := makeChangeSet(storageKey, []byte{0xBB}, false) + validCS := makeChangeSet(storageKey, padLeft32(0xBB), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{validCS})) } @@ -1668,20 +1668,13 @@ func TestApplyChangeSetsEVMKeyEmptySkipped(t *testing.T) { s := setupTestStore(t) defer s.Close() - hashBefore := s.RootHash() - - // Only zero-length keys return EVMKeyUnknown (alias for EVMKeyEmpty). - // All non-empty keys are routed to at least EVMKeyLegacy. cs := &proto.NamedChangeSet{ Name: "evm", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ {Key: []byte{}, Value: []byte{0xAA}}, }}, } - require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) - require.Equal(t, hashBefore, s.RootHash(), "empty key should be silently skipped") - require.Len(t, s.legacyWrites, 0) - require.Len(t, s.storageWrites, 0) + require.Error(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) } func TestApplyChangeSetsNonPrefixedKeyGoesToLegacy(t *testing.T) { @@ -1720,7 +1713,7 @@ func TestDoubleCommitNoApplyBetween(t *testing.T) { addr := addrN(0x01) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) v1, err := s.Commit() @@ -1740,7 +1733,7 @@ func TestCommitOnReadOnlyStore(t *testing.T) { addr := addrN(0x01) key := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, StorageKey(addr, slotN(0x01))) - cs := makeChangeSet(key, []byte{0x11}, false) + cs := makeChangeSet(key, padLeft32(0x11), false) require.NoError(t, s.ApplyChangeSets([]*proto.NamedChangeSet{cs})) commitAndCheck(t, s) diff --git a/sei-db/state_db/sc/flatkv/vtype/legacy_data_test.go b/sei-db/state_db/sc/flatkv/vtype/legacy_data_test.go index 11e98a0ac4..1713af3d65 100644 --- a/sei-db/state_db/sc/flatkv/vtype/legacy_data_test.go +++ b/sei-db/state_db/sc/flatkv/vtype/legacy_data_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" @@ -26,7 +27,7 @@ func TestLegacySerializationGoldenFile_V0(t *testing.T) { want, err := os.ReadFile(golden) require.NoError(t, err) - wantBytes, err := hex.DecodeString(string(want)) + wantBytes, err := hex.DecodeString(strings.TrimSpace(string(want))) require.NoError(t, err) require.Equal(t, wantBytes, serialized, "serialization differs from golden file") diff --git a/sei-db/state_db/sc/flatkv/vtype/testdata/legacy_data_v0.hex b/sei-db/state_db/sc/flatkv/vtype/testdata/legacy_data_v0.hex index 48962ea95e..219db48e25 100644 --- a/sei-db/state_db/sc/flatkv/vtype/testdata/legacy_data_v0.hex +++ b/sei-db/state_db/sc/flatkv/vtype/testdata/legacy_data_v0.hex @@ -1 +1 @@ -00cafebabe \ No newline at end of file +00cafebabe