diff --git a/core/blockchain.go b/core/blockchain.go index d41f301243..9a6a9b6fbf 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2095,7 +2095,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, // // Note: the main processor and prefetcher share the same reader with a local // cache for mitigating the overhead of state access. - prefetch, process, err := bc.statedb.ReadersWithCacheStats(parentRoot) + prefetch, process, err := bc.statedb.ReadersWithCache(parentRoot) if err != nil { return nil, err } @@ -2110,8 +2110,12 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, // Upload the statistics of reader at the end defer func() { if result != nil { - result.stats.StatePrefetchCacheStats = prefetch.GetStats() - result.stats.StateReadCacheStats = process.GetStats() + if stater, ok := prefetch.(state.ReaderStater); ok { + result.stats.StatePrefetchCacheStats = stater.GetStats() + } + if stater, ok := process.(state.ReaderStater); ok { + result.stats.StateReadCacheStats = stater.GetStats() + } } }() go func(start time.Time, throwaway *state.StateDB, block *types.Block) { diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index adc66266c4..f887fc1cbf 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -94,15 +94,15 @@ func (s *ExecuteStats) reportMetrics() { chainMgaspsMeter.Update(time.Duration(s.MgasPerSecond)) // TODO(rjl493456442) generalize the ResettingTimer // Cache hit rates - accountCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.AccountCacheHit) - accountCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.AccountCacheMiss) - storageCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.StorageCacheHit) - storageCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.StorageCacheMiss) + accountCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.AccountCacheHit) + accountCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.AccountCacheMiss) + storageCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.StorageCacheHit) + storageCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.StorageCacheMiss) - accountCacheHitMeter.Mark(s.StateReadCacheStats.AccountCacheHit) - accountCacheMissMeter.Mark(s.StateReadCacheStats.AccountCacheMiss) - storageCacheHitMeter.Mark(s.StateReadCacheStats.StorageCacheHit) - storageCacheMissMeter.Mark(s.StateReadCacheStats.StorageCacheMiss) + accountCacheHitMeter.Mark(s.StateReadCacheStats.StateStats.AccountCacheHit) + accountCacheMissMeter.Mark(s.StateReadCacheStats.StateStats.AccountCacheMiss) + storageCacheHitMeter.Mark(s.StateReadCacheStats.StateStats.StorageCacheHit) + storageCacheMissMeter.Mark(s.StateReadCacheStats.StateStats.StorageCacheMiss) } // slowBlockLog represents the JSON structure for slow block logging. @@ -238,14 +238,14 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat }, Cache: slowBlockCache{ Account: slowBlockCacheEntry{ - Hits: s.StateReadCacheStats.AccountCacheHit, - Misses: s.StateReadCacheStats.AccountCacheMiss, - HitRate: calculateHitRate(s.StateReadCacheStats.AccountCacheHit, s.StateReadCacheStats.AccountCacheMiss), + Hits: s.StateReadCacheStats.StateStats.AccountCacheHit, + Misses: s.StateReadCacheStats.StateStats.AccountCacheMiss, + HitRate: calculateHitRate(s.StateReadCacheStats.StateStats.AccountCacheHit, s.StateReadCacheStats.StateStats.AccountCacheMiss), }, Storage: slowBlockCacheEntry{ - Hits: s.StateReadCacheStats.StorageCacheHit, - Misses: s.StateReadCacheStats.StorageCacheMiss, - HitRate: calculateHitRate(s.StateReadCacheStats.StorageCacheHit, s.StateReadCacheStats.StorageCacheMiss), + Hits: s.StateReadCacheStats.StateStats.StorageCacheHit, + Misses: s.StateReadCacheStats.StateStats.StorageCacheMiss, + HitRate: calculateHitRate(s.StateReadCacheStats.StateStats.StorageCacheHit, s.StateReadCacheStats.StateStats.StorageCacheMiss), }, Code: slowBlockCodeCacheEntry{ Hits: s.StateReadCacheStats.CodeStats.CacheHit, diff --git a/core/state/database.go b/core/state/database.go index 4a5547d075..62888181eb 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -221,21 +221,35 @@ func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) { return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), sr), nil } -// ReadersWithCacheStats creates a pair of state readers that share the same -// underlying state reader and internal state cache, while maintaining separate -// statistics respectively. -func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (ReaderWithStats, ReaderWithStats, error) { +// ReadersWithCache creates a pair of state readers that share the same +// underlying state reader and internal state cache, while maintaining +// separate statistics respectively. +func (db *CachingDB) ReadersWithCache(stateRoot common.Hash) (Reader, Reader, error) { r, err := db.StateReader(stateRoot) if err != nil { return nil, nil, err } sr := newStateReaderWithCache(r) - - ra := newReaderWithStats(sr, newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache)) - rb := newReaderWithStats(sr, newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache)) + ra := newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newStateReaderWithStats(sr)) + rb := newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newStateReaderWithStats(sr)) return ra, rb, nil } +// ReaderEIP7928 creates a state reader with the manner of Block-level accessList. +func (db *CachingDB) ReaderEIP7928(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int) (Reader, error) { + base, err := db.StateReader(stateRoot) + if err != nil { + return nil, err + } + // Construct the state reader with native cache and associated statistics + r := newStateReaderWithStats(newStateReaderWithCache(base)) + + // Construct the state reader with background prefetching + pr := newPrefetchStateReader(r, accessList, threads) + + return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), pr), nil +} + // OpenTrie opens the main account trie at a specific root hash. func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { if db.triedb.IsVerkle() { diff --git a/core/state/reader.go b/core/state/reader.go index 35b732173b..0835d9a0ef 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -18,7 +18,6 @@ package state import ( "errors" - "fmt" "sync" "sync/atomic" @@ -38,6 +37,8 @@ import ( ) // ContractCodeReader defines the interface for accessing contract code. +// +// ContractCodeReader is supposed to be thread-safe. type ContractCodeReader interface { // Has returns the flag indicating whether the contract code with // specified address and hash exists or not. @@ -58,35 +59,10 @@ type ContractCodeReader interface { CodeSize(addr common.Address, codeHash common.Hash) (int, error) } -// ContractCodeReaderStats aggregates statistics for the contract code reader. -type ContractCodeReaderStats struct { - CacheHit int64 // Number of cache hits - CacheMiss int64 // Number of cache misses - CacheHitBytes int64 // Total bytes served from cache - CacheMissBytes int64 // Total bytes read on cache misses -} - -// HitRate returns the cache hit rate. -func (s ContractCodeReaderStats) HitRate() float64 { - if s.CacheHit == 0 { - return 0 - } - return float64(s.CacheHit) / float64(s.CacheHit+s.CacheMiss) -} - -// ContractCodeReaderWithStats extends ContractCodeReader by adding GetStats to -// expose statistics of code reader. -type ContractCodeReaderWithStats interface { - ContractCodeReader - - GetStats() ContractCodeReaderStats -} - // StateReader defines the interface for accessing accounts and storage slots // associated with a specific state. // -// StateReader is assumed to be thread-safe and implementation must take care -// of the concurrency issue by themselves. +// StateReader is supposed to be thread-safe. type StateReader interface { // Account retrieves the account associated with a particular address. // @@ -114,40 +90,6 @@ type Reader interface { StateReader } -// ReaderStats wraps the statistics of reader. -type ReaderStats struct { - AccountCacheHit int64 - AccountCacheMiss int64 - StorageCacheHit int64 - StorageCacheMiss int64 - CodeStats ContractCodeReaderStats -} - -// String implements fmt.Stringer, returning string format statistics. -func (s ReaderStats) String() string { - var ( - accountCacheHitRate float64 - storageCacheHitRate float64 - ) - if s.AccountCacheHit > 0 { - accountCacheHitRate = float64(s.AccountCacheHit) / float64(s.AccountCacheHit+s.AccountCacheMiss) * 100 - } - if s.StorageCacheHit > 0 { - storageCacheHitRate = float64(s.StorageCacheHit) / float64(s.StorageCacheHit+s.StorageCacheMiss) * 100 - } - msg := fmt.Sprintf("Reader statistics\n") - msg += fmt.Sprintf("account: hit: %d, miss: %d, rate: %.2f\n", s.AccountCacheHit, s.AccountCacheMiss, accountCacheHitRate) - msg += fmt.Sprintf("storage: hit: %d, miss: %d, rate: %.2f\n", s.StorageCacheHit, s.StorageCacheMiss, storageCacheHitRate) - msg += fmt.Sprintf("code: hit: %d(%v), miss: %d(%v), rate: %.2f\n", s.CodeStats.CacheHit, common.StorageSize(s.CodeStats.CacheHitBytes), s.CodeStats.CacheMiss, common.StorageSize(s.CodeStats.CacheMissBytes), s.CodeStats.HitRate()) - return msg -} - -// ReaderWithStats wraps the additional method to retrieve the reader statistics from. -type ReaderWithStats interface { - Reader - GetStats() ReaderStats -} - // cachingCodeReader implements ContractCodeReader, accessing contract code either in // local key-value store or the shared code cache. // @@ -210,15 +152,16 @@ func (r *cachingCodeReader) CodeSize(addr common.Address, codeHash common.Hash) return len(code), nil } -// Has returns the flag indicating whether the contract code with -// specified address and hash exists or not. +// Has implements ContractCodeReader, returning the flag indicating whether +// the contract code with specified address and hash exists or not. func (r *cachingCodeReader) Has(addr common.Address, codeHash common.Hash) bool { code, _ := r.Code(addr, codeHash) return len(code) > 0 } -// GetStats returns the statistics of the code reader. -func (r *cachingCodeReader) GetStats() ContractCodeReaderStats { +// GetCodeStats implements ContractCodeReaderStater, returning the statistics +// of the code reader. +func (r *cachingCodeReader) GetCodeStats() ContractCodeReaderStats { return ContractCodeReaderStats{ CacheHit: r.hit.Load(), CacheMiss: r.miss.Load(), @@ -495,20 +438,6 @@ func (r *multiStateReader) Storage(addr common.Address, slot common.Hash) (commo return common.Hash{}, errors.Join(errs...) } -// reader is the wrapper of ContractCodeReader and StateReader interface. -type reader struct { - ContractCodeReader - StateReader -} - -// newReader constructs a reader with the supplied code reader and state reader. -func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader { - return &reader{ - ContractCodeReader: codeReader, - StateReader: stateReader, - } -} - // stateReaderWithCache is a wrapper around StateReader that maintains additional // state caches to support concurrent state access. type stateReaderWithCache struct { @@ -619,9 +548,10 @@ func (r *stateReaderWithCache) Storage(addr common.Address, slot common.Hash) (c return value, err } -type readerWithStats struct { +// stateReaderWithStats is a wrapper over the stateReaderWithCache, tracking +// the cache hit statistics of the reader. +type stateReaderWithStats struct { *stateReaderWithCache - ContractCodeReaderWithStats accountCacheHit atomic.Int64 accountCacheMiss atomic.Int64 @@ -629,11 +559,10 @@ type readerWithStats struct { storageCacheMiss atomic.Int64 } -// newReaderWithStats constructs the reader with additional statistics tracked. -func newReaderWithStats(sr *stateReaderWithCache, cr ContractCodeReaderWithStats) *readerWithStats { - return &readerWithStats{ - stateReaderWithCache: sr, - ContractCodeReaderWithStats: cr, +// newReaderWithStats constructs the state reader with additional statistics tracked. +func newStateReaderWithStats(sr *stateReaderWithCache) *stateReaderWithStats { + return &stateReaderWithStats{ + stateReaderWithCache: sr, } } @@ -641,7 +570,7 @@ func newReaderWithStats(sr *stateReaderWithCache, cr ContractCodeReaderWithStats // The returned account might be nil if it's not existent. // // An error will be returned if the state is corrupted in the underlying reader. -func (r *readerWithStats) Account(addr common.Address) (*types.StateAccount, error) { +func (r *stateReaderWithStats) Account(addr common.Address) (*types.StateAccount, error) { account, incache, err := r.stateReaderWithCache.account(addr) if err != nil { return nil, err @@ -659,7 +588,7 @@ func (r *readerWithStats) Account(addr common.Address) (*types.StateAccount, err // existent. // // An error will be returned if the state is corrupted in the underlying reader. -func (r *readerWithStats) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { +func (r *stateReaderWithStats) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { value, incache, err := r.stateReaderWithCache.storage(addr, slot) if err != nil { return common.Hash{}, err @@ -672,13 +601,51 @@ func (r *readerWithStats) Storage(addr common.Address, slot common.Hash) (common return value, nil } -// GetStats implements ReaderWithStats, returning the statistics of state reader. -func (r *readerWithStats) GetStats() ReaderStats { - return ReaderStats{ +// GetStateStats implements StateReaderStater, returning the statistics of the +// state reader. +func (r *stateReaderWithStats) GetStateStats() StateReaderStats { + return StateReaderStats{ AccountCacheHit: r.accountCacheHit.Load(), AccountCacheMiss: r.accountCacheMiss.Load(), StorageCacheHit: r.storageCacheHit.Load(), StorageCacheMiss: r.storageCacheMiss.Load(), - CodeStats: r.ContractCodeReaderWithStats.GetStats(), + } +} + +// reader aggregates a code reader and a state reader into a single object. +type reader struct { + ContractCodeReader + StateReader +} + +// newReader constructs a reader with the supplied code reader and state reader. +func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader { + return &reader{ + ContractCodeReader: codeReader, + StateReader: stateReader, + } +} + +// GetCodeStats returns the statistics of code access. +func (r *reader) GetCodeStats() ContractCodeReaderStats { + if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok { + return stater.GetCodeStats() + } + return ContractCodeReaderStats{} +} + +// GetStateStats returns the statistics of state access. +func (r *reader) GetStateStats() StateReaderStats { + if stater, ok := r.StateReader.(StateReaderStater); ok { + return stater.GetStateStats() + } + return StateReaderStats{} +} + +// GetStats returns the aggregated statistics for both state and code access. +func (r *reader) GetStats() ReaderStats { + return ReaderStats{ + CodeStats: r.GetCodeStats(), + StateStats: r.GetStateStats(), } } diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go new file mode 100644 index 0000000000..039342801f --- /dev/null +++ b/core/state/reader_eip_7928.go @@ -0,0 +1,312 @@ +// 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 + +// The EIP27928 reader utilizes a hierarchical architecture to optimize state +// access during block execution: +// +// - Base layer: The reader is initialized with the pre-transition state root, +// providing the access of the state. +// +// - Prefetching Layer: This base reader is wrapped by newPrefetchStateReader. +// Using an Access List hint, it asynchronously fetches required state data +// in the background, minimizing I/O blocking during transaction processing. +// +// - Execution Layer: To support parallel transaction execution within the EIP +// 7928 context, readers are wrapped in ReaderWithBlockLevelAccessList. +// This layer provides a "unified view" by merging the pre-transition state +// with mutated states from preceding transactions in the block. +// +// - Tracking Layer: Finally, the readerTracker wraps the execution reader to +// capture all state accesses made during a specific transaction. These individual +// access are subsequently merged to construct a comprehensive access list +// for the entire block. +// +// The architecture can be illustrated by the diagram below: + +// [ Block Level Access List ] <────────────────┐ +// ▲ │ (Merge) +// │ │ +// ┌───────┴───────┐ ┌───────┴───────┐ +// │ readerTracker │ │ readerTracker │ (Access Tracking) +// └───────┬───────┘ └───────┬───────┘ +// │ │ +// ┌──────────────┴──────────────┐ ┌──────────────┴──────────────┐ +// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │ (Unified View) +// │ (Pre-state + Mutations) │ │ (Pre-state + Mutations) │ +// └──────────────┬──────────────┘ └──────────────┬──────────────┘ +// │ │ +// └────────────────┬─────────────────┘ +// │ +// ┌──────────────┴──────────────┐ +// │ newPrefetchStateReader │ (Async I/O) +// │ (Access List Hint driven) │ +// └──────────────┬──────────────┘ +// │ +// ┌──────────────┴──────────────┐ +// │ Base Reader │ (State Root) +// │ (State & Contract Code) │ +// └─────────────────────────────┘ + +import ( + "maps" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" +) + +type fetchTask struct { + addr common.Address + slots []common.Hash +} + +func (t *fetchTask) weight() int { return 1 + len(t.slots) } + +type prefetchStateReader struct { + StateReader + tasks []*fetchTask + nThreads int + done chan struct{} + term chan struct{} + closeOnce sync.Once +} + +func newPrefetchStateReader(reader StateReader, accessList map[common.Address][]common.Hash, nThreads int) *prefetchStateReader { + tasks := make([]*fetchTask, 0, len(accessList)) + for addr, slots := range accessList { + tasks = append(tasks, &fetchTask{ + addr: addr, + slots: slots, + }) + } + return newPrefetchStateReaderInternal(reader, tasks, nThreads) +} + +func newPrefetchStateReaderInternal(reader StateReader, tasks []*fetchTask, nThreads int) *prefetchStateReader { + r := &prefetchStateReader{ + StateReader: reader, + tasks: tasks, + nThreads: nThreads, + done: make(chan struct{}), + term: make(chan struct{}), + } + go r.prefetch() + return r +} + +func (r *prefetchStateReader) Close() { + r.closeOnce.Do(func() { + close(r.term) + <-r.done + }) +} + +func (r *prefetchStateReader) Wait() error { + select { + case <-r.term: + return nil + case <-r.done: + return nil + } +} + +func (r *prefetchStateReader) prefetch() { + defer close(r.done) + + if len(r.tasks) == 0 { + return + } + var total int + for _, t := range r.tasks { + total += t.weight() + } + var ( + wg sync.WaitGroup + unit = (total + r.nThreads - 1) / r.nThreads // round-up the per worker unit + ) + for i := 0; i < r.nThreads; i++ { + start := i * unit + if start >= total { + break + } + limit := (i + 1) * unit + if i == r.nThreads-1 { + limit = total + } + // Schedule the worker for prefetching, the items on the range [start, limit) + // is exclusively assigned for this worker. + wg.Add(1) + go func(workerID, startW, endW int) { + r.process(startW, endW) + wg.Done() + }(i, start, limit) + } + wg.Wait() +} + +func (r *prefetchStateReader) process(start, limit int) { + var total = 0 + for _, t := range r.tasks { + tw := t.weight() + if total+tw > start { + s := 0 + if start > total { + s = start - total + } + l := tw + if limit < total+tw { + l = limit - total + } + for j := s; j < l; j++ { + select { + case <-r.term: + return + default: + if j == 0 { + r.StateReader.Account(t.addr) + } else { + r.StateReader.Storage(t.addr, t.slots[j-1]) + } + } + } + } + total += tw + if total >= limit { + return + } + } +} + +// ReaderWithBlockLevelAccessList provides state access that reflects the +// pre-transition state combined with the mutations made by transactions +// prior to TxIndex. +type ReaderWithBlockLevelAccessList struct { + Reader + AccessList *bal.ConstructionBlockAccessList + TxIndex int +} + +func NewReaderWithBlockLevelAccessList(base Reader, accessList *bal.ConstructionBlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList { + return &ReaderWithBlockLevelAccessList{ + Reader: base, + AccessList: accessList, + TxIndex: txIndex, + } +} + +// Account implements Reader, returning the account with the specific address. +func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (*types.StateAccount, error) { + panic("implement me") +} + +// Storage implements Reader, returning the storage slot with the specific +// address and slot key. +func (r *ReaderWithBlockLevelAccessList) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { + panic("implement me") +} + +// Has implements Reader, returning the flag indicating whether the contract +// code with specified address and hash exists or not. +func (r *ReaderWithBlockLevelAccessList) Has(addr common.Address, codeHash common.Hash) bool { + panic("implement me") +} + +// Code implements Reader, returning the contract code with specified address +// and hash. +func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) ([]byte, error) { + panic("implement me") +} + +// CodeSize implements Reader, returning the contract code size with specified +// address and hash. +func (r *ReaderWithBlockLevelAccessList) CodeSize(addr common.Address, codeHash common.Hash) (int, error) { + panic("implement me") +} + +// StorageAccessList represents a set of storage slots accessed within an account. +type StorageAccessList map[common.Hash]struct{} + +// StateAccessList maps account addresses to their respective accessed storage slots. +type StateAccessList map[common.Address]StorageAccessList + +// Merge merges the entries from the other StateAccessList into the receiver. +func (s StateAccessList) Merge(other StateAccessList) { + for addr, otherSlots := range other { + slots, exists := s[addr] + if !exists { + s[addr] = otherSlots + continue + } + maps.Copy(slots, otherSlots) + } +} + +// StateReaderTracker defines the capability to retrieve the access footprint +// recorded during state reading operations. +type StateReaderTracker interface { + GetStateAccessList() StateAccessList +} + +type readerTracker struct { + Reader + access StateAccessList + lock sync.RWMutex +} + +func newReaderTracker(reader Reader) *readerTracker { + return &readerTracker{ + Reader: reader, + access: make(StateAccessList), + } +} + +// Account implements StateReader, tracking the accessed address locally. +func (r *readerTracker) Account(addr common.Address) (*types.StateAccount, error) { + r.lock.Lock() + defer r.lock.Unlock() + + _, exists := r.access[addr] + if !exists { + r.access[addr] = make(StorageAccessList) + } + return r.Reader.Account(addr) +} + +// Storage implements StateReader, tracking the accessed slot identifier locally. +func (r *readerTracker) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { + r.lock.Lock() + defer r.lock.Unlock() + + list, exists := r.access[addr] + if !exists { + list = make(StorageAccessList) + r.access[addr] = list + } + list[slot] = struct{}{} + + return r.Reader.Storage(addr, slot) +} + +// GetStateAccessList implements StateReaderTracker, returning the access footprint. +func (r *readerTracker) GetStateAccessList() StateAccessList { + r.lock.RLock() + defer r.lock.RUnlock() + + return r.access +} diff --git a/core/state/reader_eip_7928_test.go b/core/state/reader_eip_7928_test.go new file mode 100644 index 0000000000..d9d20f6d7b --- /dev/null +++ b/core/state/reader_eip_7928_test.go @@ -0,0 +1,201 @@ +// 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 ( + "fmt" + "maps" + "math/rand" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/internal/testrand" +) + +type countingStateReader struct { + accounts map[common.Address]int + storages map[common.Address]map[common.Hash]int + lock sync.Mutex +} + +func newRefStateReader() *countingStateReader { + return &countingStateReader{ + accounts: make(map[common.Address]int), + storages: make(map[common.Address]map[common.Hash]int), + } +} + +func (r *countingStateReader) validate(total int) error { + var sum int + for addr, n := range r.accounts { + if n != 1 { + return fmt.Errorf("duplicated account access: %x-%d", addr, n) + } + sum += 1 + + slots, exists := r.storages[addr] + if !exists { + continue + } + for key, n := range slots { + if n != 1 { + return fmt.Errorf("duplicated storage access: %x-%x-%d", addr, key, n) + } + sum += 1 + } + } + for addr := range r.storages { + _, exists := r.accounts[addr] + if !exists { + return fmt.Errorf("dangling storage access: %x", addr) + } + } + if sum != total { + return fmt.Errorf("unexpected number of access, want: %d, got: %d", total, sum) + } + return nil +} + +func (r *countingStateReader) Account(addr common.Address) (*types.StateAccount, error) { + r.lock.Lock() + defer r.lock.Unlock() + + r.accounts[addr] += 1 + return nil, nil +} +func (r *countingStateReader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { + r.lock.Lock() + defer r.lock.Unlock() + + slots, exists := r.storages[addr] + if !exists { + slots = make(map[common.Hash]int) + r.storages[addr] = slots + } + slots[slot] += 1 + return common.Hash{}, nil +} + +func makeFetchTasks(n int) ([]*fetchTask, int) { + var ( + total int + tasks []*fetchTask + ) + for i := 0; i < n; i++ { + var slots []common.Hash + if rand.Intn(3) != 0 { + for j := 0; j < rand.Intn(100); j++ { + slots = append(slots, testrand.Hash()) + } + } + tasks = append(tasks, &fetchTask{ + addr: testrand.Address(), + slots: slots, + }) + total += len(slots) + 1 + } + return tasks, total +} + +func TestPrefetchReader(t *testing.T) { + type suite struct { + tasks []*fetchTask + threads int + total int + } + var suites []suite + for i := 0; i < 100; i++ { + tasks, total := makeFetchTasks(100) + suites = append(suites, suite{ + tasks: tasks, + threads: rand.Intn(30) + 1, + total: total, + }) + } + // num(tasks) < num(threads) + tasks, total := makeFetchTasks(1) + suites = append(suites, suite{ + tasks: tasks, + threads: 100, + total: total, + }) + for _, s := range suites { + r := newRefStateReader() + pr := newPrefetchStateReaderInternal(r, s.tasks, s.threads) + pr.Wait() + if err := r.validate(s.total); err != nil { + t.Fatal(err) + } + } +} + +func makeFakeSlots(n int) map[common.Hash]struct{} { + slots := make(map[common.Hash]struct{}) + for i := 0; i < n; i++ { + slots[testrand.Hash()] = struct{}{} + } + return slots +} + +type noopStateReader struct{} + +func (r *noopStateReader) Account(addr common.Address) (*types.StateAccount, error) { return nil, nil } +func (r *noopStateReader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { + return common.Hash{}, nil +} + +type noopCodeReader struct{} + +func (r *noopCodeReader) Has(addr common.Address, codeHash common.Hash) bool { return false } + +func (r *noopCodeReader) Code(addr common.Address, codeHash common.Hash) ([]byte, error) { + return nil, nil +} + +func (r *noopCodeReader) CodeSize(addr common.Address, codeHash common.Hash) (int, error) { + return 0, nil +} + +func TestReaderWithTracker(t *testing.T) { + var r Reader = newReaderTracker(newReader(&noopCodeReader{}, &noopStateReader{})) + + accesses := map[common.Address]map[common.Hash]struct{}{ + testrand.Address(): makeFakeSlots(10), + testrand.Address(): makeFakeSlots(0), + } + for addr, slots := range accesses { + r.Account(addr) + for slot := range slots { + r.Storage(addr, slot) + } + } + got := r.(StateReaderTracker).GetStateAccessList() + if len(got) != len(accesses) { + t.Fatalf("Unexpected access list, want: %d, got: %d", len(accesses), len(got)) + } + for addr, slots := range got { + entry, ok := accesses[addr] + if !ok { + t.Fatal("Unexpected access list") + } + if !maps.Equal(slots, entry) { + t.Fatal("Unexpected slots") + } + } +} diff --git a/core/state/reader_stater.go b/core/state/reader_stater.go new file mode 100644 index 0000000000..5294275953 --- /dev/null +++ b/core/state/reader_stater.go @@ -0,0 +1,82 @@ +// 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 + +// ContractCodeReaderStats aggregates statistics for the contract code reader. +type ContractCodeReaderStats struct { + CacheHit int64 // Number of cache hits + CacheMiss int64 // Number of cache misses + CacheHitBytes int64 // Total bytes served from cache + CacheMissBytes int64 // Total bytes read on cache misses +} + +// HitRate returns the cache hit rate in percentage. +func (s ContractCodeReaderStats) HitRate() float64 { + total := s.CacheHit + s.CacheMiss + if total == 0 { + return 0 + } + return float64(s.CacheHit) / float64(total) * 100 +} + +// ContractCodeReaderStater wraps the method to retrieve the statistics of +// contract code reader. +type ContractCodeReaderStater interface { + GetCodeStats() ContractCodeReaderStats +} + +// StateReaderStats aggregates statistics for the state reader. +type StateReaderStats struct { + AccountCacheHit int64 // Number of account cache hits + AccountCacheMiss int64 // Number of account cache misses + StorageCacheHit int64 // Number of storage cache hits + StorageCacheMiss int64 // Number of storage cache misses +} + +// AccountCacheHitRate returns the cache hit rate of account requests in percentage. +func (s StateReaderStats) AccountCacheHitRate() float64 { + total := s.AccountCacheHit + s.AccountCacheMiss + if total == 0 { + return 0 + } + return float64(s.AccountCacheHit) / float64(total) * 100 +} + +// StorageCacheHitRate returns the cache hit rate of storage requests in percentage. +func (s StateReaderStats) StorageCacheHitRate() float64 { + total := s.StorageCacheHit + s.StorageCacheMiss + if total == 0 { + return 0 + } + return float64(s.StorageCacheHit) / float64(total) * 100 +} + +// StateReaderStater wraps the method to retrieve the statistics of state reader. +type StateReaderStater interface { + GetStateStats() StateReaderStats +} + +// ReaderStats wraps the statistics of reader. +type ReaderStats struct { + CodeStats ContractCodeReaderStats + StateStats StateReaderStats +} + +// ReaderStater defines the capability to retrieve aggregated statistics. +type ReaderStater interface { + GetStats() ReaderStats +}