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: 3 additions & 10 deletions execution/builder/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,16 +241,9 @@ func execBlock(ctx context0.Context, sd *execctx.SharedDomains, tx kv.TemporalTx
blockRules := blockCtx.Rules(cfg.chainConfig)
var domainKeysErr error
domainStorageKeys := func(addr accounts.Address) []accounts.StorageKey {
av := addr.Value()
const addrLen, hashLen = 20, 32
var keys []accounts.StorageKey
if iterErr := sd.IteratePrefix(kv.StorageDomain, av[:], tx, func(k, _ []byte) (bool, error) {
if len(k) >= addrLen+hashLen {
keys = append(keys, accounts.InternKey(common.BytesToHash(k[addrLen:addrLen+hashLen])))
}
return true, nil
}); iterErr != nil {
domainKeysErr = iterErr
keys, err := state.CommittedStorageKeys(sd, tx, nil, addr)
if err != nil {
domainKeysErr = err
return nil
}
return keys
Expand Down
26 changes: 6 additions & 20 deletions execution/stagedsync/exec3_parallel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3051,16 +3051,9 @@ func (be *blockExecutor) nextResult(ctx context.Context, pe *parallelExecutor, r
// and prior-block storage that vm.StorageKeys doesn't see.
var domainKeysErr error
domainStorageKeys := func(addr accounts.Address) []accounts.StorageKey {
av := addr.Value()
const addrLen, hashLen = 20, 32 // StorageDomain composite key = addr ++ slotHash
var keys []accounts.StorageKey
if iterErr := pe.rs.Domains().IteratePrefix(kv.StorageDomain, av[:], applyTx, func(k, _ []byte) (bool, error) {
if len(k) >= addrLen+hashLen {
keys = append(keys, accounts.InternKey(common.BytesToHash(k[addrLen:addrLen+hashLen])))
}
return true, nil
}); iterErr != nil {
domainKeysErr = iterErr
keys, err := state.CommittedStorageKeys(pe.rs.Domains(), applyTx, be.blockStateCache, addr)
if err != nil {
domainKeysErr = err
return nil
}
return keys
Expand Down Expand Up @@ -3322,16 +3315,9 @@ func (be *blockExecutor) nextResult(ctx context.Context, pe *parallelExecutor, r
// redundant.
var domainKeysErr error
domainStorageKeys := func(addr accounts.Address) []accounts.StorageKey {
av := addr.Value()
const addrLen, hashLen = 20, 32
var keys []accounts.StorageKey
if iterErr := pe.rs.Domains().IteratePrefix(kv.StorageDomain, av[:], applyTx, func(k, _ []byte) (bool, error) {
if len(k) >= addrLen+hashLen {
keys = append(keys, accounts.InternKey(common.BytesToHash(k[addrLen:addrLen+hashLen])))
}
return true, nil
}); iterErr != nil {
domainKeysErr = iterErr
keys, err := state.CommittedStorageKeys(pe.rs.Domains(), applyTx, be.blockStateCache, addr)
if err != nil {
domainKeysErr = err
return nil
}
return keys
Expand Down
13 changes: 13 additions & 0 deletions execution/state/rw_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,19 @@ func (c *BlockStateCache) DeleteAccount(addr accounts.Address, txNum uint64) {
c.mu.Unlock()
}

// deletedInBlock reports an address destroyed earlier in this block. DeleteAccount
// records that as a present-but-nil current entry, which the block-end flush turns
// into the domain deletes. Nil receiver: no cache, so no such window.
func (c *BlockStateCache) deletedInBlock(addr accounts.Address) bool {
if c == nil {
return false
}
c.mu.RLock()
defer c.mu.RUnlock()
enc, present := c.currentAccounts[addr]
return present && enc == nil
}

// GetCurrentAccountDecoded returns the latest account (including intra-block
// writes), avoiding GetCurrentAccount's re-encode of the committed entry.
func (c *BlockStateCache) GetCurrentAccountDecoded(addr accounts.Address) (*accounts.Account, bool, error) {
Expand Down
37 changes: 37 additions & 0 deletions execution/state/writeset_normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package state
import (
"github.com/holiman/uint256"

"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/log/v3"
"github.com/erigontech/erigon/db/kv"
"github.com/erigontech/erigon/db/state/execctx"
"github.com/erigontech/erigon/diagnostics/metrics"
"github.com/erigontech/erigon/execution/types"
"github.com/erigontech/erigon/execution/types/accounts"
Expand Down Expand Up @@ -488,3 +491,37 @@ func (writes *WriteSet) Normalize(vm *VersionMap, txIndex int, incarnation int,

return filtered, nil
}

// CommittedStorageKeys returns every storage slot committed for addr, the
// domainStorageKeys input Normalize needs to emit a full self-destruct cascade.
//
// An address with no committed account holds no committed storage: storage is
// only written for an account that exists, and deleting an account wipes its
// storage prefix. So probe the account first -- that read is served by the
// per-file existence filters, while the prefix walk has to seek the .bt index of
// every storage .kv file, paying the same price whether the address owns a
// thousand slots or none.
func CommittedStorageKeys(domains *execctx.SharedDomains, tx kv.TemporalTx, blockCache *BlockStateCache, addr accounts.Address) ([]accounts.StorageKey, error) {
av := addr.Value()
prevAcc, _, err := domains.GetLatest(kv.AccountsDomain, tx, av[:])
if err != nil {
return nil, err
}
// A destroy recorded in the block cache only reaches the domain at the
// block-end flush, so until then the account reads absent while its
// pre-block storage is still there and still owed a trie delete.
if len(prevAcc) == 0 && !blockCache.deletedInBlock(addr) {
return nil, assertNoCommittedStorage(domains, tx, av[:])
}
const addrLen, hashLen = 20, 32 // StorageDomain composite key = addr ++ slotHash
var keys []accounts.StorageKey
if err := domains.IteratePrefix(kv.StorageDomain, av[:], tx, func(k, _ []byte) (bool, error) {
if len(k) >= addrLen+hashLen {
keys = append(keys, accounts.InternKey(common.BytesToHash(k[addrLen:addrLen+hashLen])))
}
return true, nil
}); err != nil {
return nil, err
}
return keys, nil
}
122 changes: 122 additions & 0 deletions execution/state/writeset_normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"github.com/stretchr/testify/require"

"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/dbg"
"github.com/erigontech/erigon/db/kv"
"github.com/erigontech/erigon/execution/chain"
"github.com/erigontech/erigon/execution/types/accounts"
)

Expand Down Expand Up @@ -255,3 +258,122 @@ func TestAssertSelfDestructNormalized(t *testing.T) {
ws.assertSelfDestructNormalized()
}, "balance and storage deletes are legal on a self-destructed address")
}

// TestCommittedStorageKeys pins the account probe that guards the self-destruct
// storage walk. The walk seeks the .bt index of every storage .kv file, so its
// cost is set by the file count rather than by how much storage the address
// owns -- an address that owns none pays full price. A contract created and
// destroyed inside one batch never has a committed account, which is what makes
// the probe worth its own read.
func TestCommittedStorageKeys(t *testing.T) {
_, tx, domains := NewTestRwTx(t)
slot := common.HexToHash("0x01")

putStorage := func(t *testing.T, addr accounts.Address) {
t.Helper()
av := addr.Value()
key := append(append([]byte{}, av[:]...), slot[:]...)
require.NoError(t, domains.DomainPut(kv.StorageDomain, tx, key, []byte{0x42}, 0, nil))
}

t.Run("walks when the account is committed", func(t *testing.T) {
addr := accounts.InternAddress(common.HexToAddress("0xaa"))
av := addr.Value()
acc := accounts.Account{Nonce: 1, CodeHash: accounts.EmptyCodeHash}
require.NoError(t, domains.DomainPut(kv.AccountsDomain, tx, av[:], accounts.SerialiseV3(&acc), 0, nil))
putStorage(t, addr)

keys, err := CommittedStorageKeys(domains, tx, nil, addr)
require.NoError(t, err)
require.Equal(t, []accounts.StorageKey{accounts.InternKey(slot)}, keys)
})

t.Run("skips the walk without a committed account", func(t *testing.T) {
// Storage with no account is the state the guard treats as unreachable.
// Writing it is what makes the skip observable: a walk would return it.
defer func(v bool) { dbg.AssertEnabled = v }(dbg.AssertEnabled)
dbg.AssertEnabled = false
addr := accounts.InternAddress(common.HexToAddress("0xbb"))
putStorage(t, addr)

keys, err := CommittedStorageKeys(domains, tx, nil, addr)
require.NoError(t, err)
require.Empty(t, keys, "walked the storage prefix for an address with no committed account")
})

t.Run("walks when the account was destroyed in this block", func(t *testing.T) {
// The self-destruct apply path leaves the storage prefix in the domain
// until the block-end flush when a block cache is active, so an absent
// account no longer implies absent storage. Skipping here loses the
// cascade the trie needs and the root goes wrong.
defer func(v bool) { dbg.AssertEnabled = v }(dbg.AssertEnabled)
dbg.AssertEnabled = false
addr := accounts.InternAddress(common.HexToAddress("0xdd"))
putStorage(t, addr)
cache := NewBlockStateCache()
cache.DeleteAccount(addr, 0)

keys, err := CommittedStorageKeys(domains, tx, cache, addr)
require.NoError(t, err)
require.Equal(t, []accounts.StorageKey{accounts.InternKey(slot)}, keys,
"skipped the walk for an address destroyed earlier in this block")
})

t.Run("asserts on storage without an account", func(t *testing.T) {
defer func(v bool) { dbg.AssertEnabled = v }(dbg.AssertEnabled)
dbg.AssertEnabled = true
addr := accounts.InternAddress(common.HexToAddress("0xcc"))
putStorage(t, addr)

require.Panics(t, func() { _, _ = CommittedStorageKeys(domains, tx, nil, addr) },
"the skip rests on this invariant, so breaking it must not stay silent")
})
}

// TestSelfDestructCascade_PerTxnHistory pins per-txn history granularity across a
// self-destruct. Erigon records history per transaction, so an RPC re-exec of a
// later transaction must see the destroyed contract's storage already gone while
// a read at the destroying txNum still sees it. Those history records exist only
// because the cascade enumerates the committed slots -- a walk that is skipped
// when it should not be leaves the deletes out of history, which a trie-root
// check cannot see since it only compares the block's final state.
func TestSelfDestructCascade_PerTxnHistory(t *testing.T) {
_, tx, domains := NewTestRwTx(t)
addr := accounts.InternAddress(common.HexToAddress("0x5d"))
slot := common.HexToHash("0x07")
av := addr.Value()
composite := append(append([]byte{}, av[:]...), slot[:]...)

const preTxNum, sdTxNum = uint64(1), uint64(4)

acc := accounts.Account{Nonce: 1, CodeHash: accounts.EmptyCodeHash}
require.NoError(t, domains.DomainPut(kv.AccountsDomain, tx, av[:], accounts.SerialiseV3(&acc), preTxNum, nil))
require.NoError(t, domains.DomainPut(kv.StorageDomain, tx, composite, []byte{0xaa}, preTxNum, nil))

ws := &WriteSet{}
ws.SetSelfDestruct(addr, &VersionedWrite[bool]{
WriteHeader: WriteHeader{Address: addr, Path: SelfDestructPath, Version: Version{TxIndex: 1}},
Val: true,
})
domainKeys := func(a accounts.Address) []accounts.StorageKey {
keys, err := CommittedStorageKeys(domains, tx, nil, a)
require.NoError(t, err)
return keys
}
out, err := ws.Normalize(NewVersionMap(nil), 1, 0, &minimalStateReader{}, domainKeys, false, false, false)
require.NoError(t, err)
_, cascaded := out.GetStorage(addr, accounts.InternKey(slot))
require.True(t, cascaded, "the cascade dropped the committed slot, so history never records its delete")

require.NoError(t, out.Apply(domains, tx, 0, sdTxNum, nil, &chain.Rules{}, nil, false))
// History is queried through the tx, so the batch has to reach it first.
require.NoError(t, domains.Flush(t.Context(), tx))

before, _, err := tx.GetAsOf(kv.StorageDomain, composite, sdTxNum)
require.NoError(t, err)
require.Equal(t, []byte{0xaa}, before, "history lost the slot value the destroying txn read")

after, _, err := tx.GetAsOf(kv.StorageDomain, composite, sdTxNum+1)
require.NoError(t, err)
require.Empty(t, after, "a later txNum still sees the destroyed contract's storage")
}
13 changes: 3 additions & 10 deletions execution/tests/blockgen/chain_makers.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,16 +576,9 @@ func GenerateChain(config *chain.Config, parent *types.Block, engine rules.Engin
blockNum := b.header.Number.Uint64()
var domainKeysErr error
domainStorageKeys := func(addr accounts.Address) []accounts.StorageKey {
av := addr.Value()
const addrLen, hashLen = 20, 32
var keys []accounts.StorageKey
if iterErr := domains.IteratePrefix(kv.StorageDomain, av[:], tx, func(k, _ []byte) (bool, error) {
if len(k) >= addrLen+hashLen {
keys = append(keys, accounts.InternKey(common.BytesToHash(k[addrLen:addrLen+hashLen])))
}
return true, nil
}); iterErr != nil {
domainKeysErr = iterErr
keys, err := state.CommittedStorageKeys(domains, tx, nil, addr)
if err != nil {
domainKeysErr = err
return nil
}
return keys
Expand Down
Loading