perf(trie): cache root hash with dirty-bit, make Hash() O(1) on unmodified trie#39
Open
simonzg wants to merge 2 commits into
Open
perf(trie): cache root hash with dirty-bit, make Hash() O(1) on unmodified trie#39simonzg wants to merge 2 commits into
simonzg wants to merge 2 commits into
Conversation
…e is unmodified trie.Hash() previously called hashRoot() on every invocation, which performs a full recursive traversal of all trie nodes recomputing hashes bottom-up. Calling Hash() twice on an unmodified trie traversed the entire tree twice. In state/stage.go, Hash() is called in separate code paths on the same unchanged trie — once during state root computation and again during revert — paying the full traversal cost both times for no reason. Add two fields to Trie: cachedHash meter.Bytes32 — last computed root hash hashDirty bool — true if trie was modified since last Hash() TryUpdate and TryDelete set hashDirty = true. Hash() returns cachedHash immediately when hashDirty is false. CommitTo populates cachedHash after commit (hash is computed there anyway). New() initialises cachedHash = root (already known) with hashDirty = false. Hash() on an unmodified trie: O(n) tree traversal → O(1) field read. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
trie.Hash()performs a full recursive bottom-up traversal of all trie nodes to recompute hashes on every single call. There is no caching: callingHash()twice on an unchanged trie traverses the entire tree twice, doing identical work.This matters in practice because
state/stage.gocallsHash()in separate code paths on the same unchanged trie — once during state root computation and again duringRevert(). For a trie with thousands of nodes (typical for an active account or storage trie during busy block execution), this doubles the hashing work per block with zero benefit.More broadly, any code that calls
Hash()for comparison or logging re-pays the full O(n) traversal even if the trie has not been touched since the last call.What Changed
Added two fields to
Trie:New()initialisescachedHash = root(already known from the DB key) andhashDirty = false— a freshly loaded trie is clean by definition.TryUpdateandTryDeletesethashDirty = true.Hash()returnscachedHashimmediately when!hashDirty, skipping the tree traversal.CommitTopopulatescachedHashafter commit (the hash is computed there anyway) and clearshashDirty.Expected Impact
Hash()on an unmodified trie:O(n) tree traversal→O(1) field read.stage.gorevert path no longer pays full trie traversal cost.