From f674fdc18f75b84ba183a3e146e69b225bcd0c0b Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Wed, 8 Jul 2026 15:47:44 +0200 Subject: [PATCH] trie: cache rlp encoded blobs --- trie/committer.go | 12 +++++++++++- trie/hasher.go | 25 +++++++++++++++++-------- trie/iterator.go | 2 +- trie/node.go | 2 ++ trie/proof.go | 2 +- trie/secure_trie.go | 4 ++++ trie/stacktrie.go | 2 +- trie/trie.go | 8 +++++++- 8 files changed, 44 insertions(+), 13 deletions(-) diff --git a/trie/committer.go b/trie/committer.go index 2a2142e0ff..1803c37048 100644 --- a/trie/committer.go +++ b/trie/committer.go @@ -149,8 +149,18 @@ func (c *committer) store(path []byte, n node) node { return n } // Collect the dirty node to nodeset for return. + var blob []byte + switch n := n.(type) { + case *shortNode: + blob = n.flags.blob + case *fullNode: + blob = n.flags.blob + } + if len(blob) == 0 { + blob = nodeToBytes(n) + } nhash := common.BytesToHash(hash) - c.nodes.AddNode(path, trienode.NewNodeWithPrev(nhash, nodeToBytes(n), c.tracer.Get(path))) + c.nodes.AddNode(path, trienode.NewNodeWithPrev(nhash, blob, c.tracer.Get(path))) // Collect the corresponding leaf node if it's required. We don't check // full node since it's impossible to store value in fullNode. The key diff --git a/trie/hasher.go b/trie/hasher.go index a2a1f5b662..fdbf59aade 100644 --- a/trie/hasher.go +++ b/trie/hasher.go @@ -21,6 +21,7 @@ import ( "fmt" "sync" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" ) @@ -28,10 +29,11 @@ import ( // hasher is a type used for the trie Hash operation. A hasher has some // internal preallocated temp space type hasher struct { - sha crypto.KeccakState - tmp []byte - encbuf rlp.EncoderBuffer - parallel bool // Whether to use parallel threads when hashing + sha crypto.KeccakState + tmp []byte + encbuf rlp.EncoderBuffer + parallel bool // Whether to use parallel threads when hashing + cacheBlob bool // Whether to stash each node's encoding for the committer } // hasherPool holds pureHashers @@ -45,9 +47,10 @@ var hasherPool = sync.Pool{ }, } -func newHasher(parallel bool) *hasher { +func newHasher(parallel, cacheBlob bool) *hasher { h := hasherPool.Get().(*hasher) h.parallel = parallel + h.cacheBlob = cacheBlob return h } @@ -76,6 +79,9 @@ func (h *hasher) hash(n node, force bool) []byte { } hash := h.hashData(enc) n.flags.hash = hash + if h.cacheBlob { + n.flags.blob = common.CopyBytes(enc) + } return hash case *fullNode: @@ -91,6 +97,9 @@ func (h *hasher) hash(n node, force bool) []byte { } hash := h.hashData(enc) n.flags.hash = hash + if h.cacheBlob { + n.flags.blob = common.CopyBytes(enc) + } return hash case hashNode: @@ -148,9 +157,9 @@ func (h *hasher) encodeFullNode(n *fullNode) []byte { go func(i int) { defer wg.Done() - h := newHasher(false) - fn.Children[i] = h.hash(n.Children[i], false) - returnHasherToPool(h) + h2 := newHasher(false, h.cacheBlob) + fn.Children[i] = h2.hash(n.Children[i], false) + returnHasherToPool(h2) }(i) } wg.Wait() diff --git a/trie/iterator.go b/trie/iterator.go index 3d3191ffba..35f71ad3e2 100644 --- a/trie/iterator.go +++ b/trie/iterator.go @@ -234,7 +234,7 @@ func (it *nodeIterator) LeafBlob() []byte { func (it *nodeIterator) LeafProof() [][]byte { if len(it.stack) > 0 { if _, ok := it.stack[len(it.stack)-1].node.(valueNode); ok { - hasher := newHasher(false) + hasher := newHasher(false, false) defer returnHasherToPool(hasher) proofs := make([][]byte, 0, len(it.stack)) diff --git a/trie/node.go b/trie/node.go index b5094ff4b7..33a5b3040d 100644 --- a/trie/node.go +++ b/trie/node.go @@ -80,12 +80,14 @@ func (n *fullNode) EncodeRLP(w io.Writer) error { // nodeFlag contains caching-related metadata about a node. type nodeFlag struct { hash hashNode // cached hash of the node (may be nil) + blob []byte // cached rlp encoding of the node, set along with the hash (may be nil) dirty bool // whether the node has changes that must be written to the database } func (n nodeFlag) copy() nodeFlag { return nodeFlag{ hash: common.CopyBytes(n.hash), + blob: common.CopyBytes(n.blob), dirty: n.dirty, } } diff --git a/trie/proof.go b/trie/proof.go index 58075daf9b..13b3813673 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -82,7 +82,7 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { panic(fmt.Sprintf("%T: invalid node: %v", tn, tn)) } } - hasher := newHasher(false) + hasher := newHasher(false, false) defer returnHasherToPool(hasher) for i, n := range nodes { diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 9f6cea9790..91b7edc8bd 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -83,6 +83,10 @@ func NewStateTrie(id *ID, db database.NodeDatabase) (*StateTrie, error) { if err != nil { return nil, err } + // State tries always follow a hash-then-commit lifecycle, so cache the node + // encodings produced during hashing for reuse by the subsequent commit. + trie.cacheBlob = true + tr := &StateTrie{ trie: *trie, db: db, diff --git a/trie/stacktrie.go b/trie/stacktrie.go index 6bb5421e9c..7577590455 100644 --- a/trie/stacktrie.go +++ b/trie/stacktrie.go @@ -58,7 +58,7 @@ type StackTrie struct { func NewStackTrie(onTrieNode OnTrieNode) *StackTrie { return &StackTrie{ root: stPool.Get().(*stNode), - h: newHasher(false), + h: newHasher(false, false), onTrieNode: onTrieNode, kBuf: make([]byte, 64), pBuf: make([]byte, 64), diff --git a/trie/trie.go b/trie/trie.go index 0026c6048c..ede697623c 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -54,6 +54,11 @@ type Trie struct { // uncommitted is the number of updates since last commit. uncommitted int + // cacheBlob makes the hasher cache encoded trie nodes. + // Saves re-encoding on commit. Can be turned off for + // Trie's that are never committed (e.g. for snap sync). + cacheBlob bool + // reader is the handler trie can retrieve nodes from. reader *Reader @@ -75,6 +80,7 @@ func (t *Trie) Copy() *Trie { committed: t.committed, unhashed: t.unhashed, uncommitted: t.uncommitted, + cacheBlob: t.cacheBlob, reader: t.reader, opTracer: t.opTracer.copy(), prevalueTracer: t.prevalueTracer.Copy(), @@ -860,7 +866,7 @@ func (t *Trie) hashRoot() []byte { return types.EmptyRootHash.Bytes() } // If the number of changes is below 100, we let one thread handle it - h := newHasher(t.unhashed >= 100) + h := newHasher(t.unhashed >= 100, t.cacheBlob) defer func() { returnHasherToPool(h) t.unhashed = 0