core/state: bypass per-account updateTrie in IntermediateRoot for binary trie

In binary trie mode, IntermediateRoot calls updateTrie() once per dirty
account. But with the binary trie there is only one unified trie
(OpenStorageTrie returns self), so each call redundantly does per-account
trie setup: getPrefetchedTrie, getTrie, slice allocations for
deletions/used, and prefetcher.used — all for the same trie pointer.

Replace the per-account updateTrie() calls with a single flat loop that
applies all storage updates directly to s.trie. The MPT path is
unchanged. The prefetcher trie replacement is guarded to avoid
overwriting the binary trie that received updates.
This commit is contained in:
CPerezz 2026-03-17 00:01:13 +01:00
parent 98b13f342f
commit 527506d25d
No known key found for this signature in database
GPG key ID: 62045F34B97177DD

View file

@ -824,22 +824,55 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
workers errgroup.Group
)
if s.db.TrieDB().IsVerkle() {
// Whilst MPT storage tries are independent, Verkle has one single trie
// for all the accounts and all the storage slots merged together. The
// former can thus be simply parallelized, but updating the latter will
// need concurrency support within the trie itself. That's a TODO for a
// later time.
workers.SetLimit(1)
}
for addr, op := range s.mutations {
if op.applied || op.isDelete() {
continue
// Bypass per-account updateTrie() for binary trie. In binary trie mode
// there is only one unified trie (OpenStorageTrie returns self), so the
// per-account trie setup in updateTrie() (getPrefetchedTrie, getTrie,
// prefetcher.used) is redundant overhead. Apply all storage updates
// directly in a single pass.
for addr, op := range s.mutations {
if op.applied || op.isDelete() {
continue
}
obj := s.stateObjects[addr]
if len(obj.uncommittedStorage) == 0 {
continue
}
for key, origin := range obj.uncommittedStorage {
value, exist := obj.pendingStorage[key]
if value == origin || !exist {
continue
}
if (value != common.Hash{}) {
if err := s.trie.UpdateStorage(addr, key[:], common.TrimLeftZeroes(value[:])); err != nil {
s.setError(err)
}
} else {
if err := s.trie.DeleteStorage(addr, key[:]); err != nil {
s.setError(err)
}
}
}
}
obj := s.stateObjects[addr] // closure for the task runner below
workers.Go(func() error {
if s.db.TrieDB().IsVerkle() {
obj.updateTrie()
} else {
// Clear uncommittedStorage and assign trie on each touched object.
// obj.trie must be set because this path bypasses updateTrie(), which
// is where obj.trie normally gets lazily loaded via getTrie().
for addr, op := range s.mutations {
if op.applied || op.isDelete() {
continue
}
obj := s.stateObjects[addr]
if len(obj.uncommittedStorage) > 0 {
obj.uncommittedStorage = make(Storage)
}
obj.trie = s.trie
}
} else {
for addr, op := range s.mutations {
if op.applied || op.isDelete() {
continue
}
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,
@ -847,9 +880,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
if s.witness != nil && obj.trie != nil {
s.witness.AddState(obj.trie.Witness())
}
}
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
@ -911,7 +944,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 {
if s.prefetcher != nil && !s.db.TrieDB().IsVerkle() {
if trie := s.prefetcher.trie(common.Hash{}, s.originalRoot); trie == nil {
log.Error("Failed to retrieve account pre-fetcher trie")
} else {