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.DatabaseCommit = m.TrieDBCommits
stats.Prefetch = m.StatePrefetch 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.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed
stats.StateReadCacheStats = prefetchReader.(state.ReaderStater).GetStats() 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 proctime = time.Since(startTime) // processing + validation + cross validation
stats = &ExecuteStats{} stats = &ExecuteStats{}
) )
reads := statedb.Reader().(state.ReadTimer).ReadTimes() // Update the metrics touched during block processing and validation
codeLoaded, codeLoadBytes := statedb.Reader().(state.CodeLoadTracker).CodeLoads() stats.AccountReads = statedb.AccountReads // Account reads are complete (in processing)
stats.AccountReads = reads.Account stats.StorageReads = statedb.StorageReads // Storage reads are complete (in processing)
stats.StorageReads = reads.Storage stats.AccountUpdates = statedb.AccountUpdates // Account updates are complete (in validation)
stats.CodeReads = reads.Code stats.StorageUpdates = statedb.StorageUpdates // Storage updates are complete (in validation)
stats.AccountUpdates = statedb.AccountUpdates stats.AccountHashes = statedb.AccountHashes // Account hashes are complete (in validation)
stats.StorageUpdates = statedb.StorageUpdates stats.CodeReads = statedb.CodeReads
stats.AccountHashes = statedb.AccountHashes
stats.AccountLoaded = statedb.AccountLoaded stats.AccountLoaded = statedb.AccountLoaded
stats.AccountUpdated = statedb.AccountUpdated stats.AccountUpdated = statedb.AccountUpdated
@ -2465,13 +2460,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
stats.StorageLoaded = statedb.StorageLoaded stats.StorageLoaded = statedb.StorageLoaded
stats.StorageUpdated = int(statedb.StorageUpdated.Load()) stats.StorageUpdated = int(statedb.StorageUpdated.Load())
stats.StorageDeleted = int(statedb.StorageDeleted.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.CodeUpdated = statedb.CodeUpdated
stats.CodeUpdateBytes = statedb.CodeUpdateBytes stats.CodeUpdateBytes = statedb.CodeUpdateBytes
stats.Execution = ptime - (reads.Account + reads.Storage + reads.Code) stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // EVM processing time
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // Block validation time
stats.CrossValidation = xvtime stats.CrossValidation = xvtime
// Write the block to the chain and get the status. // 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" "errors"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/overlay" "github.com/ethereum/go-ethereum/core/overlay"
@ -532,10 +531,6 @@ type reader struct {
StateReader StateReader
PrefetcherMetricer PrefetcherMetricer
accountReadNS atomic.Int64
storageReadNS atomic.Int64
codeReadNS atomic.Int64
codeLoaded sync.Map // common.Address → int (first-seen len(code)) 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) { 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) return r.StateReader.Account(addr)
} }
func (r *reader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { 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) return r.StateReader.Storage(addr, slot)
} }
func (r *reader) Code(addr common.Address, codeHash common.Hash) []byte { 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) code := r.ContractCodeReader.Code(addr, codeHash)
if len(code) > 0 { if len(code) > 0 {
r.codeLoaded.LoadOrStore(addr, len(code)) 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) { 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) size, err := r.ContractCodeReader.CodeSize(addr, codeHash)
if err == nil && size > 0 { if err == nil && size > 0 {
r.codeLoaded.LoadOrStore(addr, size) r.codeLoaded.LoadOrStore(addr, size)
@ -582,14 +573,6 @@ func (r *reader) CodeSize(addr common.Address, codeHash common.Hash) (int, error
return size, err 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 // 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. // the sum of their first-seen byte lengths. Call after Reader use has quiesced.
func (r *reader) CodeLoads() (count, bytes int) { 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{}{} list[slot] = struct{}{}
} }
func (r *readerTracker) ReadTimes() ReadDurations {
return r.Reader.(ReadTimer).ReadTimes()
}
func (r *readerTracker) CodeLoads() (count, bytes int) { func (r *readerTracker) CodeLoads() (count, bytes int) {
return r.Reader.(CodeLoadTracker).CodeLoads() return r.Reader.(CodeLoadTracker).CodeLoads()
} }

View file

@ -16,20 +16,6 @@
package state 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. // CodeLoadTracker exposes a Reader's deduplicated code-load count and bytes.
type CodeLoadTracker interface { type CodeLoadTracker interface {
CodeLoads() (count, bytes int) CodeLoads() (count, bytes int)

View file

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

View file

@ -156,24 +156,33 @@ type StateDB struct {
// State witness if cross validation is needed // State witness if cross validation is needed
witness *stateless.Witness witness *stateless.Witness
// Per-block counters surfaced in ExecuteStats; read durations and code-loads // Measurements gathered during execution for debugging purposes
// are tracked on the reader (see ReadTimer, CodeLoadTracker). AccountReads time.Duration
AccountHashes time.Duration AccountHashes time.Duration
AccountUpdates time.Duration AccountUpdates time.Duration
AccountCommits time.Duration AccountCommits time.Duration
StorageReads time.Duration
StorageUpdates time.Duration StorageUpdates time.Duration
StorageCommits time.Duration StorageCommits time.Duration
DatabaseCommits time.Duration DatabaseCommits time.Duration
CodeReads time.Duration
AccountLoaded int AccountLoaded int // Number of accounts retrieved from the database during the state transition
AccountUpdated int AccountUpdated int // Number of accounts updated during the state transition
AccountDeleted int AccountDeleted int // Number of accounts deleted during the state transition
StorageLoaded int StorageLoaded int // Number of storage slots retrieved from the database during the state transition
StorageUpdated atomic.Int64 StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition
StorageDeleted atomic.Int64 StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition
CodeUpdated int
CodeUpdateBytes int // 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. // New creates a new state from a given trie.
@ -626,11 +635,13 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
s.AccountLoaded++ s.AccountLoaded++
start := time.Now()
acct, err := s.reader.Account(addr) acct, err := s.reader.Account(addr)
if err != nil { if err != nil {
s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err)) s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err))
return nil return nil
} }
s.AccountReads += time.Since(start)
// Short circuit if the account is not found // Short circuit if the account is not found
if acct == nil { if acct == nil {