This commit is contained in:
rjl493456442 2026-02-24 21:54:46 -08:00 committed by GitHub
commit 5aa2549f82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 696 additions and 116 deletions

View file

@ -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) {

View file

@ -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,

View file

@ -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() {

View file

@ -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(),
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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")
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}