diff --git a/sei-db/db_engine/view/shard.go b/sei-db/db_engine/view/shard.go index c78bc55feb..eb9e09d5a0 100644 --- a/sei-db/db_engine/view/shard.go +++ b/sei-db/db_engine/view/shard.go @@ -147,7 +147,7 @@ func (s *shard) Get( } // First, check to see if we have this value in the versioned data map. - if value, found := s.lookupVersionedRLocked(string(key), version); found { + if value, found := s.lookupVersionedRLocked(key, version); found { s.lock.Unlock() s.metrics.reportCacheHits(1) return value, value != nil, nil @@ -177,7 +177,7 @@ func (s *shard) attemptFastGetUnlocked( return nil, false, true, err } - if value, found := s.lookupVersionedRLocked(string(key), version); found { + if value, found := s.lookupVersionedRLocked(key, version); found { s.metrics.reportCacheHits(1) return value, value != nil, true, nil } @@ -204,8 +204,8 @@ func (s *shard) validateVersionRLocked(version uint64) error { // lookupVersionedRLocked checks versioned data for a key at the given version. // Returns (value, true) if found in versioned data, (nil, false) if the read cache should be // consulted. -func (s *shard) lookupVersionedRLocked(key string, version uint64) ([]byte, bool) { - deque, ok := s.versionedData[key] +func (s *shard) lookupVersionedRLocked(key []byte, version uint64) ([]byte, bool) { + deque, ok := s.versionedData[string(key)] if !ok { return nil, false } @@ -279,7 +279,7 @@ func (s *shard) attemptFastBatchGetUnlocked( for i, key := range keys { keyStr := string(key) - if value, found := s.lookupVersionedRLocked(keyStr, version); found { + if value, found := s.lookupVersionedRLocked(key, version); found { // found includes tombstones (nil value); only non-nil values are real hits to return. if value != nil { results[keyStr] = value @@ -329,7 +329,7 @@ func (s *shard) batchGetRemainingUnlocked( for _, i := range indices { key := keys[i] keyStr := string(key) - if value, found := s.lookupVersionedRLocked(keyStr, version); found { + if value, found := s.lookupVersionedRLocked(key, version); found { if value != nil { results[keyStr] = value } diff --git a/sei-db/state_db/sc/flatkv/ktype/ktype.go b/sei-db/state_db/sc/flatkv/ktype/ktype.go index a4589065a8..ab65b1662b 100644 --- a/sei-db/state_db/sc/flatkv/ktype/ktype.go +++ b/sei-db/state_db/sc/flatkv/ktype/ktype.go @@ -127,3 +127,18 @@ func PrefixEnd(prefix []byte) []byte { } return nil } + +// AppendEVMPhysicalKey appends the physical key EVMPhysicalKey would build for kind and strippedKey +// to dst and returns the extended slice. +func AppendEVMPhysicalKey(dst []byte, kind keys.EVMKeyKind, strippedKey []byte) []byte { + if kind == keys.EVMKeyCodeHash || kind == keys.EVMKeyBalance { + kind = EVMKeyAccount + } + prefixByte, ok := keys.EVMKeyPrefixByte(kind) + if !ok { + return nil + } + dst = append(dst, keys.EVMStoreKey...) + dst = append(dst, '/', prefixByte) + return append(dst, strippedKey...) +} diff --git a/sei-db/state_db/sc/flatkv/state_view.go b/sei-db/state_db/sc/flatkv/state_view.go index 77fc4143ef..a7029bfa94 100644 --- a/sei-db/state_db/sc/flatkv/state_view.go +++ b/sei-db/state_db/sc/flatkv/state_view.go @@ -80,36 +80,37 @@ func (v *flatKVStateView) Get(module string, key []byte) ([]byte, bool) { // AccountExists reports whether addr has an account in this block. func (v *flatKVStateView) AccountExists(addr gigatypes.Address) bool { - return v.accountData(addr[:]) != nil + _, ok := v.accountRow(addr) + return ok } // GetNonce returns addr's account nonce, or 0 when the account does not exist. func (v *flatKVStateView) GetNonce(addr gigatypes.Address) uint64 { - account := v.accountData(addr[:]) - if account == nil { + account, ok := v.accountRow(addr) + if !ok { return 0 } - return account.GetNonce() + return account.Nonce() } // GetBalance returns addr's balance as a 256-bit big-endian value, or the zero value when addr holds // no balance. func (v *flatKVStateView) GetBalance(addr gigatypes.Address) gigatypes.Hash { - account := v.accountData(addr[:]) - if account == nil { + account, ok := v.accountRow(addr) + if !ok { return gigatypes.Hash{} } - return gigatypes.Hash(*account.GetBalance()) + return gigatypes.Hash(account.Balance()) } // GetCodeHash returns the hash of addr's contract code, gigatypes.EmptyCodeHash when the account exists // and holds no code, or the zero hash when it does not exist. func (v *flatKVStateView) GetCodeHash(addr gigatypes.Address) gigatypes.Hash { - account := v.accountData(addr[:]) - if account == nil { + account, ok := v.accountRow(addr) + if !ok { return gigatypes.Hash{} } - codeHash := gigatypes.Hash(*account.GetCodeHash()) + codeHash := gigatypes.Hash(account.CodeHash()) if codeHash == (gigatypes.Hash{}) { // A row only exists while some field is non-zero (see AccountData.IsDelete), and the code hash // is not that field here, so this account has a nonce or a balance and no code — the case EVM @@ -121,14 +122,25 @@ func (v *flatKVStateView) GetCodeHash(addr gigatypes.Address) gigatypes.Hash { // GetStorage returns the value at key in addr's storage, or the zero hash when the slot is unset. func (v *flatKVStateView) GetStorage(addr gigatypes.Address, key gigatypes.Hash) gigatypes.Hash { - storage := v.storageData(ktype.StorageKey(ktype.Address(addr), ktype.Slot(key))) - if storage == nil { + var buf [physKeyBufLen]byte + physKey := ktype.AppendEVMPhysicalKey(buf[:0], keys.EVMKeyStorage, addr[:]) + physKey = append(physKey, key[:]...) + raw, found := v.readRow(v.blockView.StorageView(), physKey) + if !found { return gigatypes.Hash{} } - return gigatypes.Hash(*storage.GetValue()) + storage, err := vtype.ParseStorageRow(raw) + if err != nil { + panic(fmt.Sprintf("flatkv: parse storage %x at height %d: %v", physKey, v.blockView.BlockHeight(), err)) + } + if storage.IsDelete() { + return gigatypes.Hash{} + } + return gigatypes.Hash(storage.Value()) } -// GetCode returns addr's contract code, or nil when it has none. +// GetCode returns addr's contract code, or nil when it has none. The slice aliases the store's row +// and is valid until the view is closed. func (v *flatKVStateView) GetCode(addr gigatypes.Address) []byte { code := v.codeData(addr[:]) if code == nil { @@ -142,6 +154,28 @@ func (v *flatKVStateView) GetCodeSize(addr gigatypes.Address) int { return len(v.GetCode(addr)) } +// physKeyBufLen holds the longest EVM physical key: "evm/" + kind byte + address + slot. +const physKeyBufLen = len(keys.EVMStoreKey) + 2 + ktype.AddressLen + ktype.SlotLen + +// accountRow returns addr's account row, or false when no account exists in this block. The row +// aliases store memory and is valid until the view is closed. +func (v *flatKVStateView) accountRow(addr gigatypes.Address) (vtype.AccountRow, bool) { + var buf [physKeyBufLen]byte + physKey := ktype.AppendEVMPhysicalKey(buf[:0], ktype.EVMKeyAccount, addr[:]) + raw, found := v.readRow(v.blockView.AccountView(), physKey) + if !found { + return vtype.AccountRow{}, false + } + account, err := vtype.ParseAccountRow(raw) + if err != nil { + panic(fmt.Sprintf("flatkv: parse account %x at height %d: %v", addr, v.blockView.BlockHeight(), err)) + } + if account.IsDelete() { + return vtype.AccountRow{}, false + } + return account, true +} + // accountData returns the account row for the 20-byte address in keyBytes, or nil when no account // exists in this block. func (v *flatKVStateView) accountData(keyBytes []byte) *vtype.AccountData { diff --git a/sei-db/state_db/sc/flatkv/state_view_bench_test.go b/sei-db/state_db/sc/flatkv/state_view_bench_test.go new file mode 100644 index 0000000000..dfa44073a8 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/state_view_bench_test.go @@ -0,0 +1,70 @@ +package flatkv + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-db/common/keys" + "github.com/sei-protocol/sei-chain/sei-db/proto" + gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/config" +) + +// benchView opens a view over a store holding one account with a nonce, a balance, a storage slot and +// 8 KiB of code, so each benchmark measures a cache-hit read. +func benchView(b *testing.B) (gigatypes.StateView, gigatypes.Address) { + b.Helper() + cfg := config.DefaultTestConfig(&testing.T{}) + cfg.DataDir = filepath.Join(b.TempDir(), "flatkv") + s, err := newCommitStoreWithWAL(b.Context(), cfg) + require.NoError(b, err) + require.NoError(b, s.LoadLatest()) + b.Cleanup(func() { require.NoError(b, s.Close()) }) + addr := addrN(1) + code := make([]byte, 8*1024) + for i := range code { + code[i] = byte(i) + } + cs := namedCS(noncePair(addr, 7), storagePair(addr, slotN(1), padLeft32(0xaa)), codePair(addr, code)) + cs.Changeset.Pairs = append(cs.Changeset.Pairs, &proto.KVPair{ + Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]), + Value: padLeft32(0x05), + }) + require.NoError(b, s.CommitStateChanges(1, []*proto.NamedChangeSet{cs})) + v := s.OpenView() + b.Cleanup(v.Close) + return v, gigaAddr(addr) +} + +func BenchmarkStateViewGetStorage(b *testing.B) { + v, addr := benchView(b) + slot := gigatypes.Hash(slotN(1)) + require.Equal(b, gigatypes.Hash(padLeft32(0xaa)), v.GetStorage(addr, slot)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.GetStorage(addr, slot) + } +} + +func BenchmarkStateViewGetBalance(b *testing.B) { + v, addr := benchView(b) + require.Equal(b, gigatypes.Hash(padLeft32(0x05)), v.GetBalance(addr)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.GetBalance(addr) + } +} + +func BenchmarkStateViewGetCode(b *testing.B) { + v, addr := benchView(b) + require.Len(b, v.GetCode(addr), 8*1024) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.GetCode(addr) + } +} diff --git a/sei-db/state_db/sc/flatkv/vtype/code_data.go b/sei-db/state_db/sc/flatkv/vtype/code_data.go index f941f8a258..109cac0744 100644 --- a/sei-db/state_db/sc/flatkv/vtype/code_data.go +++ b/sei-db/state_db/sc/flatkv/vtype/code_data.go @@ -75,8 +75,7 @@ func DeserializeCodeData(data []byte) (*CodeData, error) { version, codeBytecodeStart, len(data)) } - bytecode := make([]byte, len(data)-codeBytecodeStart) - copy(bytecode, data[codeBytecodeStart:]) + bytecode := data[codeBytecodeStart:] return &CodeData{ version: version, diff --git a/sei-db/state_db/sc/flatkv/vtype/rows.go b/sei-db/state_db/sc/flatkv/vtype/rows.go new file mode 100644 index 0000000000..0c29bbb2b4 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/vtype/rows.go @@ -0,0 +1,92 @@ +package vtype + +import ( + "encoding/binary" + "errors" + "fmt" +) + +// AccountRow is a read-only view over a serialized account row, in either its compact or full form. +// It aliases the bytes it was parsed from rather than copying them. +type AccountRow struct { + data []byte +} + +// ParseAccountRow validates data as an account row and returns a view over it. +func ParseAccountRow(data []byte) (AccountRow, error) { + if len(data) == 0 { + return AccountRow{}, errors.New("data is empty") + } + if version := AccountDataVersion(data[accountVersionStart]); version != AccountDataVersion0 { + return AccountRow{}, fmt.Errorf("unsupported serialization version: %d", version) + } + if len(data) != accountDataLength && len(data) != accountCompactLength { + return AccountRow{}, fmt.Errorf("data length should be %d or %d, got %d", + accountCompactLength, accountDataLength, len(data)) + } + return AccountRow{data: data}, nil +} + +// Balance returns the account's balance. +func (r AccountRow) Balance() Balance { + return Balance(r.data[accountBalanceStart:accountNonceStart]) +} + +// Nonce returns the account's nonce. +func (r AccountRow) Nonce() uint64 { + return binary.BigEndian.Uint64(r.data[accountNonceStart:accountCodeHashStart]) +} + +// CodeHash returns the account's code hash, or the zero hash for a compact row. +func (r AccountRow) CodeHash() CodeHash { + if len(r.data) < accountDataLength { + return CodeHash{} + } + return CodeHash(r.data[accountCodeHashStart:accountDataLength]) +} + +// IsDelete reports whether the row is a tombstone: every field other than the version and block +// height is zero. +func (r AccountRow) IsDelete() bool { + for _, b := range r.data[accountBalanceStart:] { + if b != 0 { + return false + } + } + return true +} + +// StorageRow is a read-only view over a serialized storage row. It aliases the bytes it was parsed +// from rather than copying them. +type StorageRow struct { + data []byte +} + +// ParseStorageRow validates data as a storage row and returns a view over it. +func ParseStorageRow(data []byte) (StorageRow, error) { + if len(data) == 0 { + return StorageRow{}, errors.New("data is empty") + } + if version := StorageDataVersion(data[storageVersionStart]); version != StorageDataVersion0 { + return StorageRow{}, fmt.Errorf("unsupported serialization version: %d", version) + } + if len(data) != storageDataLength { + return StorageRow{}, fmt.Errorf("data length should be %d, got %d", storageDataLength, len(data)) + } + return StorageRow{data: data}, nil +} + +// Value returns the storage slot value. +func (r StorageRow) Value() [StorageValueLength]byte { + return [StorageValueLength]byte(r.data[storageValueStart:storageDataLength]) +} + +// IsDelete reports whether the row is a tombstone: the value is all zeros. +func (r StorageRow) IsDelete() bool { + for _, b := range r.data[storageValueStart:] { + if b != 0 { + return false + } + } + return true +}