reth_trie/proof_v2/mod.rs
1//! Proof calculation version 2: Leaf-only implementation.
2//!
3//! This module provides a rewritten proof calculator that:
4//! - Uses only leaf data (HashedAccounts/Storages) to generate proofs
5//! - Returns proof nodes sorted lexicographically by path
6//! - Automatically resets after each calculation
7//! - Re-uses cursors across calculations
8//! - Supports generic value types with lazy evaluation
9
10use crate::{
11 hashed_cursor::{HashedCursor, HashedStorageCursor},
12 trie_cursor::{depth_first, TrieCursor, TrieStorageCursor},
13};
14use alloy_primitives::{keccak256, B256, U256};
15use alloy_rlp::Encodable;
16use alloy_trie::{BranchNodeCompact, TrieMask};
17use reth_execution_errors::trie::StateProofError;
18use reth_trie_common::{
19 prefix_set::PrefixSet, BranchNodeMasks, BranchNodeRef, BranchNodeV2, Nibbles, ProofTrieNodeV2,
20 ProofV2Target, RlpNode, TrieNodeV2,
21};
22use std::cmp::Ordering;
23use tracing::{error, instrument, trace};
24
25mod value;
26pub use value::*;
27
28mod node;
29use node::*;
30
31mod target;
32pub(crate) use target::*;
33
34/// Target to use with the `tracing` crate.
35static TRACE_TARGET: &str = "trie::proof_v2";
36
37/// Number of bytes to pre-allocate for [`ProofCalculator`]'s `rlp_encode_buf` field.
38const RLP_ENCODE_BUF_SIZE: usize = 1024;
39
40/// A proof calculator that generates merkle proofs using only leaf data.
41///
42/// The calculator:
43/// - Accepts one or more B256 proof targets sorted lexicographically
44/// - Returns proof nodes sorted lexicographically by path
45/// - Automatically resets after each calculation
46/// - Re-uses cursors from one calculation to the next
47#[derive(Debug)]
48pub struct ProofCalculator<TC, HC, VE: LeafValueEncoder> {
49 /// Trie cursor for traversing stored branch nodes.
50 trie_cursor: TC,
51 /// Hashed cursor for iterating over leaf data.
52 hashed_cursor: HC,
53 /// Branches which are currently in the process of being constructed, each being a child of
54 /// the previous one.
55 branch_stack: Vec<ProofTrieBranch>,
56 /// The path of the last branch in `branch_stack`.
57 branch_path: Nibbles,
58 /// Children of branches in the `branch_stack`.
59 ///
60 /// Each branch in `branch_stack` tracks which children are in this stack using its
61 /// `state_mask`; the number of children the branch has in this stack is equal to the number of
62 /// bits set in its `state_mask`.
63 ///
64 /// The children for the bottom branch in `branch_stack` are found at the bottom of this stack,
65 /// and so on. When a branch is removed from `branch_stack` its children are removed from this
66 /// one, and the branch is pushed onto this stack in their place (see [`Self::pop_branch`].
67 ///
68 /// Children on the `child_stack` are converted to [`ProofTrieBranchChild::RlpNode`]s via the
69 /// [`Self::commit_child`] method. Committing a child indicates that no further changes are
70 /// expected to happen to it (e.g. splitting its short key when inserting a new branch). Given
71 /// that keys are consumed in lexicographical order, only the last child on the stack can
72 /// ever be modified, and therefore all children besides the last are expected to be
73 /// [`ProofTrieBranchChild::RlpNode`]s.
74 child_stack: Vec<ProofTrieBranchChild<VE::DeferredEncoder>>,
75 /// Cached branch data pulled from the `trie_cursor`. The calculator will use the cached
76 /// [`BranchNodeCompact::hashes`] to skip over the calculation of sub-tries in the overall
77 /// trie. The cached hashes cannot be used for any paths which are prefixes of a proof target.
78 cached_branch_stack: Vec<(Nibbles, BranchNodeCompact)>,
79 /// The proofs which will be returned from the calculation. This gets taken at the end of every
80 /// proof call.
81 retained_proofs: Vec<ProofTrieNodeV2>,
82 /// Free-list of re-usable buffers of [`RlpNode`]s, used for encoding branch nodes to RLP.
83 ///
84 /// We are generally able to re-use these buffers across different branch nodes for the
85 /// duration of a proof calculation, but occasionally we will lose one when a branch
86 /// node is returned as a `ProofTrieNode`.
87 rlp_nodes_bufs: Vec<Vec<RlpNode>>,
88 /// Re-usable byte buffer, used for RLP encoding.
89 rlp_encode_buf: Vec<u8>,
90 /// Prefix set for tracking changed keys.
91 prefix_set: PrefixSet,
92}
93
94impl<TC, HC, VE: LeafValueEncoder> ProofCalculator<TC, HC, VE> {
95 /// Create a new [`ProofCalculator`] instance for calculating account proofs.
96 pub fn new(trie_cursor: TC, hashed_cursor: HC) -> Self {
97 Self {
98 trie_cursor,
99 hashed_cursor,
100 branch_stack: Vec::<_>::with_capacity(64),
101 branch_path: Nibbles::new(),
102 child_stack: Vec::<_>::with_capacity(64),
103 cached_branch_stack: Vec::<_>::with_capacity(64),
104 retained_proofs: Vec::<_>::with_capacity(32),
105 rlp_nodes_bufs: Vec::<_>::with_capacity(8),
106 rlp_encode_buf: Vec::<_>::with_capacity(RLP_ENCODE_BUF_SIZE),
107 prefix_set: PrefixSet::default(),
108 }
109 }
110
111 /// Sets the prefix set and returns `self`.
112 ///
113 /// When given, all cached hashes matching the [`PrefixSet`] will be invalidated. When all but
114 /// one of a branch's children match the prefix set then that remaining child's cached hash, if
115 /// any, will also be invalidated. This allows for properly handling branch collapse situations,
116 /// where all but one child of a branch is deleted and the remaining child is required to be
117 /// unrevealed in order to collapse the branch.
118 pub fn with_prefix_set(mut self, prefix_set: PrefixSet) -> Self {
119 self.prefix_set = prefix_set;
120 self
121 }
122}
123
124impl<TC, HC, VE> ProofCalculator<TC, HC, VE>
125where
126 TC: TrieCursor,
127 HC: HashedCursor,
128 VE: LeafValueEncoder<Value = HC::Value>,
129{
130 /// Takes a re-usable `RlpNode` buffer from the internal free-list, or allocates a new one if
131 /// the free-list is empty.
132 ///
133 /// The returned Vec will have a length of zero.
134 fn take_rlp_nodes_buf(&mut self) -> Vec<RlpNode> {
135 self.rlp_nodes_bufs
136 .pop()
137 .map(|mut buf| {
138 buf.clear();
139 buf
140 })
141 .unwrap_or_else(|| Vec::with_capacity(16))
142 }
143
144 // Returns zero if `branch_stack` is empty, one otherwise.
145 //
146 // This is used when working with the `ext_len` field of [`ProofTrieBranch`]. The `ext_len` is
147 // calculated by taking the difference of the current `branch_path` and the new branch's path;
148 // if the new branch has a parent branch (ie `branch_stack` is not empty) then 1 is subtracted
149 // from the `ext_len` to account for the child's nibble on the parent.
150 #[inline]
151 const fn maybe_parent_nibble(&self) -> usize {
152 !self.branch_stack.is_empty() as usize
153 }
154
155 /// Returns true if the proof of a node at the given path should be retained. A node is retained
156 /// if its path is a prefix of any target.
157 ///
158 /// This may move the `targets` iterator forward if the given path comes after the current
159 /// target.
160 ///
161 /// This method takes advantage of the [`std::slice::Iter`] component of [`TargetsCursor`] to
162 /// check the minimum number of targets. In general it looks at a current target and the next
163 /// target simultaneously, forming an end-exclusive range.
164 ///
165 /// ```text
166 /// * Given targets: [ 0x012, 0x045, 0x678 ]
167 /// * targets.current() returns:
168 /// - (0x012, Some(0x045)): covers (0x012..0x045)
169 /// - (0x045, Some(0x678)): covers (0x045..0x678)
170 /// - (0x678, None): covers (0x678..)
171 /// ```
172 ///
173 /// As long as the path which is passed in lies within that range we can continue to use the
174 /// current target. Once the path goes beyond that range (ie path >= next target) then we can be
175 /// sure that no further paths will be in the range, and we can iterate forward.
176 ///
177 /// ```text
178 /// * Given:
179 /// - path: 0x04
180 /// - targets.current() returns (0x012, Some(0x045))
181 ///
182 /// * 0x04 comes _after_ 0x045 in depth-first order, so (0x012..0x045) does not contain 0x04.
183 ///
184 /// * targets.next() is called.
185 ///
186 /// * targets.current() now returns (0x045, Some(0x678)). This does contain 0x04.
187 ///
188 /// * 0x04 is a prefix of 0x045, and so is retained.
189 /// ```
190 #[instrument(
191 target = TRACE_TARGET,
192 level = "trace",
193 skip_all,
194 fields(?path, ?check_min_len),
195 ret,
196 )]
197 fn should_retain<'a>(
198 &self,
199 targets: &mut Option<TargetsCursor<'a>>,
200 path: &Nibbles,
201 check_min_len: bool,
202 ) -> bool {
203 // If no targets are given then we never retain anything
204 let Some(targets) = targets.as_mut() else { return false };
205
206 let (mut lower, mut upper) = targets.current();
207
208 debug_assert!(self.retained_proofs.last().is_none_or(
209 |ProofTrieNodeV2 { path: last_retained_path, .. }| {
210 depth_first::cmp(path, last_retained_path) == Ordering::Greater
211 }
212 ),
213 "should_retain called with path {path:?} which is not after previously retained node {:?} in depth-first order",
214 self.retained_proofs.last().map(|n| n.path),
215 );
216
217 loop {
218 // If the node in question is a prefix of the target then we do not iterate targets
219 // further.
220 //
221 // Even if the node is a prefix of the target's key, if the target has a non-zero
222 // `min_len` it indicates that the node should only be retained if it is
223 // longer than that value.
224 //
225 // _However_ even if the node doesn't match the target due to the target's `min_len`, it
226 // may match other targets whose keys match this node. So we search forwards and
227 // backwards for all targets which might match this node, and check against the
228 // `min_len` of each.
229 //
230 // For example, given a branch 0xabc, with children at 0, 1, and 2, and targets:
231 // - key: 0xabc0, min_len: 2
232 // - key: 0xabc1, min_len: 1
233 // - key: 0xabc2, min_len: 4 <-- current
234 // - key: 0xabc3, min_len: 3
235 //
236 // When the branch node at 0xabc is visited it will be after the targets has iterated
237 // forward to 0xabc2 (because all children will have been visited already). At this
238 // point the target for 0xabc2 will not match the branch due to its prefix, but any of
239 // the other targets would, so we need to check those as well.
240 if lower.key_nibbles.starts_with(path) {
241 return !check_min_len ||
242 (path.len() >= lower.min_len as usize ||
243 targets
244 .skip_iter()
245 .take_while(|target| target.key_nibbles.starts_with(path))
246 .any(|target| path.len() >= target.min_len as usize) ||
247 targets
248 .rev_iter()
249 .take_while(|target| target.key_nibbles.starts_with(path))
250 .any(|target| path.len() >= target.min_len as usize))
251 }
252
253 // If the path isn't in the current range then iterate forward until it is (or until
254 // there is no upper bound, indicating unbounded).
255 if upper
256 .is_some_and(|upper| depth_first::cmp(path, &upper.key_nibbles) != Ordering::Less)
257 {
258 (lower, upper) = targets.next();
259 trace!(target: TRACE_TARGET, target = ?lower, "upper target <= path, next target");
260 } else {
261 return false
262 }
263 }
264 }
265
266 /// Takes a child which has been removed from the `child_stack` and converts it to an
267 /// [`RlpNode`].
268 ///
269 /// Calling this method indicates that the child will not undergo any further modifications, and
270 /// therefore can be retained as a proof node if applicable.
271 fn commit_child<'a>(
272 &mut self,
273 targets: &mut Option<TargetsCursor<'a>>,
274 child_path: Nibbles,
275 child: ProofTrieBranchChild<VE::DeferredEncoder>,
276 ) -> Result<RlpNode, StateProofError> {
277 // If the child is already an `RlpNode` then there is nothing to do.
278 if let ProofTrieBranchChild::RlpNode(rlp_node) = child {
279 return Ok(rlp_node)
280 }
281
282 // If we should retain the child then do so.
283 if self.should_retain(targets, &child_path, true) {
284 trace!(target: TRACE_TARGET, ?child_path, "Retaining child");
285
286 // Convert to `ProofTrieNodeV2`, which will be what is retained.
287 //
288 // If this node is a branch then its `rlp_nodes_buf` will be taken and not returned to
289 // the `rlp_nodes_bufs` free-list.
290 self.rlp_encode_buf.clear();
291 let proof_node = child.into_proof_trie_node(child_path, &mut self.rlp_encode_buf)?;
292
293 // Use the `ProofTrieNodeV2` to encode the `RlpNode`, and then push it onto retained
294 // nodes before returning.
295 self.rlp_encode_buf.clear();
296 proof_node.node.encode(&mut self.rlp_encode_buf);
297
298 self.retained_proofs.push(proof_node);
299 return Ok(RlpNode::from_rlp(&self.rlp_encode_buf));
300 }
301
302 // If the child path is not being retained then we convert directly to an `RlpNode`
303 // using `into_rlp`. Since we are not retaining the node we can recover any `RlpNode`
304 // buffers for the free-list here, hence why we do this as a separate logical branch.
305 self.rlp_encode_buf.clear();
306 let (child_rlp_node, freed_rlp_nodes_buf) = child.into_rlp(&mut self.rlp_encode_buf)?;
307
308 // If there is an `RlpNode` buffer which can be re-used then push it onto the free-list.
309 if let Some(buf) = freed_rlp_nodes_buf {
310 self.rlp_nodes_bufs.push(buf);
311 }
312
313 Ok(child_rlp_node)
314 }
315
316 /// Returns the path of the child of the currently under-construction branch at the given
317 /// nibble.
318 #[inline]
319 fn child_path_at(&self, nibble: u8) -> Nibbles {
320 let mut child_path = self.branch_path;
321 debug_assert!(child_path.len() < 64);
322 child_path.push_unchecked(nibble);
323 child_path
324 }
325
326 /// Returns index of the highest nibble which is set in the mask.
327 ///
328 /// # Panics
329 ///
330 /// Will panic in debug mode if the mask is empty.
331 #[inline]
332 fn highest_set_nibble(mask: TrieMask) -> u8 {
333 debug_assert!(!mask.is_empty());
334 (u16::BITS - mask.leading_zeros() - 1) as u8
335 }
336
337 /// Returns the path of the child on top of the `child_stack`, or the root path if the stack is
338 /// empty. Returns None if the current branch has not yet pushed a child (empty `state_mask`).
339 fn last_child_path(&self) -> Option<Nibbles> {
340 // If there is no branch under construction then the top child must be the root child.
341 let Some(branch) = self.branch_stack.last() else {
342 return Some(Nibbles::new());
343 };
344
345 (!branch.state_mask.is_empty())
346 .then(|| self.child_path_at(Self::highest_set_nibble(branch.state_mask)))
347 }
348
349 /// Calls [`Self::commit_child`] on the last child of `child_stack`, replacing it with a
350 /// [`ProofTrieBranchChild::RlpNode`].
351 ///
352 /// If `child_stack` is empty then this is a no-op.
353 ///
354 /// NOTE that this method call relies on the `state_mask` of the top branch of the
355 /// `branch_stack` to determine the last child's path. When committing the last child prior to
356 /// pushing a new child, it's important to set the new child's `state_mask` bit _after_ the call
357 /// to this method.
358 #[instrument(
359 target = TRACE_TARGET,
360 level = "trace",
361 skip_all,
362 fields(child_path = ?self.last_child_path()),
363 )]
364 fn commit_last_child<'a>(
365 &mut self,
366 targets: &mut Option<TargetsCursor<'a>>,
367 ) -> Result<(), StateProofError> {
368 if matches!(self.child_stack.last(), Some(ProofTrieBranchChild::RlpNode(_))) {
369 trace!(target: TRACE_TARGET, "Last child already committed, leaving stack unchanged");
370 return Ok(())
371 }
372
373 let Some(child_path) = self.last_child_path() else { return Ok(()) };
374 let child =
375 self.child_stack.pop().expect("child_stack can't be empty if there's a child path");
376
377 // Only commit immediately if retained for the proof. Otherwise, defer conversion
378 // to pop_branch() to give DeferredEncoder time for async work.
379 if self.should_retain(targets, &child_path, true) {
380 let child_rlp_node = self.commit_child(targets, child_path, child)?;
381 trace!(target: TRACE_TARGET, ?child_rlp_node, "Pushing committed child RlpNode onto stack");
382 self.child_stack.push(ProofTrieBranchChild::RlpNode(child_rlp_node));
383 } else {
384 trace!(target: TRACE_TARGET, "Pushing uncommitted child onto stack");
385 self.child_stack.push(child);
386 }
387
388 Ok(())
389 }
390
391 /// Creates a new leaf node on a branch, setting its `state_mask` bit and pushing the leaf onto
392 /// the `child_stack`.
393 ///
394 /// # Panics
395 ///
396 /// - If `branch_stack` is empty
397 /// - If the leaf's nibble is already set in the branch's `state_mask`.
398 fn push_new_leaf<'a>(
399 &mut self,
400 targets: &mut Option<TargetsCursor<'a>>,
401 leaf_nibble: u8,
402 leaf_short_key: Nibbles,
403 leaf_val: VE::DeferredEncoder,
404 ) -> Result<(), StateProofError> {
405 // Before pushing the new leaf onto the `child_stack` we need to commit the previous last
406 // child, so that only `child_stack`'s final child is a non-RlpNode.
407 self.commit_last_child(targets)?;
408
409 // Once the last child is committed we set the new child's bit on the top branch's
410 // `state_mask` and push that new child.
411 let branch = self.branch_stack.last_mut().expect("branch_stack cannot be empty");
412
413 debug_assert!(!branch.state_mask.is_bit_set(leaf_nibble));
414 branch.state_mask.set_bit(leaf_nibble);
415
416 self.child_stack
417 .push(ProofTrieBranchChild::Leaf { short_key: leaf_short_key, value: leaf_val });
418
419 Ok(())
420 }
421
422 /// Pushes a new branch onto the `branch_stack` based on the path and short key of the last
423 /// child on the `child_stack` and the path of the next child which will be pushed on to the
424 /// stack after this call.
425 ///
426 /// Returns the nibble of the branch's `state_mask` which should be set for the new child, and
427 /// short key that the next child should use.
428 fn push_new_branch(&mut self, new_child_path: Nibbles) -> (u8, Nibbles) {
429 // First determine the new child's shortkey relative to the current branch. If there is no
430 // current branch then the short key is the full path.
431 let new_child_short_key = if self.branch_stack.is_empty() {
432 new_child_path
433 } else {
434 // When there is a current branch then trim off its path as well as the nibble that it
435 // has set for this leaf.
436 trim_nibbles_prefix(&new_child_path, self.branch_path.len() + 1)
437 };
438
439 // Get the new branch's first child, which is the child on the top of the stack with which
440 // the new child shares the same nibble on the current branch.
441 let first_child = self
442 .child_stack
443 .last_mut()
444 .expect("push_new_branch can't be called with empty child_stack");
445
446 let first_child_short_key = first_child.short_key();
447 debug_assert!(
448 !first_child_short_key.is_empty(),
449 "push_new_branch called when top child on stack is not a leaf or extension with a short key",
450 );
451
452 // Determine how many nibbles are shared between the new branch's first child and the new
453 // child. This common prefix will be the extension of the new branch
454 let common_prefix_len = first_child_short_key.common_prefix_length(&new_child_short_key);
455
456 // Trim off the common prefix from the first child's short key, plus one nibble which will
457 // stored by the new branch itself in its state mask.
458 let first_child_nibble = first_child_short_key.get_unchecked(common_prefix_len);
459 first_child.trim_short_key_prefix(common_prefix_len + 1);
460
461 // Similarly, trim off the common prefix, plus one nibble for the new branch, from the new
462 // child's short key.
463 let new_child_nibble = new_child_short_key.get_unchecked(common_prefix_len);
464 let new_child_short_key = trim_nibbles_prefix(&new_child_short_key, common_prefix_len + 1);
465
466 // Update the branch path to reflect the new branch about to be pushed. Its path will be
467 // the path of the previous branch, plus the nibble shared by each child, plus the parent
468 // extension (denoted by a non-zero `ext_len`). Since the new branch's path is a prefix of
469 // the original new_child_path we can just slice that.
470 //
471 // If the new branch is the first branch then we do not add the extra 1, as there is no
472 // nibble in a parent branch to account for.
473 let branch_path_len =
474 self.branch_path.len() + common_prefix_len + self.maybe_parent_nibble();
475 self.branch_path = new_child_path.slice_unchecked(0, branch_path_len);
476
477 // Push the new branch onto the `branch_stack`. We do not yet set the `state_mask` bit of
478 // the new child; whatever actually pushes the child onto the `child_stack` is expected to
479 // do that.
480 self.branch_stack.push(ProofTrieBranch {
481 ext_len: common_prefix_len as u8,
482 state_mask: TrieMask::new(1 << first_child_nibble),
483 masks: None,
484 });
485
486 trace!(
487 target: TRACE_TARGET,
488 ?new_child_path,
489 ?common_prefix_len,
490 ?first_child_nibble,
491 branch_path = ?self.branch_path,
492 "Pushed new branch",
493 );
494
495 (new_child_nibble, new_child_short_key)
496 }
497
498 /// Pops the top branch off of the `branch_stack`, hashes its children on the `child_stack`, and
499 /// replaces those children on the `child_stack`. The `branch_path` field will be updated
500 /// accordingly.
501 ///
502 /// # Panics
503 ///
504 /// This method panics if `branch_stack` is empty.
505 #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
506 fn pop_branch<'a>(
507 &mut self,
508 targets: &mut Option<TargetsCursor<'a>>,
509 ) -> Result<(), StateProofError> {
510 trace!(
511 target: TRACE_TARGET,
512 branch = ?self.branch_stack.last(),
513 branch_path = ?self.branch_path,
514 child_stack_len = ?self.child_stack.len(),
515 "called",
516 );
517
518 // Ensure the final child on the child stack has been committed, as this method expects all
519 // children of the branch to have been committed.
520 self.commit_last_child(targets)?;
521
522 let mut rlp_nodes_buf = self.take_rlp_nodes_buf();
523 let branch = self.branch_stack.pop().expect("branch_stack cannot be empty");
524
525 // Take the branch's children off the stack, using the state mask to determine how many
526 // there are.
527 let num_children = branch.state_mask.count_ones() as usize;
528 debug_assert!(
529 self.child_stack.len() >= num_children,
530 "Stack is missing necessary children ({num_children:?})"
531 );
532 debug_assert!(
533 num_children >= 2,
534 "A branch must have at least two children, got {num_children}"
535 );
536
537 // Collect children into RlpNode Vec. Children are in lexicographic order.
538 rlp_nodes_buf.reserve(num_children);
539 for child in self.child_stack.drain(self.child_stack.len() - num_children..) {
540 let child_rlp_node = match child {
541 ProofTrieBranchChild::RlpNode(rlp_node) => rlp_node,
542 uncommitted_child => {
543 // Convert uncommitted child (not retained for proof) to RlpNode now.
544 self.rlp_encode_buf.clear();
545 let (rlp_node, freed_buf) =
546 uncommitted_child.into_rlp(&mut self.rlp_encode_buf)?;
547 if let Some(buf) = freed_buf {
548 self.rlp_nodes_bufs.push(buf);
549 }
550 rlp_node
551 }
552 };
553 rlp_nodes_buf.push(child_rlp_node);
554 }
555
556 debug_assert_eq!(
557 rlp_nodes_buf.len(),
558 num_children,
559 "children length must match number of bits set in state_mask"
560 );
561
562 // Calculate the short key of the parent extension (if the branch has a parent extension).
563 // It's important to calculate this short key prior to modifying the `branch_path`.
564 let short_key = trim_nibbles_prefix(
565 &self.branch_path,
566 self.branch_path.len() - branch.ext_len as usize,
567 );
568
569 // Compute hash for the branch node if it has a parent extension.
570 let rlp_node = if short_key.is_empty() {
571 None
572 } else {
573 self.rlp_encode_buf.clear();
574 BranchNodeRef::new(&rlp_nodes_buf, branch.state_mask).encode(&mut self.rlp_encode_buf);
575 Some(RlpNode::from_rlp(&self.rlp_encode_buf))
576 };
577
578 // Wrap the `BranchNodeV2` so it can be pushed onto the child stack.
579 let branch_as_child = ProofTrieBranchChild::Branch {
580 node: BranchNodeV2::new(short_key, rlp_nodes_buf, branch.state_mask, rlp_node),
581 masks: branch.masks,
582 };
583
584 self.child_stack.push(branch_as_child);
585
586 // Update the branch_path. If this branch is the only branch then only its extension needs
587 // to be trimmed, otherwise we also need to remove its nibble from its parent.
588 let new_path_len =
589 self.branch_path.len() - branch.ext_len as usize - self.maybe_parent_nibble();
590
591 debug_assert!(self.branch_path.len() >= new_path_len);
592 self.branch_path = self.branch_path.slice_unchecked(0, new_path_len);
593
594 Ok(())
595 }
596
597 /// Adds a single leaf for a key to the stack, possibly collapsing an existing branch and/or
598 /// creating a new one depending on the path of the key.
599 fn push_leaf<'a>(
600 &mut self,
601 targets: &mut Option<TargetsCursor<'a>>,
602 key: Nibbles,
603 val: VE::DeferredEncoder,
604 ) -> Result<(), StateProofError> {
605 loop {
606 trace!(
607 target: TRACE_TARGET,
608 ?key,
609 branch_stack_len = ?self.branch_stack.len(),
610 branch_path = ?self.branch_path,
611 child_stack_len = ?self.child_stack.len(),
612 "push_leaf: loop",
613 );
614
615 // Get the `state_mask` of the branch currently being built. If there are no branches
616 // on the stack then it means either the trie is empty or only a single leaf has been
617 // added previously.
618 let curr_branch_state_mask = match self.branch_stack.last() {
619 Some(curr_branch) => curr_branch.state_mask,
620 None if self.child_stack.is_empty() => {
621 // If the child stack is empty then this is the first leaf, push it and be done
622 self.child_stack
623 .push(ProofTrieBranchChild::Leaf { short_key: key, value: val });
624 return Ok(())
625 }
626 None => {
627 // If the child stack is not empty then it must only have a single other child
628 // which is either a leaf or extension with a non-zero short key.
629 debug_assert_eq!(self.child_stack.len(), 1);
630 debug_assert!(!self
631 .child_stack
632 .last()
633 .expect("already checked for emptiness")
634 .short_key()
635 .is_empty());
636 let (nibble, short_key) = self.push_new_branch(key);
637 self.push_new_leaf(targets, nibble, short_key, val)?;
638 return Ok(())
639 }
640 };
641
642 // Find the common prefix length, which is the number of nibbles shared between the
643 // current branch and the key.
644 let common_prefix_len = self.branch_path.common_prefix_length(&key);
645
646 // If the current branch does not share all of its nibbles with the new key then it is
647 // not the parent of the new key. In this case the current branch will have no more
648 // children. We can pop it and loop back to the top to try again with its parent branch.
649 if common_prefix_len < self.branch_path.len() {
650 self.pop_branch(targets)?;
651 continue
652 }
653
654 // If the current branch is a prefix of the new key then the leaf is a child of the
655 // branch. If the branch doesn't have the leaf's nibble set then the leaf can be added
656 // directly, otherwise a new branch must be created in-between this branch and that
657 // existing child.
658 let nibble = key.get_unchecked(common_prefix_len);
659 if curr_branch_state_mask.is_bit_set(nibble) {
660 // Push a new branch which splits the short key of the existing child at this
661 // nibble.
662 let (nibble, short_key) = self.push_new_branch(key);
663 // Push the new leaf onto the new branch.
664 self.push_new_leaf(targets, nibble, short_key, val)?;
665 } else {
666 let short_key = key.slice_unchecked(common_prefix_len + 1, key.len());
667 self.push_new_leaf(targets, nibble, short_key, val)?;
668 }
669
670 return Ok(())
671 }
672 }
673
674 /// Given the lower and upper bounds (exclusive) of a range of keys, iterates over the
675 /// `hashed_cursor` and calculates all trie nodes possible based on those keys. If the upper
676 /// bound is None then it is considered unbounded.
677 ///
678 /// It is expected that this method is "driven" by `next_uncached_key_range`, which decides
679 /// which ranges of keys need to be calculated based on what cached trie data is available.
680 #[instrument(
681 target = TRACE_TARGET,
682 level = "trace",
683 skip_all,
684 fields(?lower_bound, ?upper_bound),
685 )]
686 fn calculate_key_range<'a>(
687 &mut self,
688 value_encoder: &mut VE,
689 targets: &mut Option<TargetsCursor<'a>>,
690 hashed_cursor_current: &mut Option<(Nibbles, VE::DeferredEncoder)>,
691 lower_bound: Nibbles,
692 upper_bound: Option<Nibbles>,
693 ) -> Result<(), StateProofError> {
694 // A helper closure for mapping entries returned from the `hashed_cursor`, converting the
695 // key to Nibbles and immediately creating the DeferredValueEncoder so that encoding of the
696 // leaf value can begin ASAP.
697 let mut map_hashed_cursor_entry = |(key_b256, val): (B256, _)| {
698 debug_assert_eq!(key_b256.len(), 32);
699 let key = Nibbles::unpack_array(key_b256.as_ref());
700 let val = value_encoder.deferred_encoder(key_b256, val);
701 (key, val)
702 };
703
704 // If the cursor hasn't been used, or the last iterated key is prior to this range's
705 // key range, then seek forward to at least the first key.
706 if hashed_cursor_current.as_ref().is_none_or(|(key, _)| key < &lower_bound) {
707 trace!(
708 target: TRACE_TARGET,
709 current=?hashed_cursor_current.as_ref().map(|(k, _)| k),
710 "Seeking hashed cursor to meet lower bound",
711 );
712
713 let lower_key = B256::right_padding_from(&lower_bound.pack());
714 *hashed_cursor_current =
715 self.hashed_cursor.seek(lower_key)?.map(&mut map_hashed_cursor_entry);
716 }
717
718 // Loop over all keys in the range, calling `push_leaf` on each.
719 while let Some((key, _)) = hashed_cursor_current.as_ref() &&
720 upper_bound.is_none_or(|upper_bound| key < &upper_bound)
721 {
722 let (key, val) =
723 core::mem::take(hashed_cursor_current).expect("while-let checks for Some");
724 self.push_leaf(targets, key, val)?;
725 *hashed_cursor_current = self.hashed_cursor.next()?.map(&mut map_hashed_cursor_entry);
726 }
727
728 trace!(target: TRACE_TARGET, "No further keys within range");
729 Ok(())
730 }
731
732 /// Constructs and returns a new [`ProofTrieBranch`] based on an existing [`BranchNodeCompact`].
733 #[inline]
734 const fn new_from_cached_branch(
735 cached_branch: &BranchNodeCompact,
736 ext_len: u8,
737 ) -> ProofTrieBranch {
738 ProofTrieBranch {
739 ext_len,
740 state_mask: TrieMask::new(0),
741 masks: Some(BranchNodeMasks {
742 tree_mask: cached_branch.tree_mask,
743 hash_mask: cached_branch.hash_mask,
744 }),
745 }
746 }
747
748 /// Pushes a new branch onto the `branch_stack` which is based on a cached branch obtained via
749 /// the trie cursor.
750 ///
751 /// If there is already a child at the top branch of `branch_stack` occupying this new branch's
752 /// nibble then that child will have its short-key split with another new branch, and this
753 /// cached branch will be a child of that splitting branch.
754 fn push_cached_branch<'a>(
755 &mut self,
756 targets: &mut Option<TargetsCursor<'a>>,
757 cached_path: Nibbles,
758 cached_branch: &BranchNodeCompact,
759 ) -> Result<(), StateProofError> {
760 debug_assert!(
761 cached_path.starts_with(&self.branch_path),
762 "push_cached_branch called with path {cached_path:?} which is not a child of current branch {:?}",
763 self.branch_path,
764 );
765
766 let parent_branch = self.branch_stack.last();
767
768 // If both stacks are empty then there were no leaves before this cached branch, push it and
769 // be done; the extension of the branch will be its full path.
770 if self.child_stack.is_empty() && parent_branch.is_none() {
771 self.branch_path = cached_path;
772 self.branch_stack
773 .push(Self::new_from_cached_branch(cached_branch, cached_path.len() as u8));
774 return Ok(())
775 }
776
777 // Get the nibble which should be set in the parent branch's `state_mask` for this new
778 // branch.
779 let cached_branch_nibble = cached_path.get_unchecked(self.branch_path.len());
780
781 // We calculate the `ext_len` of the new branch, and potentially update its nibble if a new
782 // parent branch is inserted here, based on the state of the parent branch.
783 let (cached_branch_nibble, ext_len) = if parent_branch
784 .is_none_or(|parent_branch| parent_branch.state_mask.is_bit_set(cached_branch_nibble))
785 {
786 // If the `child_stack` is not empty but the `branch_stack` is then it implies that
787 // there must be a leaf or extension at the root of the trie whose short-key will get
788 // split by a new branch, which will become the parent of both that leaf/extension and
789 // this new branch.
790 //
791 // Similarly, if there is a branch on the `branch_stack` but its `state_mask` bit for
792 // this new branch is already set, then there must be a leaf/extension with a short-key
793 // to be split.
794 debug_assert!(!self
795 .child_stack
796 .last()
797 .expect("already checked for emptiness")
798 .short_key()
799 .is_empty());
800
801 // Split that leaf/extension's short key with a new branch.
802 let (nibble, short_key) = self.push_new_branch(cached_path);
803 (nibble, short_key.len())
804 } else {
805 // If there is a parent branch but its `state_mask` bit for this branch is not set
806 // then we can simply calculate the `ext_len` based on the difference of each, minus
807 // 1 to account for the nibble in the `state_mask`.
808 (cached_branch_nibble, cached_path.len() - self.branch_path.len() - 1)
809 };
810
811 // `commit_last_child` relies on the last set bit of the parent branch's `state_mask` to
812 // determine the path of the last child on the `child_stack`. Since we are about to
813 // change that mask we need to commit that last child first.
814 self.commit_last_child(targets)?;
815
816 // When pushing a new branch we need to set its child nibble in the `state_mask` of
817 // its parent, if there is one.
818 if let Some(parent_branch) = self.branch_stack.last_mut() {
819 parent_branch.state_mask.set_bit(cached_branch_nibble);
820 }
821
822 // Finally update the `branch_path` and push the new branch.
823 self.branch_path = cached_path;
824 self.branch_stack.push(Self::new_from_cached_branch(cached_branch, ext_len as u8));
825
826 trace!(
827 target: TRACE_TARGET,
828 branch=?self.branch_stack.last(),
829 branch_path=?self.branch_path,
830 "Pushed cached branch",
831 );
832
833 Ok(())
834 }
835
836 /// Wraps [`TrieCursor::seek`], skipping cached branches whose sub-tries must be recalculated
837 /// from leaves.
838 ///
839 /// A cached branch is skipped when all but at most one of its children match the prefix set.
840 /// In that case those children might all be deleted, leaving a branch with a single child.
841 /// A single-child branch must be collapsed, but collapsing requires the child to be a full
842 /// node (not a cached hash). Skipping the branch avoids this by forcing recalculation.
843 fn trie_cursor_seek(
844 &mut self,
845 key: Nibbles,
846 ) -> Result<Option<(Nibbles, BranchNodeCompact)>, StateProofError> {
847 let mut entry = self.trie_cursor.seek(key)?;
848 while let Some((ref path, ref branch)) = entry {
849 if !self.should_skip_cached_branch(path, branch) {
850 break
851 }
852 entry = self.trie_cursor.next()?;
853 }
854 Ok(entry)
855 }
856
857 /// Returns true if the cached branch should be skipped entirely and its sub-trie recalculated
858 /// from leaves.
859 fn should_skip_cached_branch(
860 &mut self,
861 cached_path: &Nibbles,
862 cached_branch: &BranchNodeCompact,
863 ) -> bool {
864 if !self.prefix_set.contains(cached_path) {
865 return false
866 }
867
868 let mut num_unmatched = 0u32;
869 let mut child_path = *cached_path;
870 for nibble in 0u8..16 {
871 if cached_branch.state_mask.is_bit_set(nibble) {
872 child_path.truncate(cached_path.len());
873 child_path.push_unchecked(nibble);
874 if !self.prefix_set.contains(&child_path) {
875 num_unmatched += 1;
876 }
877 }
878 }
879
880 if num_unmatched <= 1 {
881 trace!(
882 target: TRACE_TARGET,
883 ?cached_path,
884 ?num_unmatched,
885 "Skipping cached branch: all but <=1 children match prefix set, branch may collapse",
886 );
887 true
888 } else {
889 false
890 }
891 }
892
893 /// Attempts to pop off the top branch of the `cached_branch_stack`, returning
894 /// [`PopCachedBranchOutcome::Popped`] on success. Returns other variants to indicate that the
895 /// stack is empty and what to do about it.
896 ///
897 /// This method only returns [`PopCachedBranchOutcome::CalculateLeaves`] if there is a cached
898 /// branch on top of the stack.
899 #[inline]
900 fn try_pop_cached_branch(
901 &mut self,
902 trie_cursor_state: &mut TrieCursorState,
903 sub_trie_prefix: &Nibbles,
904 uncalculated_lower_bound: &Option<Nibbles>,
905 ) -> Result<PopCachedBranchOutcome, StateProofError> {
906 // If the `uncalculated_lower_bound` is None it indicates that there can be no more
907 // leaf data, so similarly there can be no more cached branch data.
908 let Some(uncalculated_lower_bound) = uncalculated_lower_bound else {
909 return Ok(PopCachedBranchOutcome::Exhausted)
910 };
911
912 // If there is a branch on top of the stack we use that.
913 if let Some(cached) = self.cached_branch_stack.pop() {
914 return Ok(PopCachedBranchOutcome::Popped(cached));
915 }
916
917 // There is no cached branch on the stack. It's possible that another one exists
918 // farther on in the trie, but we perform some checks first to prevent unnecessary
919 // attempts to find it.
920
921 // If [`TrieCursorState::path`] returns None it means that the cursor has been
922 // exhausted, so there can be no more cached data.
923 let Some(mut trie_cursor_path) = trie_cursor_state.path() else {
924 return Ok(PopCachedBranchOutcome::Exhausted)
925 };
926
927 // If the trie cursor is seeked to a branch whose leaves have already been processed
928 // then we can't use it, instead we seek forward and try again.
929 if trie_cursor_path < uncalculated_lower_bound {
930 *trie_cursor_state =
931 TrieCursorState::seeked(self.trie_cursor_seek(*uncalculated_lower_bound)?);
932
933 // Having just seeked forward we need to check if the cursor is now exhausted,
934 // extracting the new path at the same time.
935 if let Some(new_trie_cursor_path) = trie_cursor_state.path() {
936 trie_cursor_path = new_trie_cursor_path
937 } else {
938 return Ok(PopCachedBranchOutcome::Exhausted)
939 };
940 }
941
942 // If the trie cursor has exceeded the sub-trie then we consider it to be exhausted.
943 if !trie_cursor_path.starts_with(sub_trie_prefix) {
944 return Ok(PopCachedBranchOutcome::Exhausted)
945 }
946
947 // At this point we can be sure that the cursor is in an `Available` state. We know for
948 // sure it's not `Exhausted` because of the calls to `path` above, and we know it's not
949 // `Taken` because we push all taken branches onto the `cached_branch_stack`, and the
950 // stack is empty.
951 //
952 // We will use this `Available` cached branch as our next branch.
953 let cached = trie_cursor_state.take();
954 trace!(target: TRACE_TARGET, cached=?cached, "Pushed next trie node onto cached_branch_stack");
955
956 // If the calculated range is not caught up to the next cached branch it means there
957 // are portions of the trie prior to that branch which may need to be calculated;
958 // return the uncalculated range up to that branch to make that happen.
959 //
960 // If the next cached branch's path is all zeros then we can skip this catch-up step,
961 // because there cannot be any keys prior to that range.
962 let cached_path = &cached.0;
963 if uncalculated_lower_bound < cached_path && !cached_path.is_zeroes() {
964 let range = (*uncalculated_lower_bound, Some(*cached_path));
965 trace!(target: TRACE_TARGET, ?range, "Returning key range to calculate in order to catch up to cached branch");
966
967 // Push the cached branch onto the stack so it's available once the leaf range is done
968 // being calculated.
969 self.cached_branch_stack.push(cached);
970
971 return Ok(PopCachedBranchOutcome::CalculateLeaves(range));
972 }
973
974 Ok(PopCachedBranchOutcome::Popped(cached))
975 }
976
977 // Pop any under-construction branches that are now complete. Assumes that all trie data prior
978 // to `next_path`, if any, has been computed. Any branches which were under-construction
979 // previously, and which do not share a prefix with `next_path`, can be assumed to be completed;
980 // they will not have any further keys added to them.
981 fn commit_branches<'a>(
982 &mut self,
983 targets: &mut Option<TargetsCursor<'a>>,
984 next_path: &Nibbles,
985 ) -> Result<(), StateProofError> {
986 while !next_path.starts_with(&self.branch_path) {
987 self.pop_branch(targets)?;
988 }
989 Ok(())
990 }
991
992 /// Accepts the current state of both hashed and trie cursors, and determines the next range of
993 /// hashed keys which need to be processed using [`Self::push_leaf`].
994 ///
995 /// This method will use cached branch node data from the trie cursor to skip over all possible
996 /// ranges of keys, to reduce computation as much as possible.
997 ///
998 /// # Returns
999 ///
1000 /// - `None`: No more data to process, finish computation
1001 ///
1002 /// - `Some(lower, None)`: Indicates to call `push_leaf` on all keys starting at `lower`, with
1003 /// no upper bound. This method won't be called again after this.
1004 ///
1005 /// - `Some(lower, Some(upper))`: Indicates to call `push_leaf` on all keys starting at `lower`,
1006 /// up to but excluding `upper`, and then call this method once done.
1007 ///
1008 /// Once returned the `branch_stack` will be in the correct state to start calculating leaves
1009 /// for the given range, if any.
1010 #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
1011 fn next_uncached_key_range<'a>(
1012 &mut self,
1013 targets: &mut Option<TargetsCursor<'a>>,
1014 trie_cursor_state: &mut TrieCursorState,
1015 sub_trie_prefix: &Nibbles,
1016 sub_trie_upper_bound: Option<&Nibbles>,
1017 mut uncalculated_lower_bound: Option<Nibbles>,
1018 ) -> Result<Option<(Nibbles, Option<Nibbles>)>, StateProofError> {
1019 loop {
1020 // Pop the currently cached branch node.
1021 //
1022 // NOTE we pop off the `cached_branch_stack` because cloning the `BranchNodeCompact`
1023 // means cloning an Arc, which incurs synchronization overhead. We have to be sure to
1024 // push the cached branch back onto the stack once done.
1025 let (cached_path, cached_branch) = match self.try_pop_cached_branch(
1026 trie_cursor_state,
1027 sub_trie_prefix,
1028 &uncalculated_lower_bound,
1029 )? {
1030 PopCachedBranchOutcome::Popped(cached) => cached,
1031 PopCachedBranchOutcome::Exhausted => {
1032 // If cached branches are exhausted it's possible that there is still an
1033 // unbounded range of leaves to be processed. `uncalculated_lower_bound` is
1034 // used to return that range.
1035 trace!(target: TRACE_TARGET, ?uncalculated_lower_bound, "Exhausted cached trie nodes");
1036 if let Some(lower) = uncalculated_lower_bound {
1037 self.commit_branches(targets, &lower)?;
1038 return Ok(Some((lower, sub_trie_upper_bound.copied())));
1039 }
1040 return Ok(None)
1041 }
1042 PopCachedBranchOutcome::CalculateLeaves(range) => {
1043 self.commit_branches(targets, &range.0)?;
1044 return Ok(Some(range));
1045 }
1046 };
1047
1048 let uncalculated_lower_bound_ref = uncalculated_lower_bound
1049 .as_ref()
1050 .expect("try_pop_cached_branch would return Exhausted if this were None");
1051
1052 trace!(
1053 target: TRACE_TARGET,
1054 branch_path = ?self.branch_path,
1055 branch_state_mask = ?self.branch_stack.last().map(|b| b.state_mask),
1056 ?cached_path,
1057 cached_branch_state_mask = ?cached_branch.state_mask,
1058 cached_branch_hash_mask = ?cached_branch.hash_mask,
1059 "loop",
1060 );
1061
1062 self.commit_branches(targets, &cached_path)?;
1063
1064 // Since we've popped all branches which don't start with cached_path, branch_path at
1065 // this point must be equal to or shorter than cached_path.
1066 debug_assert!(
1067 self.branch_path.len() < cached_path.len() || self.branch_path == cached_path,
1068 "branch_path {:?} is different-or-longer-than cached_path {cached_path:?}",
1069 self.branch_path
1070 );
1071
1072 // If the branch_path != cached_path it means the branch_stack is either empty, or the
1073 // top branch is the parent of this cached branch. Either way we push a branch
1074 // corresponding to the cached one onto the stack, so we can begin constructing it.
1075 if self.branch_path != cached_path {
1076 self.push_cached_branch(targets, cached_path, &cached_branch)?;
1077 }
1078
1079 // At this point the top of the branch stack is the same branch which was found in the
1080 // cache.
1081 let curr_branch =
1082 self.branch_stack.last().expect("top of branch_stack corresponds to cached branch");
1083
1084 let cached_state_mask = cached_branch.state_mask;
1085 let curr_state_mask = curr_branch.state_mask;
1086
1087 // Determine all child nibbles which are set in the cached branch but not the
1088 // under-construction branch.
1089 let mut next_child_nibbles = curr_state_mask ^ cached_state_mask;
1090
1091 // Also include child nibbles indicated by the prefix set. The prefix set can
1092 // indicate children that need recalculation from leaves (e.g. new keys inserted
1093 // under this branch). Skip nibbles already set in `curr_state_mask` since those
1094 // children have already been constructed.
1095 if self.prefix_set.contains(&self.branch_path) {
1096 let branch_path_len = self.branch_path.len();
1097 let mut child_path = self.branch_path;
1098 for nibble in 0u8..16 {
1099 if !curr_state_mask.is_bit_set(nibble) {
1100 child_path.truncate(branch_path_len);
1101 child_path.push_unchecked(nibble);
1102 if self.prefix_set.contains(&child_path) {
1103 next_child_nibbles.set_bit(nibble);
1104 }
1105 }
1106 }
1107 }
1108
1109 let _orig_next_child_nibbles = next_child_nibbles;
1110
1111 // Mask out any child nibbles whose ranges have already been fully processed.
1112 // This can happen when `calculate_key_range` finds no keys for a child's range,
1113 // leaving the child's bit unset in `state_mask`. Without this, re-entering this
1114 // function would select the same child again.
1115 if uncalculated_lower_bound_ref.starts_with(&self.branch_path) &&
1116 uncalculated_lower_bound_ref.len() > self.branch_path.len()
1117 {
1118 let lower_nibble =
1119 uncalculated_lower_bound_ref.get_unchecked(self.branch_path.len());
1120 // Clear all nibbles strictly below `lower_nibble` since they've been processed.
1121 let already_processed_mask = TrieMask::new((1u16 << lower_nibble) - 1);
1122 next_child_nibbles &= !already_processed_mask;
1123 trace!(
1124 target: TRACE_TARGET,
1125 branch_path = ?self.branch_path,
1126 ?_orig_next_child_nibbles,
1127 ?already_processed_mask,
1128 ?next_child_nibbles,
1129 "Unset already processed key nibbles from next_child_nibbles",
1130 );
1131 } else if !uncalculated_lower_bound_ref.starts_with(&self.branch_path) &&
1132 uncalculated_lower_bound_ref > &self.branch_path
1133 {
1134 // The lower bound has moved entirely past this branch (e.g. branch is 0x6 but
1135 // lower is 0x7). All remaining children have been processed.
1136 next_child_nibbles = TrieMask::default();
1137 trace!(
1138 target: TRACE_TARGET,
1139 branch_path = ?self.branch_path,
1140 ?_orig_next_child_nibbles,
1141 ?next_child_nibbles,
1142 "Unset all nibbles from next_child_nibbles due to branch_path being outside this subtrie",
1143 );
1144 }
1145
1146 // If there are no further children to construct for this branch then pop it off both
1147 // stacks and loop using the parent branch.
1148 if next_child_nibbles.is_empty() {
1149 trace!(
1150 target: TRACE_TARGET,
1151 path=?cached_path,
1152 ?curr_branch,
1153 ?cached_branch,
1154 "No further children, popping branch",
1155 );
1156 self.pop_branch(targets)?;
1157
1158 // no need to pop from `cached_branch_stack`, the current cached branch is already
1159 // popped (see note at the top of the loop).
1160
1161 // The just-popped branch is completely processed; we know there can be no more keys
1162 // with that prefix. Set the lower bound which can be returned from this method to
1163 // be the next possible prefix, if any.
1164 uncalculated_lower_bound = cached_path.next_without_prefix();
1165
1166 continue
1167 }
1168
1169 // Determine the next nibble of the branch which has not yet been constructed, and
1170 // determine the child's full path.
1171 let child_nibble = next_child_nibbles.trailing_zeros() as u8;
1172 let child_path = self.child_path_at(child_nibble);
1173
1174 // If the previous child was a cached branch with a short key (extension), then the new
1175 // uncalculated_lower_bound will be the increment of that branch's path. If there are
1176 // any dirty leaves between that path and this child, it indicates there may be leaves
1177 // which would split that extension node. In that case we return the range to process
1178 // the leaves.
1179 if uncalculated_lower_bound_ref < &child_path &&
1180 self.prefix_set.contains_range(uncalculated_lower_bound_ref..&child_path)
1181 {
1182 self.cached_branch_stack.push((cached_path, cached_branch));
1183 return Ok(Some((*uncalculated_lower_bound_ref, Some(child_path))));
1184 }
1185
1186 // If the `hash_mask` bit is set for the next child it means the child's hash is cached
1187 // in the `cached_branch`. We can use that instead of re-calculating the hash of the
1188 // entire sub-trie.
1189 //
1190 // If the child needs to be retained for a proof then we should not use the cached
1191 // hash, and instead continue on to calculate its node manually.
1192 //
1193 // If the child's path is in the prefix set then the cached hash is stale and must
1194 // not be used.
1195 if cached_branch.hash_mask.is_bit_set(child_nibble) &&
1196 !self.prefix_set.contains(&child_path)
1197 {
1198 // Commit the last child. We do this here for two reasons:
1199 // - `commit_last_child` will check if the last child needs to be retained. We need
1200 // to check that before the subsequent `should_retain` call here to prevent
1201 // `targets` from being moved beyond the last child before it is checked.
1202 // - If we do end up using the cached hash value, then we will need to commit the
1203 // last child before pushing a new one onto the stack anyway.
1204 self.commit_last_child(targets)?;
1205
1206 if !self.should_retain(targets, &child_path, false) {
1207 // Pull this child's hash out of the cached branch node. The hash index
1208 // is the number of hash_mask bits set below this child's nibble.
1209 let lower_bits = TrieMask::new((1u16 << child_nibble) - 1);
1210 let hash_idx = (cached_branch.hash_mask & lower_bits).count_ones() as usize;
1211 let hash = cached_branch.hashes[hash_idx];
1212
1213 trace!(
1214 target: TRACE_TARGET,
1215 ?child_path,
1216 ?hash_idx,
1217 ?hash,
1218 "Using cached hash for child",
1219 );
1220
1221 self.child_stack.push(ProofTrieBranchChild::RlpNode(RlpNode::word_rlp(&hash)));
1222 self.branch_stack
1223 .last_mut()
1224 .expect("already asserted there is a last branch")
1225 .state_mask
1226 .set_bit(child_nibble);
1227
1228 // Update the `uncalculated_lower_bound` to indicate that the child whose bit
1229 // was just set is completely processed.
1230 uncalculated_lower_bound = child_path.next_without_prefix();
1231
1232 // Push the current cached branch back onto the stack before looping.
1233 self.cached_branch_stack.push((cached_path, cached_branch));
1234
1235 continue
1236 }
1237 }
1238
1239 // We now want to check if there is a cached branch node at this child. The cached
1240 // branch node may be the node at this child directly, or this child may be an
1241 // extension and the cached branch is the child of that extension.
1242
1243 // All trie nodes prior to `child_path` will not be modified further, so we can seek the
1244 // trie cursor to the next cached node at-or-after `child_path`.
1245 if trie_cursor_state.path().is_some_and(|path| path < &child_path) {
1246 trace!(target: TRACE_TARGET, ?child_path, "Seeking trie cursor to child path");
1247 *trie_cursor_state = TrieCursorState::seeked(self.trie_cursor_seek(child_path)?);
1248 }
1249
1250 // If the next cached branch node is a child of `child_path` then we can assume it is
1251 // the cached branch for this child. We push it onto the `cached_branch_stack` and loop
1252 // back to the top.
1253 if let TrieCursorState::Available(next_cached_path, next_cached_branch) =
1254 &trie_cursor_state &&
1255 next_cached_path.starts_with(&child_path)
1256 {
1257 // Push the current cached branch back on before pushing its child and then looping
1258 self.cached_branch_stack.push((cached_path, cached_branch));
1259
1260 // If the next cached branch's path is in the prefix set, it could indicate that
1261 // there are dirty leaves which would split the cached branch's extension node (if
1262 // there is one). In that case we return the range those leaves would potentially be
1263 // in to calculate them.
1264 if self.prefix_set.contains(&child_path) {
1265 let gap_upper = Some(*next_cached_path);
1266 self.cached_branch_stack.push(trie_cursor_state.take());
1267 return Ok(Some((*uncalculated_lower_bound_ref, gap_upper)));
1268 }
1269
1270 trace!(
1271 target: TRACE_TARGET,
1272 ?child_path,
1273 ?next_cached_path,
1274 ?next_cached_branch,
1275 "Pushing cached branch for child",
1276 );
1277 self.cached_branch_stack.push(trie_cursor_state.take());
1278 continue;
1279 }
1280
1281 // There is no cached data for the sub-trie at this child, we must recalculate the
1282 // sub-trie root (this child) using the leaves. Return the range of keys based on the
1283 // child path.
1284 let child_path_upper = child_path.next_without_prefix();
1285 trace!(
1286 target: TRACE_TARGET,
1287 lower=?child_path,
1288 upper=?child_path_upper,
1289 "Returning sub-trie's key range to calculate",
1290 );
1291
1292 // Push the current cached branch back onto the stack before returning.
1293 self.cached_branch_stack.push((cached_path, cached_branch));
1294
1295 return Ok(Some((child_path, child_path_upper)));
1296 }
1297 }
1298
1299 /// Calculates trie nodes and retains proofs for targeted nodes within a sub-trie. The
1300 /// sub-trie's bounds are denoted by the `lower_bound` and `upper_bound` arguments,
1301 /// `upper_bound` is exclusive, None indicates unbounded.
1302 #[instrument(
1303 target = TRACE_TARGET,
1304 level = "trace",
1305 skip_all,
1306 fields(prefix=?sub_trie_targets.prefix),
1307 )]
1308 fn proof_subtrie<'a>(
1309 &mut self,
1310 value_encoder: &mut VE,
1311 trie_cursor_state: &mut TrieCursorState,
1312 hashed_cursor_current: &mut Option<(Nibbles, VE::DeferredEncoder)>,
1313 sub_trie_targets: SubTrieTargets<'a>,
1314 ) -> Result<(), StateProofError> {
1315 let sub_trie_upper_bound = sub_trie_targets.upper_bound();
1316
1317 // Wrap targets into a `TargetsCursor`. targets can be empty if we only want to calculate
1318 // the root, in which case we don't need a cursor.
1319 let mut targets = if sub_trie_targets.targets.is_empty() {
1320 None
1321 } else {
1322 Some(TargetsCursor::new(sub_trie_targets.targets))
1323 };
1324
1325 // Ensure initial state is cleared. By the end of the method call these should be empty once
1326 // again.
1327 debug_assert!(self.cached_branch_stack.is_empty());
1328 debug_assert!(self.branch_stack.is_empty());
1329 debug_assert!(self.branch_path.is_empty());
1330 debug_assert!(self.child_stack.is_empty());
1331
1332 // `next_uncached_key_range`, which will be called in the loop below, expects the trie
1333 // cursor to have already been seeked. If it's not yet seeked, or seeked to a prior node,
1334 // then we seek it to the prefix (the first possible node) to initialize it.
1335 if trie_cursor_state.before(&sub_trie_targets.prefix) {
1336 trace!(target: TRACE_TARGET, "Doing initial seek of trie cursor");
1337 *trie_cursor_state =
1338 TrieCursorState::seeked(self.trie_cursor_seek(sub_trie_targets.prefix)?);
1339 }
1340
1341 // `uncalculated_lower_bound` tracks the lower bound of node paths which have yet to be
1342 // visited, either via the hashed key cursor (`calculate_key_range`) or trie cursor
1343 // (`next_uncached_key_range`). If/when this becomes None then there are no further nodes
1344 // which could exist.
1345 let mut uncalculated_lower_bound = Some(sub_trie_targets.prefix);
1346
1347 trace!(target: TRACE_TARGET, "Starting loop");
1348 loop {
1349 // Save the previous lower bound to detect forward progress.
1350 let prev_uncalculated_lower_bound = uncalculated_lower_bound;
1351
1352 // Determine the range of keys of the overall trie which need to be re-computed.
1353 let Some((calc_lower_bound, calc_upper_bound)) = self.next_uncached_key_range(
1354 &mut targets,
1355 trie_cursor_state,
1356 &sub_trie_targets.prefix,
1357 sub_trie_upper_bound.as_ref(),
1358 prev_uncalculated_lower_bound,
1359 )?
1360 else {
1361 // If `next_uncached_key_range` determines that there can be no more keys then
1362 // complete the computation.
1363 break;
1364 };
1365
1366 // Forward-progress guard: detect trie inconsistencies that would cause infinite loops.
1367 // If `next_uncached_key_range` returns a range that starts before the previous
1368 // lower bound, we've gone backwards and would loop forever.
1369 //
1370 // This can specifically happen when there is a cached branch which shouldn't exist, or
1371 // if state mask bit is set on a cached branch which shouldn't be.
1372 if let Some(prev_lower) = prev_uncalculated_lower_bound.as_ref() &&
1373 calc_lower_bound < *prev_lower
1374 {
1375 let msg = format!(
1376 "next_uncached_key_range went backwards: calc_lower={calc_lower_bound:?} < \
1377 prev_lower={prev_lower:?}, calc_upper={calc_upper_bound:?}, prefix={:?}",
1378 sub_trie_targets.prefix,
1379 );
1380 error!(target: TRACE_TARGET, "{msg}");
1381 return Err(StateProofError::TrieInconsistency(msg));
1382 }
1383
1384 // Calculate the trie for that range of keys
1385 self.calculate_key_range(
1386 value_encoder,
1387 &mut targets,
1388 hashed_cursor_current,
1389 calc_lower_bound,
1390 calc_upper_bound,
1391 )?;
1392
1393 // Once outside `calculate_key_range`, `hashed_cursor_current` will be at the first key
1394 // after the range.
1395 //
1396 // If the `hashed_cursor_current` is None (exhausted), or not within the range of the
1397 // sub-trie, then there are no more keys at all, meaning the trie couldn't possibly have
1398 // more data and we should complete computation.
1399 if hashed_cursor_current
1400 .as_ref()
1401 .is_none_or(|(key, _)| !key.starts_with(&sub_trie_targets.prefix))
1402 {
1403 break;
1404 }
1405
1406 // The upper bound of previous calculation becomes the lower bound of the uncalculated
1407 // range, for which we'll once again check for cached data.
1408 uncalculated_lower_bound = calc_upper_bound;
1409 }
1410
1411 // Once there's no more leaves we can pop the remaining branches, if any.
1412 trace!(target: TRACE_TARGET, "Exited loop, popping remaining branches");
1413 while !self.branch_stack.is_empty() {
1414 self.pop_branch(&mut targets)?;
1415 }
1416
1417 // At this point the branch stack should be empty. If the child stack is empty it means no
1418 // keys were ever iterated from the hashed cursor in the first place. Otherwise there should
1419 // only be a single node left: the root node.
1420 debug_assert!(self.branch_stack.is_empty());
1421 debug_assert!(self.branch_path.is_empty());
1422 debug_assert!(self.child_stack.len() < 2);
1423
1424 // The `cached_branch_stack` may still have cached branches on it, as it's not affected by
1425 // `pop_branch`, but it is no longer needed and should be cleared.
1426 self.cached_branch_stack.clear();
1427
1428 // We always pop the root node off of the `child_stack` in order to empty it, however we
1429 // might not want to retain the node unless the `SubTrieTargets` indicates it.
1430 trace!(
1431 target: TRACE_TARGET,
1432 retain_root = ?sub_trie_targets.retain_root,
1433 child_stack_empty = self.child_stack.is_empty(),
1434 "Maybe retaining root",
1435 );
1436 match (sub_trie_targets.retain_root, self.child_stack.pop()) {
1437 (false, _) => {
1438 // Whether the root node is exists or not, we don't want it.
1439 }
1440 (true, None) => {
1441 // If `child_stack` is empty it means there was no keys at all, retain an empty
1442 // root node.
1443 self.retained_proofs.push(ProofTrieNodeV2 {
1444 path: Nibbles::new(), // root path
1445 node: TrieNodeV2::EmptyRoot,
1446 masks: None,
1447 });
1448 }
1449 (true, Some(root_node)) => {
1450 // Encode and retain the root node.
1451 self.rlp_encode_buf.clear();
1452 let root_node =
1453 root_node.into_proof_trie_node(Nibbles::new(), &mut self.rlp_encode_buf)?;
1454 self.retained_proofs.push(root_node);
1455 }
1456 }
1457
1458 Ok(())
1459 }
1460
1461 /// Clears internal computation state. Called after errors to ensure the calculator is not
1462 /// left in a partially-computed state when reused.
1463 fn clear_computation_state(&mut self) {
1464 self.branch_stack.clear();
1465 self.branch_path = Nibbles::new();
1466 self.child_stack.clear();
1467 self.cached_branch_stack.clear();
1468 self.retained_proofs.clear();
1469 }
1470
1471 /// Internal implementation of proof calculation. Assumes both cursors have already been reset.
1472 /// See docs on [`Self::proof`] for expected behavior.
1473 fn proof_inner(
1474 &mut self,
1475 value_encoder: &mut VE,
1476 targets: &mut [ProofV2Target],
1477 ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1478 // If there are no targets then nothing could be returned, return early.
1479 if targets.is_empty() {
1480 trace!(target: TRACE_TARGET, "Empty targets, returning");
1481 return Ok(Vec::new())
1482 }
1483
1484 // Initialize the variables which track the state of the two cursors. Both indicate the
1485 // cursors are unseeked.
1486 let mut trie_cursor_state = TrieCursorState::unseeked();
1487 let mut hashed_cursor_current: Option<(Nibbles, VE::DeferredEncoder)> = None;
1488
1489 // Divide targets into chunks, each chunk corresponding to a different sub-trie within the
1490 // overall trie, and handle all proofs within that sub-trie.
1491 for sub_trie_targets in iter_sub_trie_targets(targets) {
1492 if let Err(err) = self.proof_subtrie(
1493 value_encoder,
1494 &mut trie_cursor_state,
1495 &mut hashed_cursor_current,
1496 sub_trie_targets,
1497 ) {
1498 self.clear_computation_state();
1499 return Err(err);
1500 }
1501 }
1502
1503 trace!(
1504 target: TRACE_TARGET,
1505 retained_proofs_len = ?self.retained_proofs.len(),
1506 "proof_inner: returning",
1507 );
1508 Ok(core::mem::take(&mut self.retained_proofs))
1509 }
1510
1511 /// Generate a proof for the given targets.
1512 ///
1513 /// Given a set of [`ProofV2Target`]s, returns nodes whose paths are a prefix of any target. The
1514 /// returned nodes will be sorted depth-first by path.
1515 ///
1516 /// # Panics
1517 ///
1518 /// In debug builds, panics if the targets are not sorted lexicographically.
1519 #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
1520 pub fn proof(
1521 &mut self,
1522 value_encoder: &mut VE,
1523 targets: &mut [ProofV2Target],
1524 ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1525 self.trie_cursor.reset();
1526 self.hashed_cursor.reset();
1527 self.proof_inner(value_encoder, targets)
1528 }
1529
1530 /// Computes the root hash from a set of proof nodes.
1531 ///
1532 /// Returns `None` if there is no root node (partial proof), otherwise returns the hash of the
1533 /// root node.
1534 ///
1535 /// This method reuses the internal RLP encode buffer for efficiency.
1536 pub fn compute_root_hash(
1537 &mut self,
1538 proof_nodes: &[ProofTrieNodeV2],
1539 ) -> Result<Option<B256>, StateProofError> {
1540 // Find the root node (node at empty path)
1541 let root_node = proof_nodes.iter().find(|node| node.path.is_empty());
1542
1543 let Some(root) = root_node else {
1544 return Ok(None);
1545 };
1546
1547 // Compute the hash of the root node
1548 self.rlp_encode_buf.clear();
1549 root.node.encode(&mut self.rlp_encode_buf);
1550 let root_hash = keccak256(&self.rlp_encode_buf);
1551
1552 Ok(Some(root_hash))
1553 }
1554
1555 /// Calculates the root node of the trie.
1556 ///
1557 /// This method does not accept targets nor retain proofs. Returns the root node which can
1558 /// be used to compute the root hash via [`Self::compute_root_hash`].
1559 #[instrument(target = TRACE_TARGET, level = "trace", skip(self, value_encoder))]
1560 pub fn root_node(
1561 &mut self,
1562 value_encoder: &mut VE,
1563 ) -> Result<ProofTrieNodeV2, StateProofError> {
1564 // Initialize the variables which track the state of the two cursors. Both indicate the
1565 // cursors are unseeked.
1566 let mut trie_cursor_state = TrieCursorState::unseeked();
1567 let mut hashed_cursor_current: Option<(Nibbles, VE::DeferredEncoder)> = None;
1568
1569 static EMPTY_TARGETS: [ProofV2Target; 0] = [];
1570 let sub_trie_targets =
1571 SubTrieTargets { prefix: Nibbles::new(), targets: &EMPTY_TARGETS, retain_root: true };
1572
1573 if let Err(err) = self.proof_subtrie(
1574 value_encoder,
1575 &mut trie_cursor_state,
1576 &mut hashed_cursor_current,
1577 sub_trie_targets,
1578 ) {
1579 self.clear_computation_state();
1580 return Err(err);
1581 }
1582
1583 // proof_subtrie will retain the root node if retain_proof is true, regardless of if there
1584 // are any targets.
1585 let mut proofs = core::mem::take(&mut self.retained_proofs);
1586 trace!(
1587 target: TRACE_TARGET,
1588 proofs_len = ?proofs.len(),
1589 "root_node: extracting root",
1590 );
1591
1592 // The root node is at the empty path - it must exist since retain_root is true. Otherwise
1593 // targets was empty, so there should be no other retained proofs.
1594 debug_assert_eq!(
1595 proofs.len(), 1,
1596 "prefix is empty, retain_root is true, and targets is empty, so there must be only the root node"
1597 );
1598
1599 // Find and remove the root node (node at empty path)
1600 let root_node = proofs.pop().expect("prefix is empty, retain_root is true, and targets is empty, so there must be only the root node");
1601
1602 Ok(root_node)
1603 }
1604}
1605
1606/// A proof calculator for storage tries.
1607pub type StorageProofCalculator<TC, HC> = ProofCalculator<TC, HC, StorageValueEncoder>;
1608
1609impl<TC, HC> StorageProofCalculator<TC, HC>
1610where
1611 TC: TrieStorageCursor,
1612 HC: HashedStorageCursor<Value = U256>,
1613{
1614 /// Create a new [`StorageProofCalculator`] instance.
1615 pub fn new_storage(trie_cursor: TC, hashed_cursor: HC) -> Self {
1616 Self::new(trie_cursor, hashed_cursor)
1617 }
1618
1619 /// Generate a proof for a storage trie at the given hashed address.
1620 ///
1621 /// Given a set of [`ProofV2Target`]s, returns nodes whose paths are a prefix of any target. The
1622 /// returned nodes will be sorted depth-first by path.
1623 ///
1624 /// # Panics
1625 ///
1626 /// In debug builds, panics if the targets are not sorted lexicographically.
1627 #[instrument(target = TRACE_TARGET, level = "trace", skip(self, targets))]
1628 pub fn storage_proof(
1629 &mut self,
1630 hashed_address: B256,
1631 targets: &mut [ProofV2Target],
1632 ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1633 self.hashed_cursor.set_hashed_address(hashed_address);
1634
1635 // Shortcut: check if storage is empty
1636 if self.hashed_cursor.is_storage_empty()? {
1637 // Return a single EmptyRoot node at the root path
1638 return Ok(vec![ProofTrieNodeV2 {
1639 path: Nibbles::default(),
1640 node: TrieNodeV2::EmptyRoot,
1641 masks: None,
1642 }])
1643 }
1644
1645 // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has
1646 // been checked.
1647 self.trie_cursor.set_hashed_address(hashed_address);
1648
1649 // Create a mutable storage value encoder
1650 let mut storage_value_encoder = StorageValueEncoder;
1651 self.proof_inner(&mut storage_value_encoder, targets)
1652 }
1653
1654 /// Calculates the root node of a storage trie.
1655 ///
1656 /// This method does not accept targets nor retain proofs. Returns the root node which can
1657 /// be used to compute the root hash via [`Self::compute_root_hash`].
1658 #[instrument(target = TRACE_TARGET, level = "trace", skip(self))]
1659 pub fn storage_root_node(
1660 &mut self,
1661 hashed_address: B256,
1662 ) -> Result<ProofTrieNodeV2, StateProofError> {
1663 self.hashed_cursor.set_hashed_address(hashed_address);
1664
1665 if self.hashed_cursor.is_storage_empty()? {
1666 return Ok(ProofTrieNodeV2 {
1667 path: Nibbles::default(),
1668 node: TrieNodeV2::EmptyRoot,
1669 masks: None,
1670 })
1671 }
1672
1673 // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has
1674 // been checked.
1675 self.trie_cursor.set_hashed_address(hashed_address);
1676
1677 // Create a mutable storage value encoder
1678 let mut storage_value_encoder = StorageValueEncoder;
1679 self.root_node(&mut storage_value_encoder)
1680 }
1681}
1682
1683/// Helper type wrapping a slice of [`ProofV2Target`]s, primarily used to iterate through targets in
1684/// [`ProofCalculator::should_retain`].
1685///
1686/// It is assumed that the underlying slice is never empty, and that the iterator is never
1687/// exhausted.
1688struct TargetsCursor<'a> {
1689 targets: &'a [ProofV2Target],
1690 i: usize,
1691}
1692
1693impl<'a> TargetsCursor<'a> {
1694 /// Wraps a slice of [`ProofV2Target`]s with the `TargetsCursor`.
1695 ///
1696 /// # Panics
1697 ///
1698 /// Will panic in debug mode if called with an empty slice.
1699 fn new(targets: &'a [ProofV2Target]) -> Self {
1700 debug_assert!(!targets.is_empty());
1701 Self { targets, i: 0 }
1702 }
1703
1704 /// Returns the current and next [`ProofV2Target`] that the cursor is pointed at.
1705 fn current(&self) -> (&'a ProofV2Target, Option<&'a ProofV2Target>) {
1706 (&self.targets[self.i], self.targets.get(self.i + 1))
1707 }
1708
1709 /// Iterates the cursor forward.
1710 ///
1711 /// # Panics
1712 ///
1713 /// Will panic if the cursor is exhausted.
1714 fn next(&mut self) -> (&'a ProofV2Target, Option<&'a ProofV2Target>) {
1715 self.i += 1;
1716 debug_assert!(self.i < self.targets.len());
1717 self.current()
1718 }
1719
1720 // Iterate forwards over the slice, starting from the [`ProofV2Target`] after the current.
1721 fn skip_iter(&self) -> impl Iterator<Item = &'a ProofV2Target> {
1722 self.targets[self.i + 1..].iter()
1723 }
1724
1725 /// Iterated backwards over the slice, starting from the [`ProofV2Target`] previous to the
1726 /// current.
1727 fn rev_iter(&self) -> impl Iterator<Item = &'a ProofV2Target> {
1728 self.targets[..self.i].iter().rev()
1729 }
1730}
1731
1732/// Used to track the state of the trie cursor, allowing us to differentiate between a branch having
1733/// been taken (used as a cached branch) and the cursor having been exhausted.
1734#[derive(Debug)]
1735enum TrieCursorState {
1736 /// The initial state of the cursor, indicating it's never been seeked.
1737 Unseeked,
1738 /// Cursor is seeked to this path and the node has not been used yet.
1739 Available(Nibbles, BranchNodeCompact),
1740 /// Cursor is seeked to this path, but the node has been used.
1741 Taken(Nibbles),
1742 /// Cursor has been exhausted.
1743 Exhausted,
1744}
1745
1746impl TrieCursorState {
1747 /// Creates a [`Self::Unseeked`] based on an entry returned from the cursor itself.
1748 const fn unseeked() -> Self {
1749 Self::Unseeked
1750 }
1751
1752 /// Creates a [`Self`] based on an entry returned from the cursor itself.
1753 fn seeked(entry: Option<(Nibbles, BranchNodeCompact)>) -> Self {
1754 entry.map_or(Self::Exhausted, |(path, node)| Self::Available(path, node))
1755 }
1756
1757 /// Returns the path the cursor is seeked to, or None if it's exhausted.
1758 ///
1759 /// # Panics
1760 ///
1761 /// Panics if the cursor is unseeked.
1762 const fn path(&self) -> Option<&Nibbles> {
1763 match self {
1764 Self::Unseeked => panic!("cursor is unseeked"),
1765 Self::Available(path, _) | Self::Taken(path) => Some(path),
1766 Self::Exhausted => None,
1767 }
1768 }
1769
1770 /// Returns true if the cursor is unseeked, or is seeked to a node prior to the given one.
1771 fn before(&self, path: &Nibbles) -> bool {
1772 match self {
1773 Self::Unseeked => true,
1774 Self::Available(seeked_to, _) | Self::Taken(seeked_to) => path < seeked_to,
1775 Self::Exhausted => false,
1776 }
1777 }
1778
1779 /// Takes the path and node from a [`Self::Available`]. Panics if not [`Self::Available`].
1780 fn take(&mut self) -> (Nibbles, BranchNodeCompact) {
1781 let Self::Available(path, _) = self else {
1782 panic!("take called on non-Available: {self:?}")
1783 };
1784
1785 let path = *path;
1786 let Self::Available(path, node) = core::mem::replace(self, Self::Taken(path)) else {
1787 unreachable!("already checked that self is Self::Available");
1788 };
1789
1790 (path, node)
1791 }
1792}
1793
1794/// Describes the state of the currently cached branch node (if any).
1795enum PopCachedBranchOutcome {
1796 /// Cached branch has been popped from the `cached_branch_stack` and is ready to be used.
1797 Popped((Nibbles, BranchNodeCompact)),
1798 /// All cached branches have been exhausted.
1799 Exhausted,
1800 /// Need to calculate leaves from this range (exclusive upper) before the cached branch
1801 /// (catch-up range). If None then
1802 CalculateLeaves((Nibbles, Option<Nibbles>)),
1803}
1804
1805#[cfg(test)]
1806mod tests {
1807 use super::*;
1808 use crate::{
1809 hashed_cursor::{mock::MockHashedCursorFactory, HashedCursorFactory},
1810 proof::StorageProof as LegacyStorageProof,
1811 test_utils::TrieTestHarness,
1812 trie_cursor::{depth_first, TrieCursorFactory},
1813 };
1814 use alloy_primitives::map::B256Set;
1815 use alloy_rlp::Decodable;
1816 use alloy_trie::proof::AddedRemovedKeys;
1817 use itertools::Itertools;
1818 use reth_trie_common::{prefix_set::PrefixSetMut, ProofTrieNode, TrieNode};
1819 use std::collections::BTreeMap;
1820
1821 /// Converts legacy proofs to V2 proofs by combining extension nodes with their child branch
1822 /// nodes.
1823 ///
1824 /// In the legacy proof format, extension nodes and branch nodes are separate. In the V2 format,
1825 /// they are combined into a single `BranchNodeV2` where the extension's key becomes the
1826 /// branch's `key` field.
1827 ///
1828 /// Converts legacy proofs (sorted in depth-first order) to V2 format.
1829 ///
1830 /// In depth-first order, children come BEFORE parents. So when we encounter an extension node,
1831 /// its child branch has already been processed and is in the result. We need to pop it and
1832 /// combine it with the extension.
1833 fn convert_legacy_proofs_to_v2(legacy_proofs: &[ProofTrieNode]) -> Vec<ProofTrieNodeV2> {
1834 ProofTrieNodeV2::from_sorted_trie_nodes(
1835 legacy_proofs.iter().map(|p| (p.path, p.node.clone(), p.masks)),
1836 )
1837 }
1838
1839 /// A test harness for comparing `StorageProofCalculator` and legacy `StorageProof`
1840 /// implementations.
1841 ///
1842 /// Wraps [`TrieTestHarness`] and adds a method to test that both proof implementations
1843 /// produce equivalent results for storage proofs.
1844 struct ProofTestHarness {
1845 inner: TrieTestHarness,
1846 }
1847
1848 impl std::ops::Deref for ProofTestHarness {
1849 type Target = TrieTestHarness;
1850 fn deref(&self) -> &Self::Target {
1851 &self.inner
1852 }
1853 }
1854
1855 impl ProofTestHarness {
1856 /// Creates a new test harness from a map of hashed storage slots to values.
1857 fn new(storage: BTreeMap<B256, U256>) -> Self {
1858 Self { inner: TrieTestHarness::new(storage) }
1859 }
1860
1861 /// Asserts that `StorageProofCalculator` and legacy `StorageProof` produce equivalent
1862 /// results for storage proofs.
1863 fn assert_proof(
1864 &self,
1865 targets: impl IntoIterator<Item = ProofV2Target>,
1866 ) -> Result<(), StateProofError> {
1867 let mut targets_vec = targets.into_iter().collect::<Vec<_>>();
1868
1869 // Get v2 proof and root hash via harness
1870 let (proof_v2_result, root_hash) = self.proof_v2(&mut targets_vec);
1871
1872 // Verify the root hash matches the expected root (if the proof contains a root
1873 // node)
1874 if let Some(root_hash) = root_hash {
1875 pretty_assertions::assert_eq!(self.original_root(), root_hash);
1876 }
1877
1878 // Convert ProofV2Target keys to B256Set for legacy implementation
1879 let legacy_targets = targets_vec
1880 .iter()
1881 .map(|target| B256::from_slice(&target.key_nibbles.pack()))
1882 .collect::<B256Set>();
1883
1884 // Call legacy StorageProof::storage_multiproof
1885 let proof_legacy_result = LegacyStorageProof::new_hashed(
1886 self.trie_cursor_factory(),
1887 self.hashed_cursor_factory(),
1888 self.hashed_address(),
1889 )
1890 .with_branch_node_masks(true)
1891 .with_added_removed_keys(Some(AddedRemovedKeys::default().with_assume_added(true)))
1892 .storage_multiproof(legacy_targets)?;
1893
1894 // Helper function to check if a node path matches at least one target
1895 let node_matches_target = |node_path: &Nibbles| -> bool {
1896 targets_vec.iter().any(|target| {
1897 target.key_nibbles.starts_with(node_path) &&
1898 node_path.len() >= target.min_len as usize
1899 })
1900 };
1901
1902 // Decode and sort legacy proof nodes
1903 let proof_legacy_nodes = proof_legacy_result
1904 .subtree
1905 .iter()
1906 .map(|(path, node_enc)| {
1907 let mut buf = node_enc.as_ref();
1908 let node = TrieNode::decode(&mut buf)
1909 .expect("legacy implementation should not produce malformed proof nodes");
1910
1911 let masks = if path.is_empty() {
1912 None
1913 } else {
1914 proof_legacy_result.branch_node_masks.get(path).copied()
1915 };
1916
1917 ProofTrieNode { path: *path, node, masks }
1918 })
1919 .sorted_by(|a, b| depth_first::cmp(&a.path, &b.path))
1920 .collect::<Vec<_>>();
1921
1922 // Convert legacy proofs to V2 proofs by combining extensions with their child branches
1923 let proof_legacy_nodes_v2 = convert_legacy_proofs_to_v2(&proof_legacy_nodes);
1924
1925 // Filter both results to only keep nodes which match a target. The v2
1926 // storage_proof returns an EmptyRoot node even when there are no targets, so
1927 // both sides need the same filtering.
1928 let proof_legacy_nodes_v2 = proof_legacy_nodes_v2
1929 .into_iter()
1930 .filter(|ProofTrieNodeV2 { path, .. }| node_matches_target(path))
1931 .collect::<Vec<_>>();
1932
1933 let proof_v2_result = proof_v2_result
1934 .into_iter()
1935 .filter(|ProofTrieNodeV2 { path, .. }| node_matches_target(path))
1936 .collect::<Vec<_>>();
1937
1938 pretty_assertions::assert_eq!(proof_legacy_nodes_v2, proof_v2_result);
1939
1940 Ok(())
1941 }
1942 }
1943
1944 /// Tests that `clear_computation_state` properly resets internal stacks, allowing a
1945 /// `StorageProofCalculator` to be reused after a mid-computation error left stale state.
1946 /// Before the fix, stale data in `branch_stack`, `child_stack`, and `branch_path`
1947 /// could cause a `usize` underflow panic in `pop_branch`.
1948 #[test]
1949 fn test_proof_calculator_reuse_after_error() {
1950 reth_tracing::init_test_tracing();
1951
1952 let slots = [
1953 B256::right_padding_from(&[0x10]),
1954 B256::right_padding_from(&[0x20]),
1955 B256::right_padding_from(&[0x30]),
1956 B256::right_padding_from(&[0x40]),
1957 ];
1958 let storage: BTreeMap<B256, U256> =
1959 slots.iter().map(|&s| (s, U256::from(100u64))).collect();
1960
1961 let harness = ProofTestHarness::new(storage);
1962
1963 let trie_cursor_factory = harness.trie_cursor_factory();
1964 let hashed_cursor_factory = harness.hashed_cursor_factory();
1965
1966 let hashed_address = harness.hashed_address();
1967 let trie_cursor = trie_cursor_factory.storage_trie_cursor(hashed_address).unwrap();
1968 let hashed_cursor = hashed_cursor_factory.hashed_storage_cursor(hashed_address).unwrap();
1969 let mut proof_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
1970
1971 // Simulate stale state left by a mid-computation error: push fake entries onto internal
1972 // stacks and set a non-empty branch_path.
1973 proof_calculator.branch_stack.push(ProofTrieBranch {
1974 ext_len: 2,
1975 state_mask: TrieMask::new(0b1111),
1976 masks: None,
1977 });
1978 proof_calculator.branch_stack.push(ProofTrieBranch {
1979 ext_len: 0,
1980 state_mask: TrieMask::new(0b11),
1981 masks: None,
1982 });
1983 proof_calculator
1984 .child_stack
1985 .push(ProofTrieBranchChild::RlpNode(RlpNode::word_rlp(&B256::ZERO)));
1986 proof_calculator.branch_path = Nibbles::from_nibbles([0x1, 0x2, 0x3]);
1987
1988 // clear_computation_state should reset everything so a subsequent call works.
1989 proof_calculator.clear_computation_state();
1990
1991 let mut sorted_slots = slots.to_vec();
1992 sorted_slots.sort();
1993 let mut targets: Vec<ProofV2Target> =
1994 sorted_slots.iter().copied().map(ProofV2Target::new).collect();
1995
1996 let result = proof_calculator.storage_proof(hashed_address, &mut targets).unwrap();
1997
1998 // Compare against a fresh calculator to verify correctness.
1999 let trie_cursor = trie_cursor_factory.storage_trie_cursor(hashed_address).unwrap();
2000 let hashed_cursor = hashed_cursor_factory.hashed_storage_cursor(hashed_address).unwrap();
2001 let mut fresh_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
2002 let fresh_result = fresh_calculator.storage_proof(hashed_address, &mut targets).unwrap();
2003
2004 pretty_assertions::assert_eq!(fresh_result, result);
2005 }
2006
2007 mod proptest_tests {
2008 use super::*;
2009 use proptest::prelude::*;
2010
2011 /// Generate a strategy for storage datasets (hashed slot → value).
2012 fn storage_strategy() -> impl Strategy<Value = BTreeMap<B256, U256>> {
2013 prop::collection::vec((any::<[u8; 32]>(), any::<u64>()), 0..=100).prop_map(|slots| {
2014 slots
2015 .into_iter()
2016 .map(|(slot_bytes, value)| (B256::from(slot_bytes), U256::from(value)))
2017 .filter(|(_, v)| *v != U256::ZERO)
2018 .collect()
2019 })
2020 }
2021
2022 /// Generate a strategy for proof targets that are 80% from existing storage slots
2023 /// and 20% random keys. Each target has a random `min_len` of 0..16.
2024 fn proof_targets_strategy(
2025 slot_keys: Vec<B256>,
2026 ) -> impl Strategy<Value = Vec<ProofV2Target>> {
2027 let num_slots = slot_keys.len();
2028
2029 let target_count = 0..=(num_slots + 5);
2030
2031 target_count.prop_flat_map(move |count| {
2032 let slot_keys = slot_keys.clone();
2033 prop::collection::vec(
2034 (
2035 prop::bool::weighted(0.8).prop_flat_map(move |from_slots| {
2036 if from_slots && !slot_keys.is_empty() {
2037 prop::sample::select(slot_keys.clone()).boxed()
2038 } else {
2039 any::<[u8; 32]>().prop_map(B256::from).boxed()
2040 }
2041 }),
2042 0u8..16u8,
2043 )
2044 .prop_map(|(key, min_len)| ProofV2Target::new(key).with_min_len(min_len)),
2045 count,
2046 )
2047 })
2048 }
2049
2050 proptest! {
2051 #![proptest_config(ProptestConfig::with_cases(4000))]
2052 #[test]
2053 /// Tests that `StorageProofCalculator` produces valid proofs for randomly generated
2054 /// storage datasets with proof targets.
2055 fn proptest_proof_with_targets(
2056 (storage, targets) in storage_strategy()
2057 .prop_flat_map(|storage| {
2058 let mut slot_keys: Vec<B256> = storage.keys().copied().collect();
2059 slot_keys.sort_unstable();
2060 let targets_strategy = proof_targets_strategy(slot_keys);
2061 (Just(storage), targets_strategy)
2062 })
2063 ) {
2064 reth_tracing::init_test_tracing();
2065 let harness = ProofTestHarness::new(storage);
2066
2067 harness.assert_proof(targets).expect("Proof generation failed");
2068 }
2069 }
2070 }
2071
2072 #[test]
2073 fn test_big_trie() {
2074 use rand::prelude::*;
2075
2076 reth_tracing::init_test_tracing();
2077 let mut rng = rand::rngs::SmallRng::seed_from_u64(1);
2078
2079 let mut rand_b256 = || {
2080 let mut buf: [u8; 32] = [0; 32];
2081 rng.fill_bytes(&mut buf);
2082 B256::from_slice(&buf)
2083 };
2084
2085 // Generate random storage dataset.
2086 let mut storage = BTreeMap::new();
2087 for _ in 0..10240 {
2088 let hashed_slot = rand_b256();
2089 storage.insert(hashed_slot, U256::from(1u64));
2090 }
2091
2092 // Collect targets; partially from real keys, partially random keys which probably won't
2093 // exist.
2094 let mut targets = storage.keys().copied().collect::<Vec<_>>();
2095 for _ in 0..storage.len() / 5 {
2096 targets.push(rand_b256());
2097 }
2098 targets.sort();
2099
2100 // Create test harness
2101 let harness = ProofTestHarness::new(storage);
2102
2103 harness
2104 .assert_proof(targets.into_iter().map(ProofV2Target::new))
2105 .expect("Proof generation failed");
2106 }
2107
2108 #[test]
2109 fn test_node_with_masked_empty_child() {
2110 reth_tracing::init_test_tracing();
2111
2112 let val = U256::from(42u64);
2113
2114 // All storage keys share a common first nibble (0x6), so the branch is at path 0x6. The
2115 // second nibble differentiates children: 0,1,3,5,7.
2116 let slot_60 = B256::right_padding_from(&[0x60]);
2117 let slot_61 = B256::right_padding_from(&[0x61]);
2118 let slot_65 = B256::right_padding_from(&[0x65]);
2119 let slot_67 = B256::right_padding_from(&[0x67]);
2120
2121 // Construct a branch node at path 0x6 with state_mask bits 0,1,3,5,7.
2122 // hash_mask has bits 0,1,5,7 (NOT 3) — nibble 3's hash is cleared because it's in the
2123 // prefix set. Hashes are dummy values.
2124 let state_mask = TrieMask::new(0b10101011); // bits 0,1,3,5,7
2125 let hash_mask = TrieMask::new(0b10100011); // bits 0,1,5,7 (NOT 3)
2126 let hashes = vec![B256::repeat_byte(0xaa); hash_mask.count_ones() as usize];
2127 let branch = BranchNodeCompact::new(state_mask, TrieMask::new(0), hash_mask, hashes, None);
2128
2129 let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2130 std::iter::once((Nibbles::from_nibbles([0x6]), branch)).collect();
2131
2132 // Hashed cursor has slots at children 0, 1, 5, 7 — but NOT child 3 (0x63).
2133 // This simulates the post-state overlay having deleted the slot at 0x63.
2134 let mut harness = TrieTestHarness::new(
2135 [slot_60, slot_61, slot_65, slot_67].iter().map(|s| (*s, val)).collect(),
2136 );
2137 harness.set_trie_nodes(storage_nodes);
2138
2139 let storage_trie_cursor =
2140 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2141 let hashed_storage_cursor = harness
2142 .hashed_cursor_factory()
2143 .hashed_storage_cursor(harness.hashed_address())
2144 .unwrap();
2145 let mut calculator =
2146 StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
2147 let root_node = calculator
2148 .storage_root_node(harness.hashed_address())
2149 .expect("storage_root_node should succeed with masked empty child");
2150
2151 let root_hash = calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap();
2152 assert!(root_hash.is_some(), "should produce a root hash");
2153 }
2154
2155 /// Tests that `root_node` handles the case where `uncalculated_lower_bound` has advanced
2156 /// entirely past a cached branch that still has unprocessed children in its `state_mask`.
2157 ///
2158 /// Branch at `0x6` has `state_mask` bits 0,1,5,f where nibble 5 has its `hash_mask`
2159 /// cleared and no leaf data. The last child (nibble f)
2160 /// causes `calculate_key_range` to be called with range `(0x6f, Some(0x7))`. After that range,
2161 /// the hashed cursor still has keys (at `0x70...`), so `proof_subtrie` does not break and
2162 /// re-enters `next_uncached_key_range` with `uncalculated_lower_bound = Some(0x7)`.
2163 /// Since `0x7` is past `0x6`, all remaining children are skipped and the branch is popped.
2164 #[test]
2165 fn test_node_with_masked_empty_child_lower_bound_past_branch() {
2166 reth_tracing::init_test_tracing();
2167
2168 let val = U256::from(42u64);
2169
2170 // Leaf keys under 0x6 and one beyond (0x70) to keep the cursor alive after 0x6.
2171 let slot_60 = B256::right_padding_from(&[0x60]);
2172 let slot_61 = B256::right_padding_from(&[0x61]);
2173 let slot_6f = B256::right_padding_from(&[0x6f]);
2174 let slot_70 = B256::right_padding_from(&[0x70]);
2175
2176 // Branch at 0x6: state_mask bits 0,1,5,f; hash_mask bits 0,1 (NOT 5, NOT f).
2177 // Nibble 5 has state_mask set but no hash and no leaf data (masked empty child).
2178 // Nibble f has state_mask set, no hash, but DOES have leaf data.
2179 let state_mask = TrieMask::new(0b1000_0000_0010_0011); // bits 0,1,5,f
2180 let hash_mask = TrieMask::new(0b0000_0000_0000_0011); // bits 0,1
2181 let hashes = vec![B256::repeat_byte(0xaa); hash_mask.count_ones() as usize];
2182 let branch = BranchNodeCompact::new(state_mask, TrieMask::new(0), hash_mask, hashes, None);
2183
2184 let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2185 std::iter::once((Nibbles::from_nibbles([0x6]), branch)).collect();
2186
2187 // Hashed cursor: slots at 0x60, 0x61, 0x6f, 0x70 — but NOT 0x65.
2188 let mut harness = TrieTestHarness::new(
2189 [slot_60, slot_61, slot_6f, slot_70].iter().map(|s| (*s, val)).collect(),
2190 );
2191 harness.set_trie_nodes(storage_nodes);
2192
2193 let storage_trie_cursor =
2194 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2195 let hashed_storage_cursor = harness
2196 .hashed_cursor_factory()
2197 .hashed_storage_cursor(harness.hashed_address())
2198 .unwrap();
2199 let mut calculator =
2200 StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
2201 let root_node = calculator
2202 .storage_root_node(harness.hashed_address())
2203 .expect("storage_root_node should succeed when lower bound advances past branch");
2204
2205 let root_hash = calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap();
2206 assert!(root_hash.is_some(), "should produce a root hash");
2207 }
2208
2209 /// Tests that the prefix set causes `next_uncached_key_range` to add child nibbles that are
2210 /// not present in the cached branch's `state_mask`.
2211 ///
2212 /// Setup: An original state with leaves at `0x60` and `0x61` produces a cached branch at
2213 /// `0x6` with children at nibbles 0 and 1 (both with real cached hashes from `StorageRoot`).
2214 /// A new leaf is then inserted at `0x63...`, which is NOT in the branch's `state_mask`.
2215 /// The prefix set contains the new key. Without prefix set support, the calculator would
2216 /// skip nibble 3 entirely and produce a stale root hash. With prefix set support, nibble 3
2217 /// is discovered and its subtrie is recalculated from leaves.
2218 #[test]
2219 fn test_prefix_set_adds_child_nibbles() {
2220 reth_tracing::init_test_tracing();
2221
2222 let val = U256::from(42u64);
2223 let slot_60 = B256::right_padding_from(&[0x60]);
2224 let slot_61 = B256::right_padding_from(&[0x61]);
2225 let slot_63 = B256::right_padding_from(&[0x63]);
2226
2227 let harness = TrieTestHarness::new([(slot_60, val), (slot_61, val)].into_iter().collect());
2228
2229 let changeset: BTreeMap<B256, U256> = std::iter::once((slot_63, val)).collect();
2230 let (expected_root, _) = harness.get_root_with_updates(&changeset);
2231
2232 let mut updated_storage = harness.storage().clone();
2233 updated_storage.insert(slot_63, val);
2234
2235 let updated_hashed = MockHashedCursorFactory::new(
2236 BTreeMap::new(),
2237 std::iter::once((harness.hashed_address(), updated_storage)).collect(),
2238 );
2239
2240 let mut prefix_set = PrefixSetMut::default();
2241 prefix_set.insert(Nibbles::unpack(slot_63));
2242
2243 let trie_cursor =
2244 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2245 let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
2246 let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2247 .with_prefix_set(prefix_set.freeze());
2248 let root_node = calculator
2249 .storage_root_node(harness.hashed_address())
2250 .expect("storage_root_node should succeed with prefix set adding child nibbles");
2251 let got_root =
2252 calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2253
2254 pretty_assertions::assert_eq!(
2255 expected_root,
2256 got_root,
2257 "Root hash with prefix set should match fresh computation"
2258 );
2259 }
2260
2261 /// Tests that `next_uncached_key_range` does not use a cached hash when the child's path
2262 /// is in the prefix set, forcing recalculation from leaves.
2263 ///
2264 /// Setup: A cached branch at `0x6` with children at nibbles 0,1,5 — all with cached hashes.
2265 /// The leaf at `0x65...` is changed (different value). The prefix set marks `0x65...` as
2266 /// dirty. Without prefix set support, the calculator would use the stale cached hash for
2267 /// nibble 5 and produce a wrong root. With prefix set support, the cached hash is skipped
2268 /// and the subtrie is recalculated from the updated leaf data.
2269 #[test]
2270 fn test_prefix_set_invalidates_cached_hash() {
2271 reth_tracing::init_test_tracing();
2272
2273 let original_val = U256::from(42u64);
2274 let updated_val = U256::from(9999u64);
2275 let slot_60 = B256::right_padding_from(&[0x60]);
2276 let slot_61 = B256::right_padding_from(&[0x61]);
2277 let slot_65 = B256::right_padding_from(&[0x65]);
2278
2279 let harness = TrieTestHarness::new(
2280 [(slot_60, original_val), (slot_61, original_val), (slot_65, original_val)]
2281 .into_iter()
2282 .collect(),
2283 );
2284
2285 let changeset: BTreeMap<B256, U256> = std::iter::once((slot_65, updated_val)).collect();
2286 let (expected_root, _) = harness.get_root_with_updates(&changeset);
2287
2288 let mut updated_storage = harness.storage().clone();
2289 updated_storage.insert(slot_65, updated_val);
2290
2291 let updated_hashed = MockHashedCursorFactory::new(
2292 BTreeMap::new(),
2293 std::iter::once((harness.hashed_address(), updated_storage)).collect(),
2294 );
2295
2296 let mut prefix_set = PrefixSetMut::default();
2297 prefix_set.insert(Nibbles::unpack(slot_65));
2298
2299 let trie_cursor =
2300 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2301 let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
2302 let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2303 .with_prefix_set(prefix_set.freeze());
2304 let root_node = calculator
2305 .storage_root_node(harness.hashed_address())
2306 .expect("storage_root_node should succeed with prefix set invalidating cached hash");
2307 let got_root =
2308 calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2309
2310 pretty_assertions::assert_eq!(
2311 expected_root,
2312 got_root,
2313 "Root hash with prefix set invalidation should match fresh computation"
2314 );
2315 }
2316
2317 /// Helper to compute the keccak256 hash of a storage leaf node. The `short_key` is the
2318 /// leaf's key after trimming all branch/extension nibbles consumed by ancestor nodes.
2319 fn storage_leaf_hash(short_key: &Nibbles, value: &U256) -> B256 {
2320 let mut buf = Vec::new();
2321 alloy_trie::nodes::LeafNodeRef::new(short_key, &alloy_rlp::encode_fixed_size(value))
2322 .encode(&mut buf);
2323 keccak256(&buf)
2324 }
2325
2326 /// Tests branch collapse when the removed child comes BEFORE the remaining child.
2327 ///
2328 /// Trie structure (3 hashed storage keys):
2329 /// `key_a` = 0x20... (root nibble 2, sub-nibble 0)
2330 /// `key_b` = 0x21... (root nibble 2, sub-nibble 1)
2331 /// `key_c` = 0xb0... (root nibble b)
2332 ///
2333 /// This creates:
2334 /// root branch at nibbles {2, b}
2335 /// sub-branch at path [2] at nibbles {0, 1}
2336 ///
2337 /// `key_a` is removed (prefix set marks it dirty, cursor has no value for it).
2338 /// The sub-branch at [2] collapses into its remaining child (`key_b`). The removed child
2339 /// (nibble 0) comes before the remaining child (nibble 1).
2340 #[test]
2341 fn test_branch_collapse_removed_child_before_remaining() {
2342 reth_tracing::init_test_tracing();
2343
2344 let val = U256::from(1u64);
2345
2346 let key_a = B256::right_padding_from(&[0x20]); // root nibble 2, sub-nibble 0
2347 let key_b = B256::right_padding_from(&[0x21]); // root nibble 2, sub-nibble 1
2348 let key_c = B256::right_padding_from(&[0xb0]); // root nibble b
2349
2350 // Compute leaf hashes for the sub-branch's children.
2351 // The sub-branch at path [2] consumes 2 nibbles from each key (root nibble + sub-nibble).
2352 let leaf_hash_a = storage_leaf_hash(&Nibbles::unpack(key_a).slice(2..), &val);
2353 let leaf_hash_b = storage_leaf_hash(&Nibbles::unpack(key_b).slice(2..), &val);
2354
2355 // Only cache the sub-branch at path [2] — the root will be built from leaves.
2356 // The sub-branch has children at nibbles 0 and 1, both with cached hashes.
2357 let sub_branch_state_mask = TrieMask::new((1 << 0) | (1 << 1));
2358 let cached_sub_branch = BranchNodeCompact::new(
2359 sub_branch_state_mask,
2360 TrieMask::new(0),
2361 sub_branch_state_mask,
2362 vec![leaf_hash_a, leaf_hash_b],
2363 None,
2364 );
2365
2366 let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2367 std::iter::once((Nibbles::from_nibbles([0x2]), cached_sub_branch)).collect();
2368
2369 // The hashed cursor contains key_b and key_c (the root's other child). key_a was removed
2370 // (not in cursor)
2371 let mut harness = TrieTestHarness::new([(key_b, val), (key_c, val)].into_iter().collect());
2372 harness.set_trie_nodes(storage_nodes);
2373
2374 // Prefix set marks key_a as dirty (removed).
2375 let mut prefix_set_mut = PrefixSetMut::default();
2376 prefix_set_mut.insert(Nibbles::unpack(key_a));
2377 let prefix_set = prefix_set_mut.freeze();
2378
2379 // Compute root with cached branches + prefix set — triggers sub-branch collapse.
2380 let storage_trie_cursor =
2381 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2382 let hashed_storage_cursor = harness
2383 .hashed_cursor_factory()
2384 .hashed_storage_cursor(harness.hashed_address())
2385 .unwrap();
2386 let mut calculator =
2387 StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor)
2388 .with_prefix_set(prefix_set);
2389 let root_node = calculator
2390 .storage_root_node(harness.hashed_address())
2391 .expect("storage_root_node should succeed after branch collapse");
2392 let root_with_collapse =
2393 calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2394
2395 // Compute reference root from scratch (no cached branches) using the full final state.
2396 let mut fresh_harness =
2397 TrieTestHarness::new([(key_b, val), (key_c, val)].into_iter().collect());
2398 fresh_harness.set_trie_nodes(BTreeMap::new());
2399 let storage_trie_cursor = fresh_harness
2400 .trie_cursor_factory()
2401 .storage_trie_cursor(fresh_harness.hashed_address())
2402 .unwrap();
2403 let hashed_storage_cursor = fresh_harness
2404 .hashed_cursor_factory()
2405 .hashed_storage_cursor(fresh_harness.hashed_address())
2406 .unwrap();
2407 let mut fresh_calculator =
2408 StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
2409 let fresh_root_node = fresh_calculator
2410 .storage_root_node(fresh_harness.hashed_address())
2411 .expect("fresh storage_root_node should succeed");
2412 let expected_root = fresh_calculator
2413 .compute_root_hash(core::slice::from_ref(&fresh_root_node))
2414 .unwrap()
2415 .unwrap();
2416
2417 pretty_assertions::assert_eq!(
2418 expected_root,
2419 root_with_collapse,
2420 "Root hash after collapsing branch (removed child before remaining) should match fresh computation"
2421 );
2422 }
2423
2424 /// Tests branch collapse when the removed child comes AFTER the remaining child.
2425 ///
2426 /// Same trie structure as "before" test, but with nibbles 4 and 9 instead of 0 and 1 for
2427 /// the sub-branch, and nibble 9 is removed. The removed child (nibble 9) comes after the
2428 /// remaining child (nibble 4).
2429 #[test]
2430 fn test_branch_collapse_removed_child_after_remaining() {
2431 reth_tracing::init_test_tracing();
2432
2433 let val = U256::from(1u64);
2434
2435 // key_a at sub-nibble 4, key_b at sub-nibble 9 (under root nibble 2).
2436 let key_a = B256::right_padding_from(&[0x24]); // root nibble 2, sub-nibble 4
2437 let key_b = B256::right_padding_from(&[0x29]); // root nibble 2, sub-nibble 9
2438 let key_c = B256::right_padding_from(&[0xb0]); // root nibble b
2439
2440 let leaf_hash_a = storage_leaf_hash(&Nibbles::unpack(key_a).slice(2..), &val);
2441 let leaf_hash_b = storage_leaf_hash(&Nibbles::unpack(key_b).slice(2..), &val);
2442
2443 // Only cache the sub-branch at path [2] — the root will be built from leaves.
2444 let sub_branch_state_mask = TrieMask::new((1 << 4) | (1 << 9));
2445 let cached_sub_branch = BranchNodeCompact::new(
2446 sub_branch_state_mask,
2447 TrieMask::new(0),
2448 sub_branch_state_mask,
2449 vec![leaf_hash_a, leaf_hash_b],
2450 None,
2451 );
2452
2453 let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2454 std::iter::once((Nibbles::from_nibbles([0x2]), cached_sub_branch)).collect();
2455
2456 // The hashed cursor contains key_a and key_c. key_b was removed (not in cursor)
2457 let mut harness = TrieTestHarness::new([(key_a, val), (key_c, val)].into_iter().collect());
2458 harness.set_trie_nodes(storage_nodes);
2459
2460 // Prefix set marks key_b as dirty (removed).
2461 let mut prefix_set_mut = PrefixSetMut::default();
2462 prefix_set_mut.insert(Nibbles::unpack(key_b));
2463 let prefix_set = prefix_set_mut.freeze();
2464
2465 // Compute root with cached branches + prefix set — triggers sub-branch collapse.
2466 let storage_trie_cursor =
2467 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2468 let hashed_storage_cursor = harness
2469 .hashed_cursor_factory()
2470 .hashed_storage_cursor(harness.hashed_address())
2471 .unwrap();
2472 let mut calculator =
2473 StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor)
2474 .with_prefix_set(prefix_set);
2475 let root_node = calculator
2476 .storage_root_node(harness.hashed_address())
2477 .expect("storage_root_node should succeed after branch collapse");
2478 let root_with_collapse =
2479 calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2480
2481 // Compute reference root from scratch (no cached branches) using the full final state.
2482 let mut fresh_harness =
2483 TrieTestHarness::new([(key_a, val), (key_c, val)].into_iter().collect());
2484 fresh_harness.set_trie_nodes(BTreeMap::new());
2485 let storage_trie_cursor = fresh_harness
2486 .trie_cursor_factory()
2487 .storage_trie_cursor(fresh_harness.hashed_address())
2488 .unwrap();
2489 let hashed_storage_cursor = fresh_harness
2490 .hashed_cursor_factory()
2491 .hashed_storage_cursor(fresh_harness.hashed_address())
2492 .unwrap();
2493 let mut fresh_calculator =
2494 StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
2495 let fresh_root_node = fresh_calculator
2496 .storage_root_node(fresh_harness.hashed_address())
2497 .expect("fresh storage_root_node should succeed");
2498 let expected_root = fresh_calculator
2499 .compute_root_hash(core::slice::from_ref(&fresh_root_node))
2500 .unwrap()
2501 .unwrap();
2502
2503 pretty_assertions::assert_eq!(
2504 expected_root,
2505 root_with_collapse,
2506 "Root hash after collapsing branch (removed child after remaining) should match fresh computation"
2507 );
2508 }
2509
2510 #[test]
2511 fn test_cached_branch_extension_skips_diverging_target() {
2512 reth_tracing::init_test_tracing();
2513
2514 let val = U256::from(100u64);
2515
2516 // Keys whose first bytes directly set the nibble paths we need.
2517 let key_a0 = B256::right_padding_from(&[0x6a, 0x30]); // nibbles: 6,a,3,0,...
2518 let key_a1 = B256::right_padding_from(&[0x6a, 0x31]); // nibbles: 6,a,3,1,...
2519 let key_c = B256::right_padding_from(&[0x6a, 0x80]); // nibbles: 6,a,8,0,...
2520 let key_d = B256::right_padding_from(&[0x6b, 0x00]); // nibbles: 6,b,0,0,...
2521 let key_e = B256::right_padding_from(&[0x6c, 0x00]); // nibbles: 6,c,0,0,...
2522
2523 // Build a correct trie from all five leaves to get the expected root and real hashes.
2524 let all_storage: BTreeMap<B256, U256> =
2525 [(key_a0, val), (key_a1, val), (key_c, val), (key_d, val), (key_e, val)]
2526 .into_iter()
2527 .collect();
2528 let correct_harness = TrieTestHarness::new(all_storage.clone());
2529 let expected_root = correct_harness.original_root();
2530
2531 // Compute leaf hashes for constructing manual cached branch nodes.
2532 let leaf_hash_a0 = storage_leaf_hash(&Nibbles::unpack(key_a0).slice(4..), &val);
2533 let leaf_hash_a1 = storage_leaf_hash(&Nibbles::unpack(key_a1).slice(4..), &val);
2534 let leaf_hash_d = storage_leaf_hash(&Nibbles::unpack(key_d).slice(2..), &val);
2535 let leaf_hash_e = storage_leaf_hash(&Nibbles::unpack(key_e).slice(2..), &val);
2536
2537 // ── Construct cached branch at [6] ─────────────────────────────────────
2538 // state_mask: bits a, b, and c set.
2539 // hash_mask: bits b and c — both have cached leaf hashes. Bit a has no hash, so the
2540 // calculator will seek the trie cursor to find a deeper cached branch.
2541 //
2542 // Having three children with two (b, c) NOT in the prefix set ensures
2543 // `should_skip_cached_branch` does NOT skip this branch (num_unmatched >= 2).
2544 let branch_6_state_mask = TrieMask::new((1 << 0xa) | (1 << 0xb) | (1 << 0xc));
2545 let branch_6_hash_mask = TrieMask::new((1 << 0xb) | (1 << 0xc));
2546 let branch_6 = BranchNodeCompact::new(
2547 branch_6_state_mask,
2548 TrieMask::new(0),
2549 branch_6_hash_mask,
2550 vec![leaf_hash_d, leaf_hash_e],
2551 None,
2552 );
2553
2554 // ── Construct cached branch at [6,a,3] ────────────────────────────────
2555 // state_mask: bits 0 and 1 set (children key_a0 and key_a1).
2556 // hash_mask: both bits set — both children have cached hashes.
2557 let branch_6a3_state_mask = TrieMask::new((1 << 0) | (1 << 1));
2558 let branch_6a3 = BranchNodeCompact::new(
2559 branch_6a3_state_mask,
2560 TrieMask::new(0),
2561 branch_6a3_state_mask,
2562 vec![leaf_hash_a0, leaf_hash_a1],
2563 None,
2564 );
2565
2566 // Intentionally omit the branch at [6,a] — this is the inconsistency.
2567 let inconsistent_nodes: BTreeMap<Nibbles, BranchNodeCompact> = [
2568 (Nibbles::from_nibbles([0x6]), branch_6),
2569 (Nibbles::from_nibbles([0x6, 0xa, 0x3]), branch_6a3),
2570 ]
2571 .into_iter()
2572 .collect();
2573
2574 // Create harness with all five leaves but the inconsistent trie nodes.
2575 let mut harness = TrieTestHarness::new(all_storage);
2576 harness.set_trie_nodes(inconsistent_nodes);
2577
2578 // Mark key_c as dirty — in the real scenario the leaf was touched by execution.
2579 // The prefix set contains only key_c's full path. `should_skip_cached_branch` will
2580 // NOT skip branch [6] because two of its three children (b, c) are not in the set
2581 // (num_unmatched = 2 > 1). It also will not skip branch [6,a,3] because
2582 // `contains([6,a,3])` is false (key_c's nibbles 6,a,8,... do not start with 6,a,3).
2583 let mut prefix_set = PrefixSetMut::default();
2584 prefix_set.insert(Nibbles::unpack(key_c));
2585
2586 // ── Verify root hash ───────────────────────────────────────────────────
2587 let trie_cursor =
2588 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2589 let hashed_cursor = harness
2590 .hashed_cursor_factory()
2591 .hashed_storage_cursor(harness.hashed_address())
2592 .unwrap();
2593 let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2594 .with_prefix_set(prefix_set.freeze());
2595
2596 let root_node = calculator
2597 .storage_root_node(harness.hashed_address())
2598 .expect("storage_root_node should succeed");
2599 let got_root = calculator
2600 .compute_root_hash(core::slice::from_ref(&root_node))
2601 .unwrap()
2602 .expect("should produce a root hash");
2603
2604 // With the bug, the calculator skips key_c and produces a wrong root.
2605 pretty_assertions::assert_eq!(
2606 expected_root,
2607 got_root,
2608 "Root hash should match correct trie; cached extension must not skip diverging leaves"
2609 );
2610
2611 // ── Verify proof for key_c contains nodes on its path ──────────────────
2612 let mut targets = vec![ProofV2Target::new(key_c)];
2613 let proofs = calculator
2614 .storage_proof(harness.hashed_address(), &mut targets)
2615 .expect("storage_proof should succeed");
2616
2617 let key_c_nibbles = Nibbles::unpack(key_c);
2618 let has_matching_node = proofs.iter().any(|node| key_c_nibbles.starts_with(&node.path));
2619 assert!(
2620 has_matching_node,
2621 "Proof for key_c should contain at least one node on key_c's path, got: {proofs:?}"
2622 );
2623 }
2624
2625 #[test]
2626 fn test_cached_branch_extension_skips_diverging_target_before() {
2627 reth_tracing::init_test_tracing();
2628
2629 let val = U256::from(100u64);
2630
2631 // Keys whose first bytes directly set the nibble paths we need.
2632 let key_a0 = B256::right_padding_from(&[0x6a, 0x80]); // nibbles: 6,a,8,0,...
2633 let key_a1 = B256::right_padding_from(&[0x6a, 0x81]); // nibbles: 6,a,8,1,...
2634 let key_c = B256::right_padding_from(&[0x6a, 0x30]); // nibbles: 6,a,3,0,... (BEFORE [6,a,8])
2635 let key_d = B256::right_padding_from(&[0x6b, 0x00]); // nibbles: 6,b,0,0,...
2636 let key_e = B256::right_padding_from(&[0x6c, 0x00]); // nibbles: 6,c,0,0,...
2637
2638 // Build a correct trie from all five leaves to get the expected root and real hashes.
2639 let all_storage: BTreeMap<B256, U256> =
2640 [(key_a0, val), (key_a1, val), (key_c, val), (key_d, val), (key_e, val)]
2641 .into_iter()
2642 .collect();
2643 let correct_harness = TrieTestHarness::new(all_storage.clone());
2644 let expected_root = correct_harness.original_root();
2645
2646 // Compute leaf hashes for constructing manual cached branch nodes.
2647 let leaf_hash_a0 = storage_leaf_hash(&Nibbles::unpack(key_a0).slice(4..), &val);
2648 let leaf_hash_a1 = storage_leaf_hash(&Nibbles::unpack(key_a1).slice(4..), &val);
2649 let leaf_hash_d = storage_leaf_hash(&Nibbles::unpack(key_d).slice(2..), &val);
2650 let leaf_hash_e = storage_leaf_hash(&Nibbles::unpack(key_e).slice(2..), &val);
2651
2652 // ── Construct cached branch at [6] ─────────────────────────────────────
2653 // state_mask: bits a, b, and c set.
2654 // hash_mask: bits b and c — both have cached leaf hashes. Bit a has no hash, so the
2655 // calculator will seek the trie cursor to find a deeper cached branch.
2656 //
2657 // Having three children with two (b, c) NOT in the prefix set ensures
2658 // `should_skip_cached_branch` does NOT skip this branch (num_unmatched >= 2).
2659 let branch_6_state_mask = TrieMask::new((1 << 0xa) | (1 << 0xb) | (1 << 0xc));
2660 let branch_6_hash_mask = TrieMask::new((1 << 0xb) | (1 << 0xc));
2661 let branch_6 = BranchNodeCompact::new(
2662 branch_6_state_mask,
2663 TrieMask::new(0),
2664 branch_6_hash_mask,
2665 vec![leaf_hash_d, leaf_hash_e],
2666 None,
2667 );
2668
2669 // ── Construct cached branch at [6,a,8] ────────────────────────────────
2670 // state_mask: bits 0 and 1 set (children key_a0 and key_a1).
2671 // hash_mask: both bits set — both children have cached hashes.
2672 let branch_6a8_state_mask = TrieMask::new((1 << 0) | (1 << 1));
2673 let branch_6a8 = BranchNodeCompact::new(
2674 branch_6a8_state_mask,
2675 TrieMask::new(0),
2676 branch_6a8_state_mask,
2677 vec![leaf_hash_a0, leaf_hash_a1],
2678 None,
2679 );
2680
2681 // Intentionally omit the branch at [6,a] — this is the inconsistency.
2682 let inconsistent_nodes: BTreeMap<Nibbles, BranchNodeCompact> = [
2683 (Nibbles::from_nibbles([0x6]), branch_6),
2684 (Nibbles::from_nibbles([0x6, 0xa, 0x8]), branch_6a8),
2685 ]
2686 .into_iter()
2687 .collect();
2688
2689 // Create harness with all five leaves but the inconsistent trie nodes.
2690 let mut harness = TrieTestHarness::new(all_storage);
2691 harness.set_trie_nodes(inconsistent_nodes);
2692
2693 // Mark key_c as dirty — it comes BEFORE the cached branch [6,a,8] in nibble order.
2694 let mut prefix_set = PrefixSetMut::default();
2695 prefix_set.insert(Nibbles::unpack(key_c));
2696
2697 // ── Verify root hash ───────────────────────────────────────────────────
2698 let trie_cursor =
2699 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2700 let hashed_cursor = harness
2701 .hashed_cursor_factory()
2702 .hashed_storage_cursor(harness.hashed_address())
2703 .unwrap();
2704 let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2705 .with_prefix_set(prefix_set.freeze());
2706
2707 let root_node = calculator
2708 .storage_root_node(harness.hashed_address())
2709 .expect("storage_root_node should succeed");
2710 let got_root = calculator
2711 .compute_root_hash(core::slice::from_ref(&root_node))
2712 .unwrap()
2713 .expect("should produce a root hash");
2714
2715 // With the bug, the calculator skips key_c and produces a wrong root.
2716 pretty_assertions::assert_eq!(
2717 expected_root,
2718 got_root,
2719 "Root hash should match correct trie; cached extension must not skip diverging leaves before cached branch"
2720 );
2721
2722 // ── Verify proof for key_c contains nodes on its path ──────────────────
2723 let mut targets = vec![ProofV2Target::new(key_c)];
2724 let proofs = calculator
2725 .storage_proof(harness.hashed_address(), &mut targets)
2726 .expect("storage_proof should succeed");
2727
2728 let key_c_nibbles = Nibbles::unpack(key_c);
2729 let has_matching_node = proofs.iter().any(|node| key_c_nibbles.starts_with(&node.path));
2730 assert!(
2731 has_matching_node,
2732 "Proof for key_c should contain at least one node on key_c's path, got: {proofs:?}"
2733 );
2734 }
2735
2736 #[test]
2737 fn test_skipped_parent_branch_with_unskipped_child() {
2738 reth_tracing::init_test_tracing();
2739
2740 let val = U256::from(1u64);
2741 let updated_val = U256::from(2u64);
2742
2743 // We need cached branches at [2], [2,f], and [3] in the trie DB.
2744 let key_2 = B256::right_padding_from(&[0x20]);
2745 let key_2f00 = B256::right_padding_from(&[0x2f, 0x00]);
2746 let key_2f01 = B256::right_padding_from(&[0x2f, 0x01]);
2747 let key_2f10 = B256::right_padding_from(&[0x2f, 0x10]);
2748 let key_2f11 = B256::right_padding_from(&[0x2f, 0x11]);
2749 let key_300 = B256::right_padding_from(&[0x30, 0x00]);
2750 let key_301 = B256::right_padding_from(&[0x30, 0x10]);
2751 let key_310 = B256::right_padding_from(&[0x31, 0x00]);
2752 let key_311 = B256::right_padding_from(&[0x31, 0x10]);
2753 let key_500 = B256::right_padding_from(&[0x50, 0x00]);
2754 let key_501 = B256::right_padding_from(&[0x50, 0x10]);
2755 let key_510 = B256::right_padding_from(&[0x51, 0x00]);
2756 let key_511 = B256::right_padding_from(&[0x51, 0x10]);
2757
2758 let all_keys = [
2759 key_2, key_2f00, key_2f01, key_2f10, key_2f11, key_300, key_301, key_310, key_311,
2760 key_500, key_501, key_510, key_511,
2761 ];
2762
2763 let original_storage: BTreeMap<B256, U256> = all_keys.iter().map(|k| (*k, val)).collect();
2764 let harness = TrieTestHarness::new(original_storage);
2765
2766 // Verify that the expected branches exist in the trie.
2767 let trie_updates = harness.storage_trie_updates();
2768 assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x2])));
2769 assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x2, 0xf])));
2770 assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x3])));
2771
2772 // Change only key_2 — triggers skip of parent branch [2] while child [2,f] is not
2773 // skipped.
2774 let changeset: BTreeMap<B256, U256> = std::iter::once((key_2, updated_val)).collect();
2775 let (expected_root, _) = harness.get_root_with_updates(&changeset);
2776
2777 let mut updated_storage = harness.storage().clone();
2778 updated_storage.insert(key_2, updated_val);
2779
2780 let updated_hashed = MockHashedCursorFactory::new(
2781 BTreeMap::new(),
2782 std::iter::once((harness.hashed_address(), updated_storage)).collect(),
2783 );
2784
2785 let mut prefix_set = PrefixSetMut::default();
2786 prefix_set.insert(Nibbles::unpack(key_2));
2787
2788 let trie_cursor =
2789 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2790 let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
2791 let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2792 .with_prefix_set(prefix_set.freeze());
2793 let root_node = calculator
2794 .storage_root_node(harness.hashed_address())
2795 .expect("storage_root_node should succeed");
2796
2797 let got_root = calculator
2798 .compute_root_hash(&[root_node])
2799 .expect("root hash should succeed")
2800 .expect("root should get hashed");
2801 pretty_assertions::assert_eq!(expected_root, got_root);
2802 }
2803
2804 #[test]
2805 fn test_cached_hash_with_deleted_leaf() {
2806 reth_tracing::init_test_tracing();
2807
2808 // Use different values to ensure distinct leaf hashes.
2809 let val_3 = U256::from(111u64);
2810 let val_5 = U256::from(222u64);
2811 let val_8 = U256::from(333u64);
2812
2813 // Keys under a common prefix `0x6_` to create a branch at path [6].
2814 // Use second byte to distinguish short keys (so they differ after position 2).
2815 let key_63 = B256::right_padding_from(&[0x63, 0xaa]); // nibble path: 6,3,a,a,...
2816 let key_65 = B256::right_padding_from(&[0x65, 0xbb]); // nibble path: 6,5,b,b,...
2817 let key_68 = B256::right_padding_from(&[0x68, 0xcc]); // nibble path: 6,8,c,c,...
2818
2819 // Compute leaf hashes. The branch at [6] consumes 2 nibbles (the branch path [6]
2820 // plus the child nibble), so each leaf's short key starts at position 2.
2821 let leaf_hash_3 = storage_leaf_hash(&Nibbles::unpack(key_63).slice(2..), &val_3);
2822 let leaf_hash_5 = storage_leaf_hash(&Nibbles::unpack(key_65).slice(2..), &val_5);
2823 let leaf_hash_8 = storage_leaf_hash(&Nibbles::unpack(key_68).slice(2..), &val_8);
2824
2825 // Build cached branch at [6] with state_mask and hash_mask bits for nibbles 3, 5, 8.
2826 let state_mask = TrieMask::new((1 << 3) | (1 << 5) | (1 << 8));
2827 let cached_branch = BranchNodeCompact::new(
2828 state_mask,
2829 TrieMask::new(0),
2830 state_mask, // hash_mask = state_mask (all children have cached hashes)
2831 vec![leaf_hash_3, leaf_hash_5, leaf_hash_8],
2832 None,
2833 );
2834
2835 let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2836 std::iter::once((Nibbles::from_nibbles([0x6]), cached_branch)).collect();
2837
2838 // Compute the expected root from a fresh trie with just key_65 and key_68.
2839 let mut harness =
2840 TrieTestHarness::new([(key_65, val_5), (key_68, val_8)].into_iter().collect());
2841 let expected_root = harness.original_root();
2842
2843 // Update the harness with a cached trie node which will reference key_63 by hash.
2844 harness.set_trie_nodes(storage_nodes);
2845
2846 // Mark key_63 as dirty in the prefix set — in the real scenario the leaf was
2847 // deleted and the HashedPostState overlay masks it out.
2848 let mut prefix_set = PrefixSetMut::default();
2849 prefix_set.insert(Nibbles::unpack(key_63));
2850
2851 // Request a proof for key_63 (absence proof — no leaf exists).
2852 // Because the prefix set marks nibble 3's child path as dirty, the cached hash for
2853 // nibble 3 is skipped.
2854 let mut targets = vec![ProofV2Target::new(key_63)];
2855
2856 let trie_cursor =
2857 harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2858 let hashed_cursor = harness
2859 .hashed_cursor_factory()
2860 .hashed_storage_cursor(harness.hashed_address())
2861 .unwrap();
2862 let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2863 .with_prefix_set(prefix_set.freeze());
2864
2865 let proofs = calculator
2866 .storage_proof(harness.hashed_address(), &mut targets)
2867 .expect("storage_proof should succeed");
2868 assert_eq!(1, proofs.len());
2869 let got_root = calculator
2870 .compute_root_hash(&proofs)
2871 .expect("compute_root_hash should succeed")
2872 .expect("should produce a root hash (proof contains root node)");
2873
2874 // With the bug, nibble 5 gets hashes[0] (nibble 3's hash) and nibble 8 gets
2875 // hashes[1] (nibble 5's hash), producing a wrong root.
2876 pretty_assertions::assert_eq!(
2877 expected_root,
2878 got_root,
2879 "Root hash should match trie without key_63; cached hash index is off when \
2880 an earlier hashed child has no leaves (absence proof target)"
2881 );
2882 }
2883}