core, core/state: address slow-block review feedback

- Pre-BAL path keeps StateDB synchronous read-time accumulators
  (AccountReads, StorageReads, CodeReads, CodeLoaded, CodeLoadBytes)
  so Execution = ptime - reads stays well-formed under single-thread
  execution.
- BAL path drops aggregate reader read-times; under parallel workers
  they sum across goroutines and aren't a wall-clock proxy.
- Delete dead PrefetchReadTimes/WaitPrefetch forwarders on *reader and
  the now-unused ReadTimer/ReadDurations scaffolding.
- Add regression test for EIP-7702 delegation clear: empty []byte code
  in the BAL must reset CodeHash to EmptyCodeHash.
This commit is contained in:
CPerezz 2026-05-08 11:24:47 +02:00 committed by Jared Wasinger
parent fd76921afa
commit 4183a70bd6
7 changed files with 138 additions and 77 deletions

View file

@ -680,10 +680,6 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block *
stats.DatabaseCommit = m.TrieDBCommits
stats.Prefetch = m.StatePrefetch
}
readerReads := prefetchReader.(state.ReadTimer).ReadTimes()
stats.AccountReads = readerReads.Account
stats.StorageReads = readerReads.Storage
stats.CodeReads = readerReads.Code
stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed
stats.StateReadCacheStats = prefetchReader.(state.ReaderStater).GetStats()
@ -2450,14 +2446,13 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
proctime = time.Since(startTime) // processing + validation + cross validation
stats = &ExecuteStats{}
)
reads := statedb.Reader().(state.ReadTimer).ReadTimes()
codeLoaded, codeLoadBytes := statedb.Reader().(state.CodeLoadTracker).CodeLoads()
stats.AccountReads = reads.Account
stats.StorageReads = reads.Storage
stats.CodeReads = reads.Code
stats.AccountUpdates = statedb.AccountUpdates
stats.StorageUpdates = statedb.StorageUpdates
stats.AccountHashes = statedb.AccountHashes
// Update the metrics touched during block processing and validation
stats.AccountReads = statedb.AccountReads // Account reads are complete (in processing)
stats.StorageReads = statedb.StorageReads // Storage reads are complete (in processing)
stats.AccountUpdates = statedb.AccountUpdates // Account updates are complete (in validation)
stats.StorageUpdates = statedb.StorageUpdates // Storage updates are complete (in validation)
stats.AccountHashes = statedb.AccountHashes // Account hashes are complete (in validation)
stats.CodeReads = statedb.CodeReads
stats.AccountLoaded = statedb.AccountLoaded
stats.AccountUpdated = statedb.AccountUpdated
@ -2465,13 +2460,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
stats.StorageLoaded = statedb.StorageLoaded
stats.StorageUpdated = int(statedb.StorageUpdated.Load())
stats.StorageDeleted = int(statedb.StorageDeleted.Load())
stats.CodeLoaded = codeLoaded
stats.CodeLoadBytes = codeLoadBytes
stats.CodeLoaded = statedb.CodeLoaded
stats.CodeLoadBytes = statedb.CodeLoadBytes
stats.CodeUpdated = statedb.CodeUpdated
stats.CodeUpdateBytes = statedb.CodeUpdateBytes
stats.Execution = ptime - (reads.Account + reads.Storage + reads.Code)
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates)
stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // EVM processing time
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // Block validation time
stats.CrossValidation = xvtime
// Write the block to the chain and get the status.

View file

@ -0,0 +1,91 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package state
import (
"bytes"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/holiman/uint256"
)
// TestBALStateTransition_EIP7702DelegationClear locks in the fix for the
// `if len(code) > 0` regression that skipped legitimate EIP-7702 delegation
// clears (encoded as a non-nil empty byte slice in the BAL). After the fix
// the gate is `if code != nil`, so a delegation clear correctly resets
// `acct.CodeHash` to `EmptyCodeHash` and is NOT counted as a deletion.
func TestBALStateTransition_EIP7702DelegationClear(t *testing.T) {
addr := common.HexToAddress("0x000000000000000000000000000000000000aaaa")
// Pre-state: account already holds a 7702 delegation (non-empty code).
sdb := NewDatabaseForTesting()
prestate, _ := New(types.EmptyRootHash, sdb)
delegationCode := append([]byte{0xef, 0x01, 0x00}, common.HexToAddress("0xbeef").Bytes()...)
prestate.SetBalance(addr, uint256.NewInt(1e18), tracing.BalanceChangeUnspecified)
prestate.SetNonce(addr, 1, tracing.NonceChangeUnspecified)
prestate.SetCode(addr, delegationCode, tracing.CodeChangeUnspecified)
parentRoot, err := prestate.Commit(0, false, false)
if err != nil {
t.Fatalf("Commit prestate: %v", err)
}
if err := sdb.TrieDB().Commit(parentRoot, false); err != nil {
t.Fatalf("TrieDB Commit: %v", err)
}
// Build a BAL whose only mutation is a 7702 delegation clear.
// Code is non-nil but length zero — the canonical encoding.
construction := make(bal.ConstructionBlockAccessList)
construction.AccumulateMutations(bal.StateMutations{
addr: bal.AccountMutations{Code: bal.ContractCode{}},
}, 0)
accessList := construction.ToEncodingObj()
// Synthesize a zero-tx block carrying the access list.
block := types.NewBlockWithHeader(&types.Header{Number: common.Big1}).WithAccessList(accessList)
// Run the BAL state transition.
reader, err := sdb.Reader(parentRoot)
if err != nil {
t.Fatalf("Reader: %v", err)
}
bst, err := NewBALStateTransition(block, reader, sdb, parentRoot)
if err != nil {
t.Fatalf("NewBALStateTransition: %v", err)
}
bst.IntermediateRoot(false)
if err := bst.Error(); err != nil {
t.Fatalf("IntermediateRoot: %v", err)
}
post, ok := bst.postStates[addr]
if !ok {
t.Fatal("post-state must exist for the address (account is updated, not deleted)")
}
if !bytes.Equal(post.CodeHash, types.EmptyCodeHash.Bytes()) {
t.Fatalf("CodeHash: got %x, want %x", post.CodeHash, types.EmptyCodeHash.Bytes())
}
if d := bst.Deletions().Accounts; d != 0 {
t.Fatalf("Deletions.Accounts: got %d, want 0 (delegation clear is not a deletion)", d)
}
if _, deleted := bst.deletions[addr]; deleted {
t.Fatal("address must not be in s.deletions after a 7702 clear")
}
}

View file

@ -20,7 +20,6 @@ import (
"errors"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/overlay"
@ -532,10 +531,6 @@ type reader struct {
StateReader
PrefetcherMetricer
accountReadNS atomic.Int64
storageReadNS atomic.Int64
codeReadNS atomic.Int64
codeLoaded sync.Map // common.Address → int (first-seen len(code))
}
@ -555,17 +550,14 @@ func newReaderWithPrefetch(codeReader ContractCodeReader, stateReader StateReade
}
func (r *reader) Account(addr common.Address) (*types.StateAccount, error) {
defer func(start time.Time) { r.accountReadNS.Add(int64(time.Since(start))) }(time.Now())
return r.StateReader.Account(addr)
}
func (r *reader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
defer func(start time.Time) { r.storageReadNS.Add(int64(time.Since(start))) }(time.Now())
return r.StateReader.Storage(addr, slot)
}
func (r *reader) Code(addr common.Address, codeHash common.Hash) []byte {
defer func(start time.Time) { r.codeReadNS.Add(int64(time.Since(start))) }(time.Now())
code := r.ContractCodeReader.Code(addr, codeHash)
if len(code) > 0 {
r.codeLoaded.LoadOrStore(addr, len(code))
@ -574,7 +566,6 @@ func (r *reader) Code(addr common.Address, codeHash common.Hash) []byte {
}
func (r *reader) CodeSize(addr common.Address, codeHash common.Hash) (int, error) {
defer func(start time.Time) { r.codeReadNS.Add(int64(time.Since(start))) }(time.Now())
size, err := r.ContractCodeReader.CodeSize(addr, codeHash)
if err == nil && size > 0 {
r.codeLoaded.LoadOrStore(addr, size)
@ -582,14 +573,6 @@ func (r *reader) CodeSize(addr common.Address, codeHash common.Hash) (int, error
return size, err
}
func (r *reader) ReadTimes() ReadDurations {
return ReadDurations{
Account: time.Duration(r.accountReadNS.Load()),
Storage: time.Duration(r.storageReadNS.Load()),
Code: time.Duration(r.codeReadNS.Load()),
}
}
// CodeLoads returns the count of unique contracts whose code was fetched and
// the sum of their first-seen byte lengths. Call after Reader use has quiesced.
func (r *reader) CodeLoads() (count, bytes int) {
@ -625,19 +608,3 @@ func (r *reader) GetStats() ReaderStats {
}
}
// PrefetchReadTimes forwards to the wrapped prefetcher, or returns zero.
func (r *reader) PrefetchReadTimes() (account, storage time.Duration) {
if pr, ok := r.StateReader.(interface {
PrefetchReadTimes() (time.Duration, time.Duration)
}); ok {
return pr.PrefetchReadTimes()
}
return 0, 0
}
// WaitPrefetch blocks until the wrapped prefetcher drains; no-op otherwise.
func (r *reader) WaitPrefetch() {
if pr, ok := r.StateReader.(interface{ Wait() error }); ok {
_ = pr.Wait()
}
}

View file

@ -383,10 +383,6 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) {
list[slot] = struct{}{}
}
func (r *readerTracker) ReadTimes() ReadDurations {
return r.Reader.(ReadTimer).ReadTimes()
}
func (r *readerTracker) CodeLoads() (count, bytes int) {
return r.Reader.(CodeLoadTracker).CodeLoads()
}

View file

@ -16,20 +16,6 @@
package state
import "time"
// ReadDurations groups cumulative read durations by category.
type ReadDurations struct {
Account time.Duration
Storage time.Duration
Code time.Duration
}
// ReadTimer exposes a Reader's cumulative read durations.
type ReadTimer interface {
ReadTimes() ReadDurations
}
// CodeLoadTracker exposes a Reader's deduplicated code-load count and bytes.
type CodeLoadTracker interface {
CodeLoads() (count, bytes int)

View file

@ -21,6 +21,7 @@ import (
"fmt"
"maps"
"slices"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
@ -225,11 +226,13 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
}
s.db.StorageLoaded++
start := time.Now()
value, err := s.db.reader.Storage(s.address, key)
if err != nil {
s.db.setError(err)
return common.Hash{}
}
s.db.StorageReads += time.Since(start)
// Schedule the resolved storage slots for prefetching if it's enabled.
if s.db.prefetcher != nil && s.data.Root != types.EmptyRootHash {
@ -641,6 +644,12 @@ func (s *stateObject) Code() []byte {
if bytes.Equal(s.CodeHash(), types.EmptyCodeHash.Bytes()) {
return nil
}
defer func(start time.Time) {
s.db.CodeLoaded += 1
s.db.CodeReads += time.Since(start)
s.db.CodeLoadBytes += len(s.code)
}(time.Now())
code := s.db.reader.Code(s.address, common.BytesToHash(s.CodeHash()))
if len(code) == 0 {
s.db.setError(fmt.Errorf("code is not found %x", s.CodeHash()))
@ -659,6 +668,11 @@ func (s *stateObject) CodeSize() int {
if bytes.Equal(s.CodeHash(), types.EmptyCodeHash.Bytes()) {
return 0
}
defer func(start time.Time) {
s.db.CodeLoaded += 1
s.db.CodeReads += time.Since(start)
}(time.Now())
size, err := s.db.reader.CodeSize(s.address, common.BytesToHash(s.CodeHash()))
if err != nil {
s.db.setError(err)

View file

@ -156,24 +156,33 @@ type StateDB struct {
// State witness if cross validation is needed
witness *stateless.Witness
// Per-block counters surfaced in ExecuteStats; read durations and code-loads
// are tracked on the reader (see ReadTimer, CodeLoadTracker).
// Measurements gathered during execution for debugging purposes
AccountReads time.Duration
AccountHashes time.Duration
AccountUpdates time.Duration
AccountCommits time.Duration
StorageReads time.Duration
StorageUpdates time.Duration
StorageCommits time.Duration
DatabaseCommits time.Duration
CodeReads time.Duration
AccountLoaded int
AccountUpdated int
AccountDeleted int
StorageLoaded int
StorageUpdated atomic.Int64
StorageDeleted atomic.Int64
CodeUpdated int
CodeUpdateBytes int
AccountLoaded int // Number of accounts retrieved from the database during the state transition
AccountUpdated int // Number of accounts updated during the state transition
AccountDeleted int // Number of accounts deleted during the state transition
StorageLoaded int // Number of storage slots retrieved from the database during the state transition
StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition
StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition
// CodeLoadBytes is the total number of bytes read from contract code.
// This value may be smaller than the actual number of bytes read, since
// some APIs (e.g. CodeSize) may load the entire code from either the
// cache or the database when the size is not available in the cache.
CodeLoaded int // Number of contract code loaded during the state transition
CodeLoadBytes int // Total bytes of resolved code
CodeUpdated int // Number of contracts with code changes that persisted
CodeUpdateBytes int // Total bytes of persisted code written
}
// New creates a new state from a given trie.
@ -626,11 +635,13 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
s.AccountLoaded++
start := time.Now()
acct, err := s.reader.Account(addr)
if err != nil {
s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err))
return nil
}
s.AccountReads += time.Since(start)
// Short circuit if the account is not found
if acct == nil {