mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-04-10 03:47:30 +00:00
The BAL reader tracker captures access list reads at the reader level. When statedb has an account cached the BAL tracker is not informed of the access. This is ok during the lifetime of a transaction because you only need to record the access the first time. It is also ok during the lifetime of a block because BAL reads are block-level (same as statedb caches). Where I think the issue can rise is in the miner. Namely when building a block, if the miner picks up a tx which fails, it drops it and picks up another tx to include. There might be some edge case here where the failed tx which is not included poisons the cache and a future block which is included omits an account because it wasn't aware of the access.
364 lines
12 KiB
Go
364 lines
12 KiB
Go
// 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 (
|
|
"sync"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
|
|
"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 bal.StorageKeys, 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.AccessListReader
|
|
TxIndex int
|
|
}
|
|
|
|
func NewReaderWithBlockLevelAccessList(base Reader, accessList bal.BlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList {
|
|
return &ReaderWithBlockLevelAccessList{
|
|
Reader: base,
|
|
AccessList: bal.NewAccessListReader(accessList),
|
|
TxIndex: txIndex,
|
|
}
|
|
}
|
|
|
|
// Account implements Reader, returning the account with the specific address.
|
|
func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (acct *types.StateAccount, err error) {
|
|
acct, err = r.Reader.Account(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mut := r.AccessList.AccountMutations(addr, r.TxIndex)
|
|
if mut == nil {
|
|
return
|
|
}
|
|
|
|
if acct == nil {
|
|
acct = types.NewEmptyStateAccount()
|
|
} else {
|
|
// the account returned by the underlying reader is a reference
|
|
// copy it to avoid mutating the reader's instance
|
|
acct = acct.Copy()
|
|
}
|
|
|
|
if mut.Balance != nil {
|
|
acct.Balance = mut.Balance
|
|
}
|
|
if mut.Code != nil {
|
|
codeHash := crypto.Keccak256Hash(mut.Code)
|
|
acct.CodeHash = codeHash[:]
|
|
}
|
|
if mut.Nonce != nil {
|
|
acct.Nonce = *mut.Nonce
|
|
}
|
|
return
|
|
}
|
|
|
|
// 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) {
|
|
val := r.AccessList.Storage(addr, slot, r.TxIndex)
|
|
if val != nil {
|
|
return *val, nil
|
|
}
|
|
return r.Reader.Storage(addr, slot)
|
|
}
|
|
|
|
// 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 {
|
|
mut := r.AccessList.AccountMutations(addr, r.TxIndex)
|
|
if mut != nil && mut.Code != nil {
|
|
return crypto.Keccak256Hash(mut.Code) == codeHash
|
|
}
|
|
return r.Reader.Has(addr, codeHash)
|
|
}
|
|
|
|
// Code implements Reader, returning the contract code with specified address
|
|
// and hash.
|
|
func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) []byte {
|
|
mut := r.AccessList.AccountMutations(addr, r.TxIndex)
|
|
if mut != nil && mut.Code != nil && crypto.Keccak256Hash(mut.Code) == codeHash {
|
|
// TODO: need to copy here?
|
|
return mut.Code
|
|
}
|
|
return r.Reader.Code(addr, codeHash)
|
|
}
|
|
|
|
// 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) {
|
|
mut := r.AccessList.AccountMutations(addr, r.TxIndex)
|
|
if mut != nil && mut.Code != nil && crypto.Keccak256Hash(mut.Code) == codeHash {
|
|
return len(mut.Code), nil
|
|
}
|
|
return r.Reader.CodeSize(addr, codeHash)
|
|
}
|
|
|
|
// StateReaderTracker defines the capability to retrieve the access footprint
|
|
// recorded during state reading operations.
|
|
type StateReaderTracker interface {
|
|
GetStateAccessList() bal.StateAccesses
|
|
Clear()
|
|
|
|
// TouchAccount records an account access without performing a database read.
|
|
// This is used to ensure cache hits in the StateDB are still tracked for BAL.
|
|
TouchAccount(addr common.Address)
|
|
|
|
// TouchStorage records a storage slot access without performing a database read.
|
|
// This is used to ensure cache hits in the StateDB are still tracked for BAL.
|
|
TouchStorage(addr common.Address, slot common.Hash)
|
|
}
|
|
|
|
func NewReaderWithTracker(r Reader) Reader {
|
|
return newReaderTracker(r)
|
|
}
|
|
|
|
type readerTracker struct {
|
|
Reader
|
|
access bal.StateAccesses
|
|
}
|
|
|
|
func newReaderTracker(reader Reader) *readerTracker {
|
|
return &readerTracker{
|
|
Reader: reader,
|
|
access: make(bal.StateAccesses),
|
|
}
|
|
}
|
|
|
|
// Account implements StateReader, tracking the accessed address locally.
|
|
func (r *readerTracker) Account(addr common.Address) (*types.StateAccount, error) {
|
|
_, exists := r.access[addr]
|
|
if !exists {
|
|
r.access[addr] = make(bal.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) {
|
|
list, exists := r.access[addr]
|
|
if !exists {
|
|
list = make(bal.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() bal.StateAccesses {
|
|
return r.access
|
|
}
|
|
|
|
func (r *readerTracker) Clear() {
|
|
r.access = make(bal.StateAccesses)
|
|
}
|
|
|
|
// TouchAccount records an account access without performing a database read.
|
|
func (r *readerTracker) TouchAccount(addr common.Address) {
|
|
if _, exists := r.access[addr]; !exists {
|
|
r.access[addr] = make(bal.StorageAccessList)
|
|
}
|
|
}
|
|
|
|
// TouchStorage records a storage slot access without performing a database read.
|
|
func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) {
|
|
list, exists := r.access[addr]
|
|
if !exists {
|
|
list = make(bal.StorageAccessList)
|
|
r.access[addr] = list
|
|
}
|
|
list[slot] = struct{}{}
|
|
}
|