Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions sei-db/db_engine/view/shard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
15 changes: 15 additions & 0 deletions sei-db/state_db/sc/flatkv/ktype/ktype.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] AppendEVMPhysicalKey duplicates EVMPhysicalKey's codehash/balance canonicalization, prefix-byte lookup, and layout. EVMPhysicalKey can become a one-liner over it — return AppendEVMPhysicalKey(make([]byte, 0, len(keys.EVMStoreKey)+2+len(strippedKey)), kind, strippedKey) — which keeps its single allocation and its nil-on-unknown-kind return, and leaves one place where the physical key layout is defined. Right now a change to the layout has to be made twice, and the two can diverge silently since nothing asserts they agree.

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...)
}
62 changes: 48 additions & 14 deletions sei-db/state_db/sc/flatkv/state_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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[:])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] GetStorage inlines key building, row parsing, the panic message, and the tombstone check, while the account read next to it is the named accountRow step. A matching storageRow(addr, key) (vtype.StorageRow, bool) would restore the symmetry and keep GetStorage a single named step, which is the shape the rest of this file uses.

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 {
Expand All @@ -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 {
Expand Down
70 changes: 70 additions & 0 deletions sei-db/state_db/sc/flatkv/state_view_bench_test.go
Original file line number Diff line number Diff line change
@@ -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{})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] config.DefaultTestConfig(&testing.T{}) calls TempDir() on a zero-value testing.T: it happens to work today, but the directory it creates is registered with that fake T's cleanup list, which never runs, so each benchmark invocation leaks an empty directory under the system temp dir (the real data dir is then overwritten on the next line). Widening DefaultTestConfig to testing.TB and passing b removes both the leak and the dependency on zero-value testing.T internals.

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)
}
}
3 changes: 1 addition & 2 deletions sei-db/state_db/sc/flatkv/vtype/code_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The returned bytecode now aliases data, so a CodeData is only valid as long as its source bytes are — but neither DeserializeCodeData's nor GetBytecode's doc says so, and DeserializeMiscData still copies, so the package contract is now per-type and only discoverable by reading the bodies. The type comment covers mutation ("not safe to modify without first copying") but not lifetime. Worth stating the aliasing on DeserializeCodeData, since the non-flatkv callers (composite.convertFlatKVNodes, seidb evm_logical_digest) hand the slice on to code that has no view of where it came from.


return &CodeData{
version: version,
Expand Down
92 changes: 92 additions & 0 deletions sei-db/state_db/sc/flatkv/vtype/rows.go
Original file line number Diff line number Diff line change
@@ -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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] ParseAccountRow/ParseStorageRow re-implement the validation that DeserializeAccountData/DeserializeStorageData already perform (non-empty, version byte, accepted lengths), and AccountRow.IsDelete/StorageRow.IsDelete restate the tombstone rule a second time. The row format now has two readers per type, and state_view.go keeps accountData/storageData alive solely for the Get path, so both definitions are live. Consider making the old types delegate (AccountData wrapping an AccountRow, or IsDelete forwarding), or moving Get onto the row types, so the tombstone rule and the length/version contract each have one definition.

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
}
Loading