From 4183a70bd6b8e33c805969377bb3ef8092d320ec Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 8 May 2026 11:24:47 +0200 Subject: [PATCH] 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. --- core/blockchain.go | 28 ++++---- core/state/bal_state_transition_test.go | 91 +++++++++++++++++++++++++ core/state/reader.go | 33 --------- core/state/reader_eip_7928.go | 4 -- core/state/reader_stater.go | 14 ---- core/state/state_object.go | 14 ++++ core/state/statedb.go | 31 ++++++--- 7 files changed, 138 insertions(+), 77 deletions(-) create mode 100644 core/state/bal_state_transition_test.go diff --git a/core/blockchain.go b/core/blockchain.go index 7b6b36334d..85fa097166 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -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. diff --git a/core/state/bal_state_transition_test.go b/core/state/bal_state_transition_test.go new file mode 100644 index 0000000000..3f628c2c91 --- /dev/null +++ b/core/state/bal_state_transition_test.go @@ -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 . + +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") + } +} diff --git a/core/state/reader.go b/core/state/reader.go index 58af034057..c892803967 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -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() - } -} diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index b651d20c3f..ab9dcabcb8 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -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() } diff --git a/core/state/reader_stater.go b/core/state/reader_stater.go index 1b5e7aa469..fdfe49b3c8 100644 --- a/core/state/reader_stater.go +++ b/core/state/reader_stater.go @@ -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) diff --git a/core/state/state_object.go b/core/state/state_object.go index a4a7ec6679..b83d66d3d6 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -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) diff --git a/core/state/statedb.go b/core/state/statedb.go index 68da74d148..b8081c149a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -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 {