core/state: remove trie prefetcher and witness from stateDB

This commit is contained in:
Gary Rong 2026-03-20 14:21:21 +08:00 committed by CPerezz
parent e2c00d6c96
commit 9daaef1923
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
3 changed files with 18 additions and 208 deletions

View file

@ -119,11 +119,6 @@ func (s *stateObject) addrHash() common.Hash {
return *s.addressHash return *s.addressHash
} }
// storageTrieID returns the unique identifier for this account's storage trie.
func (s *stateObject) storageTrieID() trie.ID {
return *trie.StorageTrieID(s.db.originalRoot, s.addrHash(), s.data.Root)
}
func (s *stateObject) markSelfdestructed() { func (s *stateObject) markSelfdestructed() {
s.selfDestructed = true s.selfDestructed = true
} }
@ -149,23 +144,6 @@ func (s *stateObject) getTrie() (Trie, error) {
return s.trie, nil return s.trie, nil
} }
// getPrefetchedTrie returns the associated trie, as populated by the prefetcher
// if it's available.
//
// Note, opposed to getTrie, this method will *NOT* blindly cache the resulting
// trie in the state object. The caller might want to do that, but it's cleaner
// to break the hidden interdependency between retrieving tries from the db or
// from the prefetcher.
func (s *stateObject) getPrefetchedTrie() Trie {
// If there's nothing to meaningfully return, let the user figure it out by
// pulling the trie from disk.
if (s.data.Root == types.EmptyRootHash && !s.db.db.TrieDB().IsVerkle()) || s.db.prefetcher == nil {
return nil
}
// Attempt to retrieve the trie from the prefetcher
return s.db.prefetcher.trie(s.storageTrieID())
}
// GetState retrieves a value associated with the given storage key. // GetState retrieves a value associated with the given storage key.
func (s *stateObject) GetState(key common.Hash) common.Hash { func (s *stateObject) GetState(key common.Hash) common.Hash {
value, _ := s.getState(key) value, _ := s.getState(key)
@ -226,12 +204,6 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
} }
s.db.StorageReads += time.Since(start) 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 {
if err = s.db.prefetcher.prefetchStorage(s.storageTrieID(), s.address, []common.Hash{key}, true); err != nil {
log.Error("Failed to prefetch storage slot", "addr", s.address, "key", key, "err", err)
}
}
s.originStorage[key] = value s.originStorage[key] = value
return value return value
} }
@ -265,7 +237,6 @@ func (s *stateObject) setState(key common.Hash, value common.Hash, origin common
// finalise moves all dirty storage slots into the pending area to be hashed or // finalise moves all dirty storage slots into the pending area to be hashed or
// committed later. It is invoked at the end of every transaction. // committed later. It is invoked at the end of every transaction.
func (s *stateObject) finalise() { func (s *stateObject) finalise() {
slotsToPrefetch := make([]common.Hash, 0, len(s.dirtyStorage))
for key, value := range s.dirtyStorage { for key, value := range s.dirtyStorage {
if origin, exist := s.uncommittedStorage[key]; exist && origin == value { if origin, exist := s.uncommittedStorage[key]; exist && origin == value {
// The slot is reverted to its original value, delete the entry // The slot is reverted to its original value, delete the entry
@ -278,7 +249,6 @@ func (s *stateObject) finalise() {
// The slot is different from its original value and hasn't been // The slot is different from its original value and hasn't been
// tracked for commit yet. // tracked for commit yet.
s.uncommittedStorage[key] = s.GetCommittedState(key) s.uncommittedStorage[key] = s.GetCommittedState(key)
slotsToPrefetch = append(slotsToPrefetch, key) // Copy needed for closure
} }
// Aggregate the dirty storage slots into the pending area. It might // Aggregate the dirty storage slots into the pending area. It might
// be possible that the value of tracked slot here is same with the // be possible that the value of tracked slot here is same with the
@ -288,11 +258,6 @@ func (s *stateObject) finalise() {
// byzantium fork) and entry is necessary to modify the value back. // byzantium fork) and entry is necessary to modify the value back.
s.pendingStorage[key] = value s.pendingStorage[key] = value
} }
if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
if err := s.db.prefetcher.prefetchStorage(s.storageTrieID(), s.address, slotsToPrefetch, false); err != nil {
log.Error("Failed to prefetch slots", "addr", s.address, "slots", len(slotsToPrefetch), "err", err)
}
}
if len(s.dirtyStorage) > 0 { if len(s.dirtyStorage) > 0 {
s.dirtyStorage = make(Storage) s.dirtyStorage = make(Storage)
} }
@ -311,34 +276,16 @@ func (s *stateObject) finalise() {
// //
// It assumes all the dirty storage slots have been finalized before. // It assumes all the dirty storage slots have been finalized before.
func (s *stateObject) updateTrie() (Trie, error) { func (s *stateObject) updateTrie() (Trie, error) {
// Short circuit if nothing was accessed, don't trigger a prefetcher warning // Short circuit if nothing was accessed
if len(s.uncommittedStorage) == 0 {
// Nothing was written, so we could stop early. Unless we have both reads
// and witness collection enabled, in which case we need to fetch the trie.
if s.db.witness == nil || len(s.originStorage) == 0 {
return s.trie, nil
}
}
// Retrieve a pretecher populated trie, or fall back to the database. This will
// block until all prefetch tasks are done, which are needed for witnesses even
// for unmodified state objects.
tr := s.getPrefetchedTrie()
if tr != nil {
// Prefetcher returned a live trie, swap it out for the current one
s.trie = tr
} else {
// Fetcher not running or empty trie, fallback to the database trie
var err error
tr, err = s.getTrie()
if err != nil {
s.db.setError(err)
return nil, err
}
}
// Short circuit if nothing changed, don't bother with hashing anything
if len(s.uncommittedStorage) == 0 { if len(s.uncommittedStorage) == 0 {
return s.trie, nil return s.trie, nil
} }
// Fetcher not running or empty trie, fallback to the database trie
tr, err := s.getTrie()
if err != nil {
s.db.setError(err)
return nil, err
}
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes // Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
// in circumstances similar to the following: // in circumstances similar to the following:
// //
@ -351,7 +298,6 @@ func (s *stateObject) updateTrie() (Trie, error) {
// Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved. // Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved.
var ( var (
deletions []common.Hash deletions []common.Hash
used = make([]common.Hash, 0, len(s.uncommittedStorage))
) )
for key, origin := range s.uncommittedStorage { for key, origin := range s.uncommittedStorage {
// Skip noop changes, persist actual changes // Skip noop changes, persist actual changes
@ -373,8 +319,6 @@ func (s *stateObject) updateTrie() (Trie, error) {
} else { } else {
deletions = append(deletions, key) deletions = append(deletions, key)
} }
// Cache the items for preloading
used = append(used, key) // Copy needed for closure
} }
for _, key := range deletions { for _, key := range deletions {
if err := tr.DeleteStorage(s.address, key[:]); err != nil { if err := tr.DeleteStorage(s.address, key[:]); err != nil {
@ -383,9 +327,6 @@ func (s *stateObject) updateTrie() (Trie, error) {
} }
s.db.StorageDeleted.Add(1) s.db.StorageDeleted.Add(1)
} }
if s.db.prefetcher != nil {
s.db.prefetcher.used(s.storageTrieID(), nil, used)
}
s.uncommittedStorage = make(Storage) // empties the commit markers s.uncommittedStorage = make(Storage) // empties the commit markers
return tr, nil return tr, nil
} }

View file

@ -32,7 +32,6 @@ import (
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
@ -75,10 +74,9 @@ func (m *mutation) isDelete() bool {
// must be created with new root and updated database for accessing post- // must be created with new root and updated database for accessing post-
// commit states. // commit states.
type StateDB struct { type StateDB struct {
db Database db Database
prefetcher *triePrefetcher reader Reader
reader Reader trie Trie // it's resolved on first access
trie Trie // it's resolved on first access
// originalRoot is the pre-state root, before any changes were made. // originalRoot is the pre-state root, before any changes were made.
// It will be updated when the Commit is called. // It will be updated when the Commit is called.
@ -133,9 +131,6 @@ type StateDB struct {
// Snapshot and RevertToSnapshot. // Snapshot and RevertToSnapshot.
journal *journal journal *journal
// State witness if cross validation is needed
witness *stateless.Witness
// Measurements gathered during execution for debugging purposes // Measurements gathered during execution for debugging purposes
AccountReads time.Duration AccountReads time.Duration
AccountHashes time.Duration AccountHashes time.Duration
@ -200,47 +195,11 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro
// state trie concurrently while the state is mutated so that when we reach the // state trie concurrently while the state is mutated so that when we reach the
// commit phase, most of the needed data is already hot. // commit phase, most of the needed data is already hot.
func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) { func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) {
// Terminate any previously running prefetcher
s.StopPrefetcher()
// Enable witness collection if requested
s.witness = witness
// With the switch to the Proof-of-Stake consensus algorithm, block production
// rewards are now handled at the consensus layer. Consequently, a block may
// have no state transitions if it contains no transactions and no withdrawals.
// In such cases, the account trie won't be scheduled for prefetching, leading
// to unnecessary error logs.
//
// To prevent this, the account trie is always scheduled for prefetching once
// the prefetcher is constructed. For more details, see:
// https://github.com/ethereum/go-ethereum/issues/29880
opener := func(id trie.ID, addr common.Address) (Trie, error) {
if s.db.TrieDB().IsVerkle() {
return s.db.OpenTrie(id.StateRoot)
}
if id.Owner != (common.Hash{}) {
return s.db.OpenStorageTrie(id.StateRoot, addr, id.Root, nil)
}
return s.db.OpenTrie(id.StateRoot)
}
s.prefetcher = newTriePrefetcher(opener, s.originalRoot, namespace, witness == nil)
id := trie.StateTrieID(s.originalRoot)
if err := s.prefetcher.prefetchAccounts(*id, nil, false); err != nil {
log.Error("Failed to prefetch account trie", "root", s.originalRoot, "err", err)
}
} }
// StopPrefetcher terminates a running prefetcher and reports any leftover stats // StopPrefetcher terminates a running prefetcher and reports any leftover stats
// from the gathered metrics. // from the gathered metrics.
func (s *StateDB) StopPrefetcher() { func (s *StateDB) StopPrefetcher() {}
if s.prefetcher != nil {
s.prefetcher.terminate(false)
s.prefetcher.report()
s.prefetcher = nil
}
}
// setError remembers the first non-nil error it is called with. // setError remembers the first non-nil error it is called with.
func (s *StateDB) setError(err error) { func (s *StateDB) setError(err error) {
@ -368,9 +327,6 @@ func (s *StateDB) TxIndex() int {
func (s *StateDB) GetCode(addr common.Address) []byte { func (s *StateDB) GetCode(addr common.Address) []byte {
stateObject := s.getStateObject(addr) stateObject := s.getStateObject(addr)
if stateObject != nil { if stateObject != nil {
if s.witness != nil {
s.witness.AddCode(stateObject.Code())
}
return stateObject.Code() return stateObject.Code()
} }
return nil return nil
@ -379,9 +335,6 @@ func (s *StateDB) GetCode(addr common.Address) []byte {
func (s *StateDB) GetCodeSize(addr common.Address) int { func (s *StateDB) GetCodeSize(addr common.Address) int {
stateObject := s.getStateObject(addr) stateObject := s.getStateObject(addr)
if stateObject != nil { if stateObject != nil {
if s.witness != nil {
s.witness.AddCode(stateObject.Code())
}
return stateObject.CodeSize() return stateObject.CodeSize()
} }
return 0 return 0
@ -612,13 +565,6 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
if acct == nil { if acct == nil {
return nil return nil
} }
// Schedule the resolved account for prefetching if it's enabled.
if s.prefetcher != nil {
id := trie.StateTrieID(s.originalRoot)
if err = s.prefetcher.prefetchAccounts(*id, []common.Address{addr}, true); err != nil {
log.Error("Failed to prefetch account", "addr", addr, "err", err)
}
}
// Insert into the live set // Insert into the live set
obj := newObject(s, addr, acct) obj := newObject(s, addr, acct)
s.setStateObject(obj) s.setStateObject(obj)
@ -710,9 +656,6 @@ func (s *StateDB) Copy() *StateDB {
if s.trie != nil { if s.trie != nil {
state.trie = mustCopyTrie(s.trie) state.trie = mustCopyTrie(s.trie)
} }
if s.witness != nil {
state.witness = s.witness.Copy()
}
if s.accessEvents != nil { if s.accessEvents != nil {
state.accessEvents = s.accessEvents.Copy() state.accessEvents = s.accessEvents.Copy()
} }
@ -797,7 +740,6 @@ func (s *StateDB) LogsForBurnAccounts() []*types.Log {
// the journal as well as the refunds. Finalise, however, will not push any updates // the journal as well as the refunds. Finalise, however, will not push any updates
// into the tries just yet. Only IntermediateRoot or Commit will do that. // into the tries just yet. Only IntermediateRoot or Commit will do that.
func (s *StateDB) Finalise(deleteEmptyObjects bool) { func (s *StateDB) Finalise(deleteEmptyObjects bool) {
addressesToPrefetch := make([]common.Address, 0, len(s.journal.dirties))
for addr := range s.journal.dirties { for addr := range s.journal.dirties {
obj, exist := s.stateObjects[addr] obj, exist := s.stateObjects[addr]
if !exist { if !exist {
@ -822,16 +764,6 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) {
obj.finalise() obj.finalise()
s.markUpdate(addr) s.markUpdate(addr)
} }
// At this point, also ship the address off to the precacher. The precacher
// will start loading tries, and when the change is eventually committed,
// the commit-phase will be a lot faster
addressesToPrefetch = append(addressesToPrefetch, addr) // Copy needed for closure
}
if s.prefetcher != nil && len(addressesToPrefetch) > 0 {
id := trie.StateTrieID(s.originalRoot)
if err := s.prefetcher.prefetchAccounts(*id, addressesToPrefetch, false); err != nil {
log.Error("Failed to prefetch addresses", "addresses", len(addressesToPrefetch), "err", err)
}
} }
// Invalidate journal because reverting across transactions is not allowed. // Invalidate journal because reverting across transactions is not allowed.
s.clearJournalAndRefund() s.clearJournalAndRefund()
@ -858,15 +790,6 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
} }
s.trie = tr s.trie = tr
} }
// If there was a trie prefetcher operating, terminate it async so that the
// individual storage tries can be updated as soon as the disk load finishes.
if s.prefetcher != nil {
s.prefetcher.terminate(true)
defer func() {
s.prefetcher.report()
s.prefetcher = nil // Pre-byzantium, unset any used up prefetcher
}()
}
// Process all storage updates concurrently. The state object update root // Process all storage updates concurrently. The state object update root
// method will internally call a blocking trie fetch from the prefetcher, // method will internally call a blocking trie fetch from the prefetcher,
// so there's no need to explicitly wait for the prefetchers to finish. // so there's no need to explicitly wait for the prefetchers to finish.
@ -927,49 +850,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
obj := s.stateObjects[addr] // closure for the task runner below obj := s.stateObjects[addr] // closure for the task runner below
workers.Go(func() error { workers.Go(func() error {
obj.updateRoot() obj.updateRoot()
// If witness building is enabled and the state object has a trie,
// gather the witnesses for its specific storage trie
if s.witness != nil && obj.trie != nil {
s.witness.AddState(obj.trie.Witness(), obj.addrHash())
}
return nil return nil
}) })
} }
} }
// If witness building is enabled, gather all the read-only accesses.
// Skip witness collection in Verkle mode, they will be gathered
// together at the end.
if s.witness != nil && !s.db.TrieDB().IsVerkle() {
// Pull in anything that has been accessed before destruction
for _, obj := range s.stateObjectsDestruct {
// Skip any objects that haven't touched their storage
if len(obj.originStorage) == 0 {
continue
}
if trie := obj.getPrefetchedTrie(); trie != nil {
s.witness.AddState(trie.Witness(), obj.addrHash())
} else if obj.trie != nil {
s.witness.AddState(obj.trie.Witness(), obj.addrHash())
}
}
// Pull in only-read and non-destructed trie witnesses
for _, obj := range s.stateObjects {
// Skip any objects that have been updated
if _, ok := s.mutations[obj.address]; ok {
continue
}
// Skip any objects that haven't touched their storage
if len(obj.originStorage) == 0 {
continue
}
if trie := obj.getPrefetchedTrie(); trie != nil {
s.witness.AddState(trie.Witness(), obj.addrHash())
} else if obj.trie != nil {
s.witness.AddState(obj.trie.Witness(), obj.addrHash())
}
}
}
workers.Wait() workers.Wait()
s.StorageUpdates += time.Since(start) s.StorageUpdates += time.Since(start)
@ -981,14 +865,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// only a single trie is used for state hashing. Replacing a non-nil verkle tree // only a single trie is used for state hashing. Replacing a non-nil verkle tree
// here could result in losing uncommitted changes from storage. // here could result in losing uncommitted changes from storage.
start = time.Now() start = time.Now()
if s.prefetcher != nil {
id := trie.StateTrieID(s.originalRoot)
if trie := s.prefetcher.trie(*id); trie == nil {
log.Error("Failed to retrieve account pre-fetcher trie")
} else {
s.trie = trie
}
}
// Perform updates before deletions. This prevents resolution of unnecessary trie nodes // Perform updates before deletions. This prevents resolution of unnecessary trie nodes
// in circumstances similar to the following: // in circumstances similar to the following:
// //
@ -1000,7 +877,6 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// into a shortnode. This requires `B` to be resolved from disk. // into a shortnode. This requires `B` to be resolved from disk.
// Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved. // Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved.
var ( var (
usedAddrs []common.Address
deletedAddrs []common.Address deletedAddrs []common.Address
) )
for addr, op := range s.mutations { for addr, op := range s.mutations {
@ -1022,7 +898,6 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
s.CodeUpdateBytes += len(obj.code) s.CodeUpdateBytes += len(obj.code)
} }
} }
usedAddrs = append(usedAddrs, addr) // Copy needed for closure
} }
for _, deletedAddr := range deletedAddrs { for _, deletedAddr := range deletedAddrs {
s.deleteStateObject(deletedAddr) s.deleteStateObject(deletedAddr)
@ -1030,20 +905,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
} }
s.AccountUpdates += time.Since(start) s.AccountUpdates += time.Since(start)
if s.prefetcher != nil {
id := trie.StateTrieID(s.originalRoot)
s.prefetcher.used(*id, usedAddrs, nil)
}
// Track the amount of time wasted on hashing the account trie // Track the amount of time wasted on hashing the account trie
defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now()) defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now())
hash := s.trie.Hash() return s.trie.Hash()
// If witness building is enabled, gather the account trie witness
if s.witness != nil {
s.witness.AddState(s.trie.Witness(), common.Hash{})
}
return hash
} }
// SetTxContext sets the current transaction hash and index which are // SetTxContext sets the current transaction hash and index which are
@ -1476,7 +1341,7 @@ func (s *StateDB) markUpdate(addr common.Address) {
// Witness retrieves the current state witness being collected. // Witness retrieves the current state witness being collected.
func (s *StateDB) Witness() *stateless.Witness { func (s *StateDB) Witness() *stateless.Witness {
return s.witness return nil
} }
func (s *StateDB) AccessEvents() *AccessEvents { func (s *StateDB) AccessEvents() *AccessEvents {

View file

@ -26,6 +26,8 @@ import (
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
) )
//lint:file-ignore U1000 this file intentionally keeps unused helpers for future use
var ( var (
// triePrefetchMetricsPrefix is the prefix under which to publish the metrics. // triePrefetchMetricsPrefix is the prefix under which to publish the metrics.
triePrefetchMetricsPrefix = "trie/prefetch/" triePrefetchMetricsPrefix = "trie/prefetch/"
@ -111,6 +113,7 @@ func (p *triePrefetcher) terminate(async bool) {
} }
// report aggregates the pre-fetching and usage metrics and reports them. // report aggregates the pre-fetching and usage metrics and reports them.
// nolint:unused
func (p *triePrefetcher) report() { func (p *triePrefetcher) report() {
if !metrics.Enabled() { if !metrics.Enabled() {
return return
@ -205,6 +208,7 @@ func (p *triePrefetcher) trie(id trie.ID) Trie {
// used marks a batch of state items used to allow creating statistics as to // used marks a batch of state items used to allow creating statistics as to
// how useful or wasteful the fetcher is. // how useful or wasteful the fetcher is.
// nolint:unused
func (p *triePrefetcher) used(id trie.ID, usedAddr []common.Address, usedSlot []common.Hash) { func (p *triePrefetcher) used(id trie.ID, usedAddr []common.Address, usedSlot []common.Hash) {
if fetcher := p.fetchers[id]; fetcher != nil { if fetcher := p.fetchers[id]; fetcher != nil {
fetcher.wait() // ensure the fetcher's idle before poking in its internals fetcher.wait() // ensure the fetcher's idle before poking in its internals