Skip to content
Draft
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
13 changes: 9 additions & 4 deletions giga/evmonly/giga_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,28 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar
return result, nil
}

// gigaSnapshotStateReader adapts a giga state view to StateReader, which has no way to report that a
// value is absent, so every read here answers a missing entry with the zero value.
type gigaSnapshotStateReader struct {
snapshot gigastore.EVMStateView
}

func (r gigaSnapshotStateReader) GetBalance(addr common.Address) *big.Int {
balance := r.snapshot.GetBalance(addr)
balance, _ := r.snapshot.GetBalance(addr)
return new(big.Int).SetBytes(balance[:])
}

func (r gigaSnapshotStateReader) GetNonce(addr common.Address) uint64 {
return r.snapshot.GetNonce(addr)
nonce, _ := r.snapshot.GetNonce(addr)
return nonce
}

func (r gigaSnapshotStateReader) GetCode(addr common.Address) []byte {
return cloneBytes(r.snapshot.GetCode(addr))
code, _ := r.snapshot.GetCode(addr)
return cloneBytes(code)
}

func (r gigaSnapshotStateReader) GetState(addr common.Address, key common.Hash) common.Hash {
return r.snapshot.GetStorage(addr, key)
value, _ := r.snapshot.GetStorage(addr, key)
return value
}
34 changes: 20 additions & 14 deletions giga/evmonly/giga_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,31 +73,37 @@ func (s *memoryGigaSnapshot) AccountExists(address gigastore.Address) bool {
return false
}

func (s *memoryGigaSnapshot) GetStorage(address gigastore.Address, slot gigastore.Hash) gigastore.Hash {
return s.storage[gigaStorageKey{address: address, key: slot}]
func (s *memoryGigaSnapshot) GetStorage(address gigastore.Address, slot gigastore.Hash) (gigastore.Hash, bool) {
value, ok := s.storage[gigaStorageKey{address: address, key: slot}]
return value, ok
}

func (s *memoryGigaSnapshot) GetBalance(address gigastore.Address) gigastore.Hash {
return s.balances[address]
func (s *memoryGigaSnapshot) GetBalance(address gigastore.Address) (gigastore.Hash, bool) {
value, ok := s.balances[address]
return value, ok
}

func (s *memoryGigaSnapshot) GetNonce(address gigastore.Address) uint64 {
return s.nonces[address]
func (s *memoryGigaSnapshot) GetNonce(address gigastore.Address) (uint64, bool) {
value, ok := s.nonces[address]
return value, ok
}

func (s *memoryGigaSnapshot) GetCodeSize(address gigastore.Address) int {
return len(s.code[address])
func (s *memoryGigaSnapshot) GetCodeSize(address gigastore.Address) (int, bool) {
code, ok := s.GetCode(address)
return len(code), ok
}

func (s *memoryGigaSnapshot) GetCodeHash(address gigastore.Address) gigastore.Hash {
if !s.AccountExists(address) {
return gigastore.Hash{}
func (s *memoryGigaSnapshot) GetCodeHash(address gigastore.Address) (gigastore.Hash, bool) {
code, ok := s.GetCode(address)
if !ok {
return gigastore.Hash{}, false
}
return crypto.Keccak256Hash(s.code[address])
return crypto.Keccak256Hash(code), true
}

func (s *memoryGigaSnapshot) GetCode(address gigastore.Address) []byte {
return s.code[address]
func (s *memoryGigaSnapshot) GetCode(address gigastore.Address) ([]byte, bool) {
code, ok := s.code[address]
return code, ok
}

func (s *memoryGigaSnapshot) GetBlockHeight() int64 {
Expand Down
69 changes: 41 additions & 28 deletions giga/evmonly/memory_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ func (s *memoryStoreSnapshot) AccountExists(address gigastore.Address) bool {
return balance != nil && balance.Sign() != 0 || s.store.base.GetNonce(address) != 0 || len(s.store.base.GetCode(address)) != 0
}

func (s *memoryStoreSnapshot) GetStorage(address gigastore.Address, slot gigastore.Hash) gigastore.Hash {
func (s *memoryStoreSnapshot) GetStorage(address gigastore.Address, slot gigastore.Hash) (gigastore.Hash, bool) {
s.requireOpen()
key := memoryStoreStorageKey{address: address, slot: slot}
s.store.mu.RLock()
Expand All @@ -400,70 +400,83 @@ func (s *memoryStoreSnapshot) GetStorage(address gigastore.Address, slot gigasto
s.store.mu.RUnlock()
if valueOK && (!clearOK || value.height >= clearHeight) {
if value.delete {
return gigastore.Hash{}
return gigastore.Hash{}, false
}
return value.value
return value.value, true
}
if clearOK {
return gigastore.Hash{}
return gigastore.Hash{}, false
}
return s.store.base.GetState(address, slot)
baseValue := s.store.base.GetState(address, slot)
// The base reader reports no presence of its own, so an unset slot is indistinguishable from one
// holding zero.
return baseValue, baseValue != (gigastore.Hash{})
}

func (s *memoryStoreSnapshot) GetBalance(address gigastore.Address) gigastore.Hash {
func (s *memoryStoreSnapshot) GetBalance(address gigastore.Address) (gigastore.Hash, bool) {
s.requireOpen()
s.store.mu.RLock()
value, ok := latestMemoryStoreValue(s.store.balances[address], s.height)
s.store.mu.RUnlock()
if ok {
return value.value
return value.value, true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Zero overlay balance reported present

Medium Severity

The memory-store overlay reports a balance as present for any latest write, including a zeroed or deleted value. FlatKV and the new read contract treat a zero balance as not stored, so GetBalance's ok disagrees across implementations when an account is drained or a balance is removed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 792b4af. Configure here.

}
var balance common.Hash
baseBalance := s.store.base.GetBalance(address)
if baseBalance != nil {
if err := validateMemoryStoreBalance(baseBalance); err != nil {
panic(err)
}
baseBalance.FillBytes(balance[:])
if baseBalance == nil {
return gigastore.Hash{}, false
}
return balance
if err := validateMemoryStoreBalance(baseBalance); err != nil {
panic(err)
}
var balance common.Hash
baseBalance.FillBytes(balance[:])
// The base reader reports no presence of its own, so an account with no balance is
// indistinguishable from one holding zero.
return balance, baseBalance.Sign() != 0
}

func (s *memoryStoreSnapshot) GetNonce(address gigastore.Address) uint64 {
func (s *memoryStoreSnapshot) GetNonce(address gigastore.Address) (uint64, bool) {
s.requireOpen()
s.store.mu.RLock()
value, ok := latestMemoryStoreValue(s.store.nonces[address], s.height)
s.store.mu.RUnlock()
if ok {
return value.value
return value.value, true
}
return s.store.base.GetNonce(address)
baseNonce := s.store.base.GetNonce(address)
// The base reader reports no presence of its own, so a missing account is indistinguishable from
// one whose nonce is zero.
return baseNonce, baseNonce != 0

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] This contradicts the contract the PR just wrote for EVMStateView: "an account that exists with a nonce of 0 reads as (0, true)" (sei-db/state_db/giga/state_view.go:50-51), and the method doc "whether addr has an account". An account present only in the base reader with nonce 0 (e.g. funded but never having sent a tx) reads as (0, false) here.

AccountExists on this same type (line 387) already solves the base-presence problem by type-asserting the base reader for an AccountExists(common.Address) bool method; GetNonce could reuse it instead of inferring presence from baseNonce != 0.

The mirror-image case is GetBalance (line 422): an explicitly written zero balance returns (0, true), whereas flatKVStateView.GetBalance returns (0, false) for a stored zero. So the two implementations of one interface disagree on the zero cases in opposite directions. No caller branches on the bool yet, so nothing is broken today — but the first one that does will get implementation-dependent behaviour. Either align the implementations or state in the interface doc that a zero value may be reported as present or absent for fields whose zero encodes absence.

}

func (s *memoryStoreSnapshot) GetCodeSize(address gigastore.Address) int {
return len(s.GetCode(address))
func (s *memoryStoreSnapshot) GetCodeSize(address gigastore.Address) (int, bool) {
code, ok := s.GetCode(address)
return len(code), ok
}

func (s *memoryStoreSnapshot) GetCodeHash(address gigastore.Address) gigastore.Hash {
s.requireOpen()
if !s.AccountExists(address) {
return gigastore.Hash{}
func (s *memoryStoreSnapshot) GetCodeHash(address gigastore.Address) (gigastore.Hash, bool) {
code, ok := s.GetCode(address)
if !ok {
return gigastore.Hash{}, false
}
return crypto.Keccak256Hash(s.GetCode(address))
return crypto.Keccak256Hash(code), true
}

func (s *memoryStoreSnapshot) GetCode(address gigastore.Address) []byte {
func (s *memoryStoreSnapshot) GetCode(address gigastore.Address) ([]byte, bool) {
s.requireOpen()
s.store.mu.RLock()
value, ok := latestMemoryStoreValue(s.store.code[address], s.height)
s.store.mu.RUnlock()
if ok {
if value.delete {
return nil
return nil, false
}
return cloneBytes(value.value)
return cloneBytes(value.value), true
}
return cloneBytes(s.store.base.GetCode(address))
baseCode := s.store.base.GetCode(address)
// The base reader reports no presence of its own, so an account with no code is indistinguishable
// from one holding empty code.
return cloneBytes(baseCode), len(baseCode) != 0
}

func (s *memoryStoreSnapshot) GetBlockHeight() int64 {
Expand Down
70 changes: 56 additions & 14 deletions giga/evmonly/memory_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,41 @@ func TestMemoryStoreSnapshotsRemainVersionedAcrossCommits(t *testing.T) {
require.False(t, ok)

require.Equal(t, int64(0), initial.GetBlockHeight())
require.Equal(t, big.NewInt(10), gigaHashToBig(initial.GetBalance(address)))
require.Equal(t, uint64(1), initial.GetNonce(address))
require.Equal(t, common.HexToHash("0xaa"), initial.GetStorage(address, baseSlot))

initialBalance, ok := initial.GetBalance(address)
require.True(t, ok)
require.Equal(t, big.NewInt(10), gigaHashToBig(initialBalance))

initialNonce, ok := initial.GetNonce(address)
require.True(t, ok)
require.Equal(t, uint64(1), initialNonce)

initialSlot, ok := initial.GetStorage(address, baseSlot)
require.True(t, ok)
require.Equal(t, common.HexToHash("0xaa"), initialSlot)

for _, snapshot := range []gigastore.StateView{current, historical} {
require.Equal(t, int64(7), snapshot.GetBlockHeight())
require.Equal(t, big.NewInt(20), gigaHashToBig(snapshot.GetBalance(address)))
require.Equal(t, uint64(2), snapshot.GetNonce(address))
require.Equal(t, []byte{0x60, 0x01}, snapshot.GetCode(address))
require.Equal(t, gigastore.Hash{}, snapshot.GetStorage(address, baseSlot))
require.Equal(t, common.HexToHash("0xbb"), snapshot.GetStorage(address, newSlot))

balance, ok := snapshot.GetBalance(address)
require.True(t, ok)
require.Equal(t, big.NewInt(20), gigaHashToBig(balance))

nonce, ok := snapshot.GetNonce(address)
require.True(t, ok)
require.Equal(t, uint64(2), nonce)

code, ok := snapshot.GetCode(address)
require.True(t, ok)
require.Equal(t, []byte{0x60, 0x01}, code)

clearedSlot, ok := snapshot.GetStorage(address, baseSlot)
require.False(t, ok, "a storage clear leaves the slot unset, not set to zero")
require.Equal(t, gigastore.Hash{}, clearedSlot)

value, ok := snapshot.GetStorage(address, newSlot)
require.True(t, ok)
require.Equal(t, common.HexToHash("0xbb"), value)
}

deleteChanges, err := store.EncodeChangeSet(StateChangeSet{
Expand All @@ -128,10 +152,22 @@ func TestMemoryStoreSnapshotsRemainVersionedAcrossCommits(t *testing.T) {
require.NoError(t, err)
require.NoError(t, store.CommitStateChanges(8, deleteChanges))
afterDelete := store.OpenView()
require.Empty(t, afterDelete.GetCode(address))
require.Equal(t, gigastore.Hash{}, afterDelete.GetStorage(address, newSlot))
require.Equal(t, []byte{0x60, 0x01}, historical.GetCode(address))
require.Equal(t, common.HexToHash("0xbb"), historical.GetStorage(address, newSlot))

deletedCode, ok := afterDelete.GetCode(address)
require.False(t, ok, "a deleted entry reads as missing, not as empty")
require.Empty(t, deletedCode)

deletedSlot, ok := afterDelete.GetStorage(address, newSlot)
require.False(t, ok)
require.Equal(t, gigastore.Hash{}, deletedSlot)

historicalCode, ok := historical.GetCode(address)
require.True(t, ok)
require.Equal(t, []byte{0x60, 0x01}, historicalCode)

historicalSlot, ok := historical.GetStorage(address, newSlot)
require.True(t, ok)
require.Equal(t, common.HexToHash("0xbb"), historicalSlot)

initial.Close()
current.Close()
Expand Down Expand Up @@ -215,8 +251,14 @@ func TestExecutorCommitsConsecutiveBlocksThroughMemoryStore(t *testing.T) {
snapshot := store.OpenView()
defer snapshot.Close()
require.Equal(t, int64(2), snapshot.GetBlockHeight())
require.Equal(t, uint64(2), snapshot.GetNonce(sender))
require.Equal(t, big.NewInt(2), gigaHashToBig(snapshot.GetBalance(recipient)))

nonce, ok := snapshot.GetNonce(sender)
require.True(t, ok)
require.Equal(t, uint64(2), nonce)

balance, ok := snapshot.GetBalance(recipient)
require.True(t, ok)
require.Equal(t, big.NewInt(2), gigaHashToBig(balance))
}

func gigaHashToBig(value gigastore.Hash) *big.Int {
Expand Down
16 changes: 14 additions & 2 deletions sei-db/common/keys/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var (
codeKeyPrefix = []byte{0x07}
codeHashKeyPrefix = []byte{0x08}
nonceKeyPrefix = []byte{0x0a}
balanceKeyPrefix = []byte{0x21}

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] This is the second declaration of 0x21; x/evm/types.BalanceKeyPrefix is the first, and the block comment above ("mirrored from x/evm/types.go") is the only thing tying them together. TestEVMKeyPrefixesAreDistinct checks the mirrored prefixes are distinct from each other but not that they still match their originals, so a future prefix change in x/evm/types reroutes balance keys to the misc lane with a green test suite.

sei-db test packages can import x/evm/types (see sei-db/state_db/ss/composite/recovery_test.go:374 using evmtypes.NonceKeyPrefix), so a drift test asserting each EVMKeyPrefixByte(kind) equals the corresponding evmtypes.*KeyPrefix[0] is available and would cover the whole mirrored set, not just balance.

)

// StateKeyPrefix returns the storage state key prefix (0x03).
Expand All @@ -34,18 +35,21 @@ func StateKeyPrefix() []byte { return stateKeyPrefix }
// EVMKeyKind identifies an EVM key family.
type EVMKeyKind uint8

// These values are in-memory routing tags, renumbered whenever a kind is added. Writing one into a
// key, a value, or any other stored or wire format is forbidden.
const (
EVMKeyEmpty EVMKeyKind = iota // Returned only for zero-length keys
EVMKeyNonce // Stripped key: 20-byte address
EVMKeyCodeHash // Stripped key: 20-byte address
EVMKeyBalance // Stripped key: 20-byte address
EVMKeyCode // Stripped key: 20-byte address
EVMKeyStorage // Stripped key: addr||slot (20+32 bytes)
EVMKeyMisc // Full original key preserved (address mappings, codesize, etc.)
)

// ParseEVMKey parses an EVM key from the x/evm store keyspace.
//
// For optimized keys (nonce, code, codehash, storage), keyBytes is the stripped key.
// For optimized keys (nonce, code, codehash, storage, balance), keyBytes is the stripped key.
// For misc keys (all other EVM data including codesize), keyBytes is the full original key.
// Only returns EVMKeyEmpty for zero-length keys.
func ParseEVMKey(key []byte) (kind EVMKeyKind, keyBytes []byte) {
Expand Down Expand Up @@ -77,6 +81,12 @@ func ParseEVMKey(key []byte) (kind EVMKeyKind, keyBytes []byte) {
return EVMKeyMisc, key
}
return EVMKeyStorage, key[len(stateKeyPrefix):]

case bytes.HasPrefix(key, balanceKeyPrefix):
if len(key) != len(balanceKeyPrefix)+AddressLen {
return EVMKeyMisc, key
}
return EVMKeyBalance, key[len(balanceKeyPrefix):]
}

// All other EVM keys go to the misc store (address mappings, codesize, etc.)
Expand All @@ -95,6 +105,8 @@ func EVMKeyPrefixByte(kind EVMKeyKind) (byte, bool) {
return codeHashKeyPrefix[0], true
case EVMKeyCode:
return codeKeyPrefix[0], true
case EVMKeyBalance:
return balanceKeyPrefix[0], true
default:
return 0, false
}
Expand Down Expand Up @@ -123,7 +135,7 @@ func InternalKeyLen(kind EVMKeyKind) int {
switch kind {
case EVMKeyStorage:
return AddressLen + slotLen // 52 bytes
case EVMKeyNonce, EVMKeyCodeHash, EVMKeyCode:
case EVMKeyNonce, EVMKeyCodeHash, EVMKeyCode, EVMKeyBalance:
return AddressLen // 20 bytes
default:
return 0
Expand Down
Loading
Loading