From 9daaef1923d685f239df03f2a7241a4f98b282b2 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 20 Mar 2026 14:21:21 +0800 Subject: [PATCH] core/state: remove trie prefetcher and witness from stateDB --- core/state/state_object.go | 73 ++--------------- core/state/statedb.go | 149 ++-------------------------------- core/state/trie_prefetcher.go | 4 + 3 files changed, 18 insertions(+), 208 deletions(-) diff --git a/core/state/state_object.go b/core/state/state_object.go index b5cc51a157..e6405b0f05 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -119,11 +119,6 @@ func (s *stateObject) addrHash() common.Hash { 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() { s.selfDestructed = true } @@ -149,23 +144,6 @@ func (s *stateObject) getTrie() (Trie, error) { 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. func (s *stateObject) GetState(key common.Hash) common.Hash { value, _ := s.getState(key) @@ -226,12 +204,6 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { } s.db.StorageReads += time.Since(start) - // Schedule the resolved storage slots for prefetching if it's enabled. - if s.db.prefetcher != nil && s.data.Root != types.EmptyRootHash { - 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 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 // committed later. It is invoked at the end of every transaction. func (s *stateObject) finalise() { - slotsToPrefetch := make([]common.Hash, 0, len(s.dirtyStorage)) for key, value := range s.dirtyStorage { if origin, exist := s.uncommittedStorage[key]; exist && origin == value { // 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 // tracked for commit yet. 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 // 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. 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 { s.dirtyStorage = make(Storage) } @@ -311,34 +276,16 @@ func (s *stateObject) finalise() { // // It assumes all the dirty storage slots have been finalized before. func (s *stateObject) updateTrie() (Trie, error) { - // Short circuit if nothing was accessed, don't trigger a prefetcher warning - 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 + // Short circuit if nothing was accessed if len(s.uncommittedStorage) == 0 { 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 // 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. var ( deletions []common.Hash - used = make([]common.Hash, 0, len(s.uncommittedStorage)) ) for key, origin := range s.uncommittedStorage { // Skip noop changes, persist actual changes @@ -373,8 +319,6 @@ func (s *stateObject) updateTrie() (Trie, error) { } else { deletions = append(deletions, key) } - // Cache the items for preloading - used = append(used, key) // Copy needed for closure } for _, key := range deletions { if err := tr.DeleteStorage(s.address, key[:]); err != nil { @@ -383,9 +327,6 @@ func (s *stateObject) updateTrie() (Trie, error) { } 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 return tr, nil } diff --git a/core/state/statedb.go b/core/state/statedb.go index 9c83036a45..d085bd31c6 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" "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- // commit states. type StateDB struct { - db Database - prefetcher *triePrefetcher - reader Reader - trie Trie // it's resolved on first access + db Database + reader Reader + trie Trie // it's resolved on first access // originalRoot is the pre-state root, before any changes were made. // It will be updated when the Commit is called. @@ -133,9 +131,6 @@ type StateDB struct { // Snapshot and RevertToSnapshot. journal *journal - // State witness if cross validation is needed - witness *stateless.Witness - // Measurements gathered during execution for debugging purposes AccountReads 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 // commit phase, most of the needed data is already hot. 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 // from the gathered metrics. -func (s *StateDB) StopPrefetcher() { - if s.prefetcher != nil { - s.prefetcher.terminate(false) - s.prefetcher.report() - s.prefetcher = nil - } -} +func (s *StateDB) StopPrefetcher() {} // setError remembers the first non-nil error it is called with. func (s *StateDB) setError(err error) { @@ -368,9 +327,6 @@ func (s *StateDB) TxIndex() int { func (s *StateDB) GetCode(addr common.Address) []byte { stateObject := s.getStateObject(addr) if stateObject != nil { - if s.witness != nil { - s.witness.AddCode(stateObject.Code()) - } return stateObject.Code() } return nil @@ -379,9 +335,6 @@ func (s *StateDB) GetCode(addr common.Address) []byte { func (s *StateDB) GetCodeSize(addr common.Address) int { stateObject := s.getStateObject(addr) if stateObject != nil { - if s.witness != nil { - s.witness.AddCode(stateObject.Code()) - } return stateObject.CodeSize() } return 0 @@ -612,13 +565,6 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { if acct == 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 obj := newObject(s, addr, acct) s.setStateObject(obj) @@ -710,9 +656,6 @@ func (s *StateDB) Copy() *StateDB { if s.trie != nil { state.trie = mustCopyTrie(s.trie) } - if s.witness != nil { - state.witness = s.witness.Copy() - } if s.accessEvents != nil { 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 // into the tries just yet. Only IntermediateRoot or Commit will do that. func (s *StateDB) Finalise(deleteEmptyObjects bool) { - addressesToPrefetch := make([]common.Address, 0, len(s.journal.dirties)) for addr := range s.journal.dirties { obj, exist := s.stateObjects[addr] if !exist { @@ -822,16 +764,6 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { obj.finalise() 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. s.clearJournalAndRefund() @@ -858,15 +790,6 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { } 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 // method will internally call a blocking trie fetch from the prefetcher, // 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 workers.Go(func() error { 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 }) } } - // 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() 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 // here could result in losing uncommitted changes from storage. 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 // 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. // Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved. var ( - usedAddrs []common.Address deletedAddrs []common.Address ) for addr, op := range s.mutations { @@ -1022,7 +898,6 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { s.CodeUpdateBytes += len(obj.code) } } - usedAddrs = append(usedAddrs, addr) // Copy needed for closure } for _, deletedAddr := range deletedAddrs { s.deleteStateObject(deletedAddr) @@ -1030,20 +905,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { } 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 defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now()) - hash := 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 + return s.trie.Hash() } // 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. func (s *StateDB) Witness() *stateless.Witness { - return s.witness + return nil } func (s *StateDB) AccessEvents() *AccessEvents { diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index f6cfed834b..cb64e0fb77 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -26,6 +26,8 @@ import ( "github.com/ethereum/go-ethereum/trie" ) +//lint:file-ignore U1000 this file intentionally keeps unused helpers for future use + var ( // triePrefetchMetricsPrefix is the prefix under which to publish the metrics. triePrefetchMetricsPrefix = "trie/prefetch/" @@ -111,6 +113,7 @@ func (p *triePrefetcher) terminate(async bool) { } // report aggregates the pre-fetching and usage metrics and reports them. +// nolint:unused func (p *triePrefetcher) report() { if !metrics.Enabled() { 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 // how useful or wasteful the fetcher is. +// nolint:unused func (p *triePrefetcher) used(id trie.ID, usedAddr []common.Address, usedSlot []common.Hash) { if fetcher := p.fetchers[id]; fetcher != nil { fetcher.wait() // ensure the fetcher's idle before poking in its internals