Skip to main content

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_parent_path),
195        ret,
196    )]
197    fn should_retain<'a>(
198        &self,
199        targets: &mut Option<TargetsCursor<'a>>,
200        path: &Nibbles,
201        check_parent_path: 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        loop {
209            // If the node in question is a prefix of the target then we do not iterate targets
210            // further.
211            //
212            // Even if the node is a prefix of the target's key, a target with a parent path only
213            // retains nodes strictly below that already-revealed parent.
214            //
215            // _However_ even if the node doesn't match one target due to its parent path, it may
216            // match other targets whose keys match this node. So we search forwards and backwards
217            // for all targets which might match this node.
218            //
219            // For example, given a branch 0xabc, with children at 0, 1, and 2, and targets:
220            // - key: 0xabc0, parent path length: 1
221            // - key: 0xabc1, parent path length: 0
222            // - key: 0xabc2, parent path length: 3 <-- current
223            // - key: 0xabc3, parent path length: 2
224            //
225            // When the branch node at 0xabc is visited it will be after the targets has iterated
226            // forward to 0xabc2 (because all children will have been visited already). At this
227            // point the target for 0xabc2 will not match the branch due to its prefix, but any of
228            // the other targets would, so we need to check those as well.
229            if lower.key_nibbles.starts_with(path) {
230                let is_below_parent = |target: &ProofV2Target| {
231                    target.parent.path_len().is_none_or(|len| path.len() > len)
232                };
233                return !check_parent_path ||
234                    (is_below_parent(lower) ||
235                        targets
236                            .skip_iter()
237                            .take_while(|target| target.key_nibbles.starts_with(path))
238                            .any(is_below_parent) ||
239                        targets
240                            .rev_iter()
241                            .take_while(|target| target.key_nibbles.starts_with(path))
242                            .any(is_below_parent))
243            }
244
245            // If the path isn't in the current range then iterate forward until it is (or until
246            // there is no upper bound, indicating unbounded).
247            if upper
248                .is_some_and(|upper| depth_first::cmp(path, &upper.key_nibbles) != Ordering::Less)
249            {
250                (lower, upper) = targets.next();
251                trace!(target: TRACE_TARGET, target = ?lower, "upper target <= path, next target");
252            } else {
253                return false
254            }
255        }
256    }
257
258    /// Takes a child which has been removed from the `child_stack` and converts it to an
259    /// [`RlpNode`].
260    ///
261    /// Calling this method indicates that the child will not undergo any further modifications, and
262    /// therefore can be retained as a proof node if applicable.
263    fn commit_child<'a>(
264        &mut self,
265        targets: &mut Option<TargetsCursor<'a>>,
266        child_path: Nibbles,
267        child: ProofTrieBranchChild<VE::DeferredEncoder>,
268    ) -> Result<RlpNode, StateProofError> {
269        // If the child is already an `RlpNode` then there is nothing to do.
270        if let ProofTrieBranchChild::RlpNode(rlp_node) = child {
271            return Ok(rlp_node)
272        }
273
274        // If we should retain the child then do so.
275        if self.should_retain(targets, &child_path, true) {
276            trace!(target: TRACE_TARGET, ?child_path, "Retaining child");
277
278            // Convert to `ProofTrieNodeV2`, which will be what is retained.
279            //
280            // If this node is a branch then its `rlp_nodes_buf` will be taken and not returned to
281            // the `rlp_nodes_bufs` free-list.
282            self.rlp_encode_buf.clear();
283            let proof_node = child.into_proof_trie_node(child_path, &mut self.rlp_encode_buf)?;
284
285            // Use the `ProofTrieNodeV2` to encode the `RlpNode`, and then push it onto retained
286            // nodes before returning.
287            self.rlp_encode_buf.clear();
288            proof_node.node.encode(&mut self.rlp_encode_buf);
289
290            self.retained_proofs.push(proof_node);
291            return Ok(RlpNode::from_rlp(&self.rlp_encode_buf));
292        }
293
294        // If the child path is not being retained then we convert directly to an `RlpNode`
295        // using `into_rlp`. Since we are not retaining the node we can recover any `RlpNode`
296        // buffers for the free-list here, hence why we do this as a separate logical branch.
297        self.rlp_encode_buf.clear();
298        let (child_rlp_node, freed_rlp_nodes_buf) = child.into_rlp(&mut self.rlp_encode_buf)?;
299
300        // If there is an `RlpNode` buffer which can be re-used then push it onto the free-list.
301        if let Some(buf) = freed_rlp_nodes_buf {
302            self.rlp_nodes_bufs.push(buf);
303        }
304
305        Ok(child_rlp_node)
306    }
307
308    /// Returns the path of the child of the currently under-construction branch at the given
309    /// nibble.
310    #[inline]
311    fn child_path_at(&self, nibble: u8) -> Nibbles {
312        let mut child_path = self.branch_path;
313        debug_assert!(child_path.len() < 64);
314        child_path.push_unchecked(nibble);
315        child_path
316    }
317
318    /// Returns index of the highest nibble which is set in the mask.
319    ///
320    /// # Panics
321    ///
322    /// Will panic in debug mode if the mask is empty.
323    #[inline]
324    fn highest_set_nibble(mask: TrieMask) -> u8 {
325        debug_assert!(!mask.is_empty());
326        (u16::BITS - mask.leading_zeros() - 1) as u8
327    }
328
329    /// Returns the path of the child on top of the `child_stack`, or the root path if the stack is
330    /// empty. Returns None if the current branch has not yet pushed a child (empty `state_mask`).
331    fn last_child_path(&self) -> Option<Nibbles> {
332        // If there is no branch under construction then the top child must be the root child.
333        let Some(branch) = self.branch_stack.last() else {
334            return Some(Nibbles::new());
335        };
336
337        (!branch.state_mask.is_empty())
338            .then(|| self.child_path_at(Self::highest_set_nibble(branch.state_mask)))
339    }
340
341    /// Calls [`Self::commit_child`] on the last child of `child_stack`, replacing it with a
342    /// [`ProofTrieBranchChild::RlpNode`].
343    ///
344    /// If `child_stack` is empty then this is a no-op.
345    ///
346    /// NOTE that this method call relies on the `state_mask` of the top branch of the
347    /// `branch_stack` to determine the last child's path. When committing the last child prior to
348    /// pushing a new child, it's important to set the new child's `state_mask` bit _after_ the call
349    /// to this method.
350    #[instrument(
351        target = TRACE_TARGET,
352        level = "trace",
353        skip_all,
354        fields(child_path = ?self.last_child_path()),
355    )]
356    fn commit_last_child<'a>(
357        &mut self,
358        targets: &mut Option<TargetsCursor<'a>>,
359    ) -> Result<(), StateProofError> {
360        if matches!(self.child_stack.last(), Some(ProofTrieBranchChild::RlpNode(_))) {
361            trace!(target: TRACE_TARGET, "Last child already committed, leaving stack unchanged");
362            return Ok(())
363        }
364
365        let Some(child_path) = self.last_child_path() else { return Ok(()) };
366        let child =
367            self.child_stack.pop().expect("child_stack can't be empty if there's a child path");
368
369        // Only commit immediately if retained for the proof. Otherwise, defer conversion
370        // to pop_branch() to give DeferredEncoder time for async work.
371        if self.should_retain(targets, &child_path, true) {
372            let child_rlp_node = self.commit_child(targets, child_path, child)?;
373            trace!(target: TRACE_TARGET, ?child_rlp_node, "Pushing committed child RlpNode onto stack");
374            self.child_stack.push(ProofTrieBranchChild::RlpNode(child_rlp_node));
375        } else {
376            trace!(target: TRACE_TARGET, "Pushing uncommitted child onto stack");
377            self.child_stack.push(child);
378        }
379
380        Ok(())
381    }
382
383    /// Creates a new leaf node on a branch, setting its `state_mask` bit and pushing the leaf onto
384    /// the `child_stack`.
385    ///
386    /// # Panics
387    ///
388    /// - If `branch_stack` is empty
389    /// - If the leaf's nibble is already set in the branch's `state_mask`.
390    fn push_new_leaf<'a>(
391        &mut self,
392        targets: &mut Option<TargetsCursor<'a>>,
393        leaf_nibble: u8,
394        leaf_short_key: Nibbles,
395        leaf_val: VE::DeferredEncoder,
396    ) -> Result<(), StateProofError> {
397        // Before pushing the new leaf onto the `child_stack` we need to commit the previous last
398        // child, so that only `child_stack`'s final child is a non-RlpNode.
399        self.commit_last_child(targets)?;
400
401        // Once the last child is committed we set the new child's bit on the top branch's
402        // `state_mask` and push that new child.
403        let branch = self.branch_stack.last_mut().expect("branch_stack cannot be empty");
404
405        debug_assert!(!branch.state_mask.is_bit_set(leaf_nibble));
406        branch.state_mask.set_bit(leaf_nibble);
407
408        self.child_stack
409            .push(ProofTrieBranchChild::Leaf { short_key: leaf_short_key, value: leaf_val });
410
411        Ok(())
412    }
413
414    /// Pushes a new branch onto the `branch_stack` based on the path and short key of the last
415    /// child on the `child_stack` and the path of the next child which will be pushed on to the
416    /// stack after this call.
417    ///
418    /// Returns the nibble of the branch's `state_mask` which should be set for the new child, and
419    /// short key that the next child should use.
420    fn push_new_branch(&mut self, new_child_path: Nibbles) -> (u8, Nibbles) {
421        // First determine the new child's shortkey relative to the current branch. If there is no
422        // current branch then the short key is the full path.
423        let new_child_short_key = if self.branch_stack.is_empty() {
424            new_child_path
425        } else {
426            // When there is a current branch then trim off its path as well as the nibble that it
427            // has set for this leaf.
428            trim_nibbles_prefix(&new_child_path, self.branch_path.len() + 1)
429        };
430
431        // Get the new branch's first child, which is the child on the top of the stack with which
432        // the new child shares the same nibble on the current branch.
433        let first_child = self
434            .child_stack
435            .last_mut()
436            .expect("push_new_branch can't be called with empty child_stack");
437
438        let first_child_short_key = first_child.short_key();
439        debug_assert!(
440            !first_child_short_key.is_empty(),
441            "push_new_branch called when top child on stack is not a leaf or extension with a short key",
442        );
443
444        // Determine how many nibbles are shared between the new branch's first child and the new
445        // child. This common prefix will be the extension of the new branch
446        let common_prefix_len = first_child_short_key.common_prefix_length(&new_child_short_key);
447
448        // Trim off the common prefix from the first child's short key, plus one nibble which will
449        // stored by the new branch itself in its state mask.
450        let first_child_nibble = first_child_short_key.get_unchecked(common_prefix_len);
451        first_child.trim_short_key_prefix(common_prefix_len + 1);
452
453        // Similarly, trim off the common prefix, plus one nibble for the new branch, from the new
454        // child's short key.
455        let new_child_nibble = new_child_short_key.get_unchecked(common_prefix_len);
456        let new_child_short_key = trim_nibbles_prefix(&new_child_short_key, common_prefix_len + 1);
457
458        // Update the branch path to reflect the new branch about to be pushed. Its path will be
459        // the path of the previous branch, plus the nibble shared by each child, plus the parent
460        // extension (denoted by a non-zero `ext_len`). Since the new branch's path is a prefix of
461        // the original new_child_path we can just slice that.
462        //
463        // If the new branch is the first branch then we do not add the extra 1, as there is no
464        // nibble in a parent branch to account for.
465        let branch_path_len =
466            self.branch_path.len() + common_prefix_len + self.maybe_parent_nibble();
467        self.branch_path = new_child_path.slice_unchecked(0, branch_path_len);
468
469        // Push the new branch onto the `branch_stack`. We do not yet set the `state_mask` bit of
470        // the new child; whatever actually pushes the child onto the `child_stack` is expected to
471        // do that.
472        self.branch_stack.push(ProofTrieBranch {
473            ext_len: common_prefix_len as u8,
474            state_mask: TrieMask::new(1 << first_child_nibble),
475            masks: None,
476        });
477
478        trace!(
479            target: TRACE_TARGET,
480            ?new_child_path,
481            ?common_prefix_len,
482            ?first_child_nibble,
483            branch_path = ?self.branch_path,
484            "Pushed new branch",
485        );
486
487        (new_child_nibble, new_child_short_key)
488    }
489
490    /// Pops the top branch off of the `branch_stack`, hashes its children on the `child_stack`, and
491    /// replaces those children on the `child_stack`. The `branch_path` field will be updated
492    /// accordingly.
493    ///
494    /// # Panics
495    ///
496    /// This method panics if `branch_stack` is empty.
497    #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
498    fn pop_branch<'a>(
499        &mut self,
500        targets: &mut Option<TargetsCursor<'a>>,
501    ) -> Result<(), StateProofError> {
502        trace!(
503            target: TRACE_TARGET,
504            branch = ?self.branch_stack.last(),
505            branch_path = ?self.branch_path,
506            child_stack_len = ?self.child_stack.len(),
507            "called",
508        );
509
510        // Ensure the final child on the child stack has been committed, as this method expects all
511        // children of the branch to have been committed.
512        self.commit_last_child(targets)?;
513
514        let mut rlp_nodes_buf = self.take_rlp_nodes_buf();
515        let branch = self.branch_stack.pop().expect("branch_stack cannot be empty");
516
517        // Take the branch's children off the stack, using the state mask to determine how many
518        // there are.
519        let num_children = branch.state_mask.count_ones() as usize;
520        debug_assert!(
521            self.child_stack.len() >= num_children,
522            "Stack is missing necessary children ({num_children:?})"
523        );
524        debug_assert!(
525            num_children >= 2,
526            "A branch must have at least two children, got {num_children}"
527        );
528
529        // Collect children into RlpNode Vec. Children are in lexicographic order.
530        rlp_nodes_buf.reserve(num_children);
531        for child in self.child_stack.drain(self.child_stack.len() - num_children..) {
532            let child_rlp_node = match child {
533                ProofTrieBranchChild::RlpNode(rlp_node) => rlp_node,
534                uncommitted_child => {
535                    // Convert uncommitted child (not retained for proof) to RlpNode now.
536                    self.rlp_encode_buf.clear();
537                    let (rlp_node, freed_buf) =
538                        uncommitted_child.into_rlp(&mut self.rlp_encode_buf)?;
539                    if let Some(buf) = freed_buf {
540                        self.rlp_nodes_bufs.push(buf);
541                    }
542                    rlp_node
543                }
544            };
545            rlp_nodes_buf.push(child_rlp_node);
546        }
547
548        debug_assert_eq!(
549            rlp_nodes_buf.len(),
550            num_children,
551            "children length must match number of bits set in state_mask"
552        );
553
554        // Calculate the short key of the parent extension (if the branch has a parent extension).
555        // It's important to calculate this short key prior to modifying the `branch_path`.
556        let short_key = trim_nibbles_prefix(
557            &self.branch_path,
558            self.branch_path.len() - branch.ext_len as usize,
559        );
560
561        // Compute hash for the branch node if it has a parent extension.
562        let rlp_node = if short_key.is_empty() {
563            None
564        } else {
565            self.rlp_encode_buf.clear();
566            BranchNodeRef::new(&rlp_nodes_buf, branch.state_mask).encode(&mut self.rlp_encode_buf);
567            Some(RlpNode::from_rlp(&self.rlp_encode_buf))
568        };
569
570        // Wrap the `BranchNodeV2` so it can be pushed onto the child stack.
571        let branch_as_child = ProofTrieBranchChild::Branch {
572            node: BranchNodeV2::new(short_key, rlp_nodes_buf, branch.state_mask, rlp_node),
573            masks: branch.masks,
574        };
575
576        self.child_stack.push(branch_as_child);
577
578        // Update the branch_path. If this branch is the only branch then only its extension needs
579        // to be trimmed, otherwise we also need to remove its nibble from its parent.
580        let new_path_len =
581            self.branch_path.len() - branch.ext_len as usize - self.maybe_parent_nibble();
582
583        debug_assert!(self.branch_path.len() >= new_path_len);
584        self.branch_path = self.branch_path.slice_unchecked(0, new_path_len);
585
586        Ok(())
587    }
588
589    /// Adds a single leaf for a key to the stack, possibly collapsing an existing branch and/or
590    /// creating a new one depending on the path of the key.
591    fn push_leaf<'a>(
592        &mut self,
593        targets: &mut Option<TargetsCursor<'a>>,
594        key: Nibbles,
595        val: VE::DeferredEncoder,
596    ) -> Result<(), StateProofError> {
597        loop {
598            trace!(
599                target: TRACE_TARGET,
600                ?key,
601                branch_stack_len = ?self.branch_stack.len(),
602                branch_path = ?self.branch_path,
603                child_stack_len = ?self.child_stack.len(),
604                "push_leaf: loop",
605            );
606
607            // Get the `state_mask` of the branch currently being built. If there are no branches
608            // on the stack then it means either the trie is empty or only a single leaf has been
609            // added previously.
610            let curr_branch_state_mask = match self.branch_stack.last() {
611                Some(curr_branch) => curr_branch.state_mask,
612                None if self.child_stack.is_empty() => {
613                    // If the child stack is empty then this is the first leaf, push it and be done
614                    self.child_stack
615                        .push(ProofTrieBranchChild::Leaf { short_key: key, value: val });
616                    return Ok(())
617                }
618                None => {
619                    // If the child stack is not empty then it must only have a single other child
620                    // which is either a leaf or extension with a non-zero short key.
621                    debug_assert_eq!(self.child_stack.len(), 1);
622                    debug_assert!(!self
623                        .child_stack
624                        .last()
625                        .expect("already checked for emptiness")
626                        .short_key()
627                        .is_empty());
628                    let (nibble, short_key) = self.push_new_branch(key);
629                    self.push_new_leaf(targets, nibble, short_key, val)?;
630                    return Ok(())
631                }
632            };
633
634            // Find the common prefix length, which is the number of nibbles shared between the
635            // current branch and the key.
636            let common_prefix_len = self.branch_path.common_prefix_length(&key);
637
638            // If the current branch does not share all of its nibbles with the new key then it is
639            // not the parent of the new key. In this case the current branch will have no more
640            // children. We can pop it and loop back to the top to try again with its parent branch.
641            if common_prefix_len < self.branch_path.len() {
642                self.pop_branch(targets)?;
643                continue
644            }
645
646            // If the current branch is a prefix of the new key then the leaf is a child of the
647            // branch. If the branch doesn't have the leaf's nibble set then the leaf can be added
648            // directly, otherwise a new branch must be created in-between this branch and that
649            // existing child.
650            let nibble = key.get_unchecked(common_prefix_len);
651            if curr_branch_state_mask.is_bit_set(nibble) {
652                // Push a new branch which splits the short key of the existing child at this
653                // nibble.
654                let (nibble, short_key) = self.push_new_branch(key);
655                // Push the new leaf onto the new branch.
656                self.push_new_leaf(targets, nibble, short_key, val)?;
657            } else {
658                let short_key = key.slice_unchecked(common_prefix_len + 1, key.len());
659                self.push_new_leaf(targets, nibble, short_key, val)?;
660            }
661
662            return Ok(())
663        }
664    }
665
666    /// Given the lower and upper bounds (exclusive) of a range of keys, iterates over the
667    /// `hashed_cursor` and calculates all trie nodes possible based on those keys. If the upper
668    /// bound is None then it is considered unbounded.
669    ///
670    /// It is expected that this method is "driven" by `next_uncached_key_range`, which decides
671    /// which ranges of keys need to be calculated based on what cached trie data is available.
672    #[instrument(
673        target = TRACE_TARGET,
674        level = "trace",
675        skip_all,
676        fields(?lower_bound, ?upper_bound),
677    )]
678    fn calculate_key_range<'a>(
679        &mut self,
680        value_encoder: &mut VE,
681        targets: &mut Option<TargetsCursor<'a>>,
682        hashed_cursor_state: &mut HashedCursorState<VE::DeferredEncoder>,
683        lower_bound: Nibbles,
684        upper_bound: Option<Nibbles>,
685    ) -> Result<(), StateProofError> {
686        // A helper closure for mapping entries returned from the `hashed_cursor`, converting the
687        // key to Nibbles and immediately creating the DeferredValueEncoder so that encoding of the
688        // leaf value can begin ASAP.
689        let mut map_hashed_cursor_entry = |(key_b256, val): (B256, _)| {
690            debug_assert_eq!(key_b256.len(), 32);
691            let key = Nibbles::unpack_array(key_b256.as_ref());
692            let val = value_encoder.deferred_encoder(key_b256, val);
693            (key, val)
694        };
695
696        // If the cursor hasn't been used, or the last iterated key is prior to this range's key
697        // range, then seek forward to at least the first key.
698        if hashed_cursor_state.needs_seek_to(&lower_bound) {
699            trace!(
700                target: TRACE_TARGET,
701                current=?hashed_cursor_state.path(),
702                "Seeking hashed cursor to meet lower bound",
703            );
704
705            let lower_key = B256::right_padding_from(&lower_bound.pack());
706            *hashed_cursor_state = HashedCursorState::seeked(
707                lower_bound,
708                self.hashed_cursor.seek(lower_key)?.map(&mut map_hashed_cursor_entry),
709            );
710        }
711
712        // Loop over all keys in the range, calling `push_leaf` on each.
713        while hashed_cursor_state
714            .path()
715            .is_some_and(|key| upper_bound.is_none_or(|upper_bound| key < &upper_bound))
716        {
717            let (key, val) = hashed_cursor_state.take();
718            self.push_leaf(targets, key, val)?;
719            *hashed_cursor_state = HashedCursorState::seeked(
720                key,
721                self.hashed_cursor.next()?.map(&mut map_hashed_cursor_entry),
722            );
723        }
724
725        trace!(target: TRACE_TARGET, "No further keys within range");
726        Ok(())
727    }
728
729    /// Constructs and returns a new [`ProofTrieBranch`] based on an existing [`BranchNodeCompact`].
730    #[inline]
731    const fn new_from_cached_branch(
732        cached_branch: &BranchNodeCompact,
733        ext_len: u8,
734    ) -> ProofTrieBranch {
735        ProofTrieBranch {
736            ext_len,
737            state_mask: TrieMask::new(0),
738            masks: Some(BranchNodeMasks {
739                tree_mask: cached_branch.tree_mask,
740                hash_mask: cached_branch.hash_mask,
741            }),
742        }
743    }
744
745    /// Pushes a new branch onto the `branch_stack` which is based on a cached branch obtained via
746    /// the trie cursor.
747    ///
748    /// If there is already a child at the top branch of `branch_stack` occupying this new branch's
749    /// nibble then that child will have its short-key split with another new branch, and this
750    /// cached branch will be a child of that splitting branch.
751    fn push_cached_branch<'a>(
752        &mut self,
753        targets: &mut Option<TargetsCursor<'a>>,
754        cached_path: Nibbles,
755        cached_branch: &BranchNodeCompact,
756    ) -> Result<(), StateProofError> {
757        debug_assert!(
758            cached_path.starts_with(&self.branch_path),
759            "push_cached_branch called with path {cached_path:?} which is not a child of current branch {:?}",
760            self.branch_path,
761        );
762
763        let parent_branch = self.branch_stack.last();
764
765        // If both stacks are empty then there were no leaves before this cached branch, push it and
766        // be done; the extension of the branch will be its full path.
767        if self.child_stack.is_empty() && parent_branch.is_none() {
768            self.branch_path = cached_path;
769            self.branch_stack
770                .push(Self::new_from_cached_branch(cached_branch, cached_path.len() as u8));
771            return Ok(())
772        }
773
774        // Get the nibble which should be set in the parent branch's `state_mask` for this new
775        // branch.
776        let cached_branch_nibble = cached_path.get_unchecked(self.branch_path.len());
777
778        // We calculate the `ext_len` of the new branch, and potentially update its nibble if a new
779        // parent branch is inserted here, based on the state of the parent branch.
780        let (cached_branch_nibble, ext_len) = if parent_branch
781            .is_none_or(|parent_branch| parent_branch.state_mask.is_bit_set(cached_branch_nibble))
782        {
783            // If the `child_stack` is not empty but the `branch_stack` is then it implies that
784            // there must be a leaf or extension at the root of the trie whose short-key will get
785            // split by a new branch, which will become the parent of both that leaf/extension and
786            // this new branch.
787            //
788            // Similarly, if there is a branch on the `branch_stack` but its `state_mask` bit for
789            // this new branch is already set, then there must be a leaf/extension with a short-key
790            // to be split.
791            debug_assert!(!self
792                .child_stack
793                .last()
794                .expect("already checked for emptiness")
795                .short_key()
796                .is_empty());
797
798            // Split that leaf/extension's short key with a new branch.
799            let (nibble, short_key) = self.push_new_branch(cached_path);
800            (nibble, short_key.len())
801        } else {
802            // If there is a parent branch but its `state_mask` bit for this branch is not set
803            // then we can simply calculate the `ext_len` based on the difference of each, minus
804            // 1 to account for the nibble in the `state_mask`.
805            (cached_branch_nibble, cached_path.len() - self.branch_path.len() - 1)
806        };
807
808        // `commit_last_child` relies on the last set bit of the parent branch's `state_mask` to
809        // determine the path of the last child on the `child_stack`. Since we are about to
810        // change that mask we need to commit that last child first.
811        self.commit_last_child(targets)?;
812
813        // When pushing a new branch we need to set its child nibble in the `state_mask` of
814        // its parent, if there is one.
815        if let Some(parent_branch) = self.branch_stack.last_mut() {
816            parent_branch.state_mask.set_bit(cached_branch_nibble);
817        }
818
819        // Finally update the `branch_path` and push the new branch.
820        self.branch_path = cached_path;
821        self.branch_stack.push(Self::new_from_cached_branch(cached_branch, ext_len as u8));
822
823        trace!(
824            target: TRACE_TARGET,
825            branch=?self.branch_stack.last(),
826            branch_path=?self.branch_path,
827            "Pushed cached branch",
828        );
829
830        Ok(())
831    }
832
833    /// Wraps [`TrieCursor::seek`], skipping cached branches whose sub-tries must be recalculated
834    /// from leaves.
835    ///
836    /// A cached branch is skipped when all but at most one of its children match the prefix set.
837    /// In that case those children might all be deleted, leaving a branch with a single child.
838    /// A single-child branch must be collapsed, but collapsing requires the child to be a full
839    /// node (not a cached hash). Skipping the branch avoids this by forcing recalculation.
840    fn trie_cursor_seek(
841        &mut self,
842        key: Nibbles,
843    ) -> Result<Option<(Nibbles, BranchNodeCompact)>, StateProofError> {
844        let mut entry = self.trie_cursor.seek(key)?;
845        while let Some((ref path, ref branch)) = entry {
846            if !self.should_skip_cached_branch(path, branch) {
847                break
848            }
849            entry = self.trie_cursor.next()?;
850        }
851        Ok(entry)
852    }
853
854    /// Returns true if the cached branch should be skipped entirely and its sub-trie recalculated
855    /// from leaves.
856    fn should_skip_cached_branch(
857        &mut self,
858        cached_path: &Nibbles,
859        cached_branch: &BranchNodeCompact,
860    ) -> bool {
861        if !self.prefix_set.contains(cached_path) {
862            return false
863        }
864
865        let mut num_unmatched = 0u32;
866        let mut child_path = *cached_path;
867        for nibble in 0u8..16 {
868            if cached_branch.state_mask.is_bit_set(nibble) {
869                child_path.truncate(cached_path.len());
870                child_path.push_unchecked(nibble);
871                if !self.prefix_set.contains(&child_path) {
872                    num_unmatched += 1;
873                }
874            }
875        }
876
877        if num_unmatched <= 1 {
878            trace!(
879                target: TRACE_TARGET,
880                ?cached_path,
881                ?num_unmatched,
882                "Skipping cached branch: all but <=1 children match prefix set, branch may collapse",
883            );
884            true
885        } else {
886            false
887        }
888    }
889
890    /// Attempts to pop off the top branch of the `cached_branch_stack`, returning
891    /// [`PopCachedBranchOutcome::Popped`] on success. Returns other variants to indicate that the
892    /// stack is empty and what to do about it.
893    ///
894    /// This method only returns [`PopCachedBranchOutcome::CalculateLeaves`] if there is a cached
895    /// branch on top of the stack.
896    #[inline]
897    fn try_pop_cached_branch(
898        &mut self,
899        trie_cursor_state: &mut TrieCursorState,
900        traversal_upper_bound: Option<&Nibbles>,
901        uncalculated_lower_bound: &Option<Nibbles>,
902    ) -> Result<PopCachedBranchOutcome, StateProofError> {
903        // If the `uncalculated_lower_bound` is None it indicates that there can be no more
904        // leaf data, so similarly there can be no more cached branch data.
905        let Some(uncalculated_lower_bound) = uncalculated_lower_bound else {
906            return Ok(PopCachedBranchOutcome::Exhausted)
907        };
908
909        // If there is a branch on top of the stack we use that.
910        if let Some(cached) = self.cached_branch_stack.pop() {
911            return Ok(PopCachedBranchOutcome::Popped(cached));
912        }
913
914        // There is no cached branch on the stack. It's possible that another one exists
915        // farther on in the trie, but we perform some checks first to prevent unnecessary
916        // attempts to find it.
917
918        // If [`TrieCursorState::path`] returns None it means that the cursor has been
919        // exhausted, so there can be no more cached data.
920        let Some(mut trie_cursor_path) = trie_cursor_state.path() else {
921            return Ok(PopCachedBranchOutcome::Exhausted)
922        };
923
924        // If the trie cursor is seeked to a branch whose leaves have already been processed
925        // then we can't use it, instead we seek forward and try again.
926        if trie_cursor_path < uncalculated_lower_bound {
927            *trie_cursor_state = TrieCursorState::seeked(
928                *uncalculated_lower_bound,
929                self.trie_cursor_seek(*uncalculated_lower_bound)?,
930            );
931
932            // Having just seeked forward we need to check if the cursor is now exhausted,
933            // extracting the new path at the same time.
934            if let Some(new_trie_cursor_path) = trie_cursor_state.path() {
935                trie_cursor_path = new_trie_cursor_path
936            } else {
937                return Ok(PopCachedBranchOutcome::Exhausted)
938            };
939        }
940
941        // If the trie cursor has reached the end of the traversal range then we consider cached
942        // data to be exhausted. The cursor itself remains positioned for reuse by a later range.
943        if traversal_upper_bound.is_some_and(|upper_bound| trie_cursor_path >= upper_bound) {
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
980    /// completed; they will not have any further keys added to them.
981    ///
982    /// Returns a range to calculate if a branch still has dirty keys to process, or popping it
983    /// exposes dirty keys which could split its extension. A missing lower bound disables these
984    /// checks when the caller has already scheduled the remaining range.
985    fn commit_branches<'a>(
986        &mut self,
987        targets: &mut Option<TargetsCursor<'a>>,
988        next_path: &Nibbles,
989        uncalculated_lower_bound: Option<&Nibbles>,
990    ) -> Result<Option<(Nibbles, Option<Nibbles>)>, StateProofError> {
991        let dirty_range = |prefix_set: &mut PrefixSet, upper_bound: Option<Nibbles>| {
992            let uncalculated_lower_bound = uncalculated_lower_bound?;
993
994            if upper_bound.as_ref().is_some_and(|upper| uncalculated_lower_bound >= upper) {
995                return None
996            }
997
998            match upper_bound {
999                Some(upper_bound) => prefix_set
1000                    .contains_range(uncalculated_lower_bound..&upper_bound)
1001                    .then_some((*uncalculated_lower_bound, Some(upper_bound))),
1002                None => prefix_set
1003                    .contains_from(uncalculated_lower_bound)
1004                    .then_some((*uncalculated_lower_bound, None)),
1005            }
1006        };
1007
1008        let mut popped_child_path_upper = None;
1009        while !next_path.starts_with(&self.branch_path) {
1010            // If the lower bound is still within this branch, process any remaining dirty keys
1011            // before popping it so they can be added directly to the branch.
1012            if uncalculated_lower_bound.is_some_and(|lower| lower.starts_with(&self.branch_path)) &&
1013                let Some(range) =
1014                    dirty_range(&mut self.prefix_set, self.branch_path.next_without_prefix())
1015            {
1016                return Ok(Some(range))
1017            }
1018
1019            let branch = self.branch_stack.last().expect("branch_stack cannot be empty");
1020            // Once popped, this branch becomes a child at this path. Its upper bound therefore
1021            // covers any keys which could split the branch's extension on the right.
1022            popped_child_path_upper = Some(
1023                self.branch_path
1024                    .slice_unchecked(0, self.branch_path.len() - branch.ext_len as usize)
1025                    .next_without_prefix(),
1026            );
1027
1028            self.pop_branch(targets)?;
1029        }
1030
1031        // An empty branch_stack is skipped because a popped local root does not need this check:
1032        // any gap before `next_path` was already returned by `try_pop_cached_branch`, and forward
1033        // traversal will split its extension and process later dirty keys as needed.
1034        if !self.branch_stack.is_empty() &&
1035            let Some(upper_bound) = popped_child_path_upper &&
1036            let Some(range) = dirty_range(&mut self.prefix_set, upper_bound)
1037        {
1038            return Ok(Some(range))
1039        }
1040
1041        Ok(None)
1042    }
1043
1044    /// Accepts the current state of both hashed and trie cursors, and determines the next range of
1045    /// hashed keys which need to be processed using [`Self::push_leaf`].
1046    ///
1047    /// This method will use cached branch node data from the trie cursor to skip over all possible
1048    /// ranges of keys, to reduce computation as much as possible.
1049    ///
1050    /// # Returns
1051    ///
1052    /// - `None`: No more data to process, finish computation
1053    ///
1054    /// - `Some(lower, None)`: Indicates to call `push_leaf` on all keys starting at `lower`, with
1055    ///   no upper bound. This method won't be called again after this.
1056    ///
1057    /// - `Some(lower, Some(upper))`: Indicates to call `push_leaf` on all keys starting at `lower`,
1058    ///   up to but excluding `upper`, and then call this method once done.
1059    ///
1060    /// Once returned the `branch_stack` will be in the correct state to start calculating leaves
1061    /// for the given range, if any.
1062    #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
1063    fn next_uncached_key_range<'a>(
1064        &mut self,
1065        targets: &mut Option<TargetsCursor<'a>>,
1066        trie_cursor_state: &mut TrieCursorState,
1067        traversal_upper_bound: Option<&Nibbles>,
1068        mut uncalculated_lower_bound: Option<Nibbles>,
1069    ) -> Result<Option<(Nibbles, Option<Nibbles>)>, StateProofError> {
1070        loop {
1071            if let (Some(lower_bound), Some(upper_bound)) =
1072                (uncalculated_lower_bound.as_ref(), traversal_upper_bound) &&
1073                lower_bound >= upper_bound
1074            {
1075                return Ok(None)
1076            }
1077
1078            // Pop the currently cached branch node.
1079            //
1080            // NOTE we pop off the `cached_branch_stack` because cloning the `BranchNodeCompact`
1081            // means cloning an Arc, which incurs synchronization overhead. We have to be sure to
1082            // push the cached branch back onto the stack once done.
1083            let (cached_path, cached_branch) = match self.try_pop_cached_branch(
1084                trie_cursor_state,
1085                traversal_upper_bound,
1086                &uncalculated_lower_bound,
1087            )? {
1088                PopCachedBranchOutcome::Popped(cached) => cached,
1089                PopCachedBranchOutcome::Exhausted => {
1090                    // If cached branches are exhausted it's possible that there is still an
1091                    // unbounded range of leaves to be processed. `uncalculated_lower_bound` is
1092                    // used to return that range.
1093                    trace!(target: TRACE_TARGET, ?uncalculated_lower_bound, "Exhausted cached trie nodes");
1094                    if let Some(lower) = uncalculated_lower_bound {
1095                        self.commit_branches(targets, &lower, None)?;
1096                        return Ok(Some((lower, traversal_upper_bound.copied())));
1097                    }
1098                    return Ok(None)
1099                }
1100                PopCachedBranchOutcome::CalculateLeaves(range) => {
1101                    self.commit_branches(targets, &range.0, None)?;
1102                    return Ok(Some(range));
1103                }
1104            };
1105
1106            let uncalculated_lower_bound_ref = uncalculated_lower_bound
1107                .as_ref()
1108                .expect("try_pop_cached_branch would return Exhausted if this were None");
1109
1110            trace!(
1111                target: TRACE_TARGET,
1112                branch_path = ?self.branch_path,
1113                branch_state_mask = ?self.branch_stack.last().map(|b| b.state_mask),
1114                ?cached_path,
1115                cached_branch_state_mask = ?cached_branch.state_mask,
1116                cached_branch_hash_mask = ?cached_branch.hash_mask,
1117                "loop",
1118            );
1119
1120            if let Some(range) =
1121                self.commit_branches(targets, &cached_path, Some(uncalculated_lower_bound_ref))?
1122            {
1123                self.cached_branch_stack.push((cached_path, cached_branch));
1124                return Ok(Some(range))
1125            }
1126
1127            // Since we've popped all branches which don't start with cached_path, branch_path at
1128            // this point must be equal to or shorter than cached_path.
1129            debug_assert!(
1130                self.branch_path.len() < cached_path.len() || self.branch_path == cached_path,
1131                "branch_path {:?} is different-or-longer-than cached_path {cached_path:?}",
1132                self.branch_path
1133            );
1134
1135            // If the branch_path != cached_path it means the branch_stack is either empty, or the
1136            // top branch is the parent of this cached branch. Either way we push a branch
1137            // corresponding to the cached one onto the stack, so we can begin constructing it.
1138            if self.branch_path != cached_path {
1139                // If the prefix set contains any entries from the lower bound up until the new
1140                // cached path it means that there might be a new node(s) which split the extension
1141                // node between cached_path and its parent (self.branch_path).
1142                if uncalculated_lower_bound_ref < &cached_path &&
1143                    self.prefix_set.contains_range(uncalculated_lower_bound_ref..&cached_path)
1144                {
1145                    self.cached_branch_stack.push((cached_path, cached_branch));
1146                    return Ok(Some((*uncalculated_lower_bound_ref, Some(cached_path))))
1147                }
1148
1149                self.push_cached_branch(targets, cached_path, &cached_branch)?;
1150            }
1151
1152            // At this point the top of the branch stack is the same branch which was found in the
1153            // cache.
1154            let curr_branch =
1155                self.branch_stack.last().expect("top of branch_stack corresponds to cached branch");
1156
1157            let cached_state_mask = cached_branch.state_mask;
1158            let curr_state_mask = curr_branch.state_mask;
1159
1160            // Determine all child nibbles which are set in the cached branch but not the
1161            // under-construction branch.
1162            let mut next_child_nibbles = curr_state_mask ^ cached_state_mask;
1163
1164            // Also include child nibbles indicated by the prefix set. The prefix set can
1165            // indicate children that need recalculation from leaves (e.g. new keys inserted
1166            // under this branch). Skip nibbles already set in `curr_state_mask` since those
1167            // children have already been constructed.
1168            if self.prefix_set.contains(&self.branch_path) {
1169                let branch_path_len = self.branch_path.len();
1170                let mut child_path = self.branch_path;
1171                for nibble in 0u8..16 {
1172                    if !curr_state_mask.is_bit_set(nibble) {
1173                        child_path.truncate(branch_path_len);
1174                        child_path.push_unchecked(nibble);
1175                        if self.prefix_set.contains(&child_path) {
1176                            next_child_nibbles.set_bit(nibble);
1177                        }
1178                    }
1179                }
1180            }
1181
1182            let _orig_next_child_nibbles = next_child_nibbles;
1183
1184            // Mask out any child nibbles whose ranges have already been fully processed.
1185            // This can happen when `calculate_key_range` finds no keys for a child's range,
1186            // leaving the child's bit unset in `state_mask`. Without this, re-entering this
1187            // function would select the same child again.
1188            if uncalculated_lower_bound_ref.starts_with(&self.branch_path) &&
1189                uncalculated_lower_bound_ref.len() > self.branch_path.len()
1190            {
1191                let lower_nibble =
1192                    uncalculated_lower_bound_ref.get_unchecked(self.branch_path.len());
1193                // Clear all nibbles strictly below `lower_nibble` since they've been processed.
1194                let already_processed_mask = TrieMask::new((1u16 << lower_nibble) - 1);
1195                next_child_nibbles &= !already_processed_mask;
1196                trace!(
1197                    target: TRACE_TARGET,
1198                    branch_path = ?self.branch_path,
1199                    ?_orig_next_child_nibbles,
1200                    ?already_processed_mask,
1201                    ?next_child_nibbles,
1202                    "Unset already processed key nibbles from next_child_nibbles",
1203                );
1204            } else if !uncalculated_lower_bound_ref.starts_with(&self.branch_path) &&
1205                uncalculated_lower_bound_ref > &self.branch_path
1206            {
1207                // The lower bound has moved entirely past this branch (e.g. branch is 0x6 but
1208                // lower is 0x7). All remaining children have been processed.
1209                next_child_nibbles = TrieMask::default();
1210                trace!(
1211                    target: TRACE_TARGET,
1212                    branch_path = ?self.branch_path,
1213                    ?_orig_next_child_nibbles,
1214                    ?next_child_nibbles,
1215                    "Unset all nibbles from next_child_nibbles due to branch_path being outside this subtrie",
1216                );
1217            }
1218
1219            // Defer popping this completed branch to `commit_branches`, which checks for dirty
1220            // keys around it before looping with the parent branch.
1221            if next_child_nibbles.is_empty() {
1222                trace!(
1223                    target: TRACE_TARGET,
1224                    path=?cached_path,
1225                    ?curr_branch,
1226                    ?cached_branch,
1227                    "No further children",
1228                );
1229
1230                // no need to pop from `cached_branch_stack`, the current cached branch is already
1231                // popped (see note at the top of the loop).
1232
1233                // The completed branch has no more keys with its prefix. Set the lower bound which
1234                // can be returned from this method to be the next possible prefix, if any.
1235                uncalculated_lower_bound = cached_path.next_without_prefix();
1236
1237                continue
1238            }
1239
1240            // Determine the next nibble of the branch which has not yet been constructed, and
1241            // determine the child's full path.
1242            let child_nibble = next_child_nibbles.trailing_zeros() as u8;
1243            let child_path = self.child_path_at(child_nibble);
1244
1245            // If the `hash_mask` bit is set for the next child it means the child's hash is cached
1246            // in the `cached_branch`. We can use that instead of re-calculating the hash of the
1247            // entire sub-trie.
1248            //
1249            // If the child needs to be retained for a proof then we should not use the cached
1250            // hash, and instead continue on to calculate its node manually.
1251            //
1252            // If the child's path is in the prefix set then the cached hash is stale and must
1253            // not be used.
1254            if cached_branch.hash_mask.is_bit_set(child_nibble) &&
1255                !self.prefix_set.contains(&child_path)
1256            {
1257                // Commit the last child. We do this here for two reasons:
1258                // - `commit_last_child` will check if the last child needs to be retained. We need
1259                //   to check that before the subsequent `should_retain` call here to prevent
1260                //   `targets` from being moved beyond the last child before it is checked.
1261                // - If we do end up using the cached hash value, then we will need to commit the
1262                //   last child before pushing a new one onto the stack anyway.
1263                self.commit_last_child(targets)?;
1264
1265                if !self.should_retain(targets, &child_path, false) {
1266                    // Pull this child's hash out of the cached branch node. The hash index
1267                    // is the number of hash_mask bits set below this child's nibble.
1268                    let lower_bits = TrieMask::new((1u16 << child_nibble) - 1);
1269                    let hash_idx = (cached_branch.hash_mask & lower_bits).count_ones() as usize;
1270                    let hash = cached_branch.hashes[hash_idx];
1271
1272                    trace!(
1273                        target: TRACE_TARGET,
1274                        ?child_path,
1275                        ?hash_idx,
1276                        ?hash,
1277                        "Using cached hash for child",
1278                    );
1279
1280                    self.child_stack.push(ProofTrieBranchChild::RlpNode(RlpNode::word_rlp(&hash)));
1281                    self.branch_stack
1282                        .last_mut()
1283                        .expect("already asserted there is a last branch")
1284                        .state_mask
1285                        .set_bit(child_nibble);
1286
1287                    // Update the `uncalculated_lower_bound` to indicate that the child whose bit
1288                    // was just set is completely processed.
1289                    uncalculated_lower_bound = child_path.next_without_prefix();
1290
1291                    // Push the current cached branch back onto the stack before looping.
1292                    self.cached_branch_stack.push((cached_path, cached_branch));
1293
1294                    continue
1295                }
1296            }
1297
1298            // We now want to check if there is a cached branch node at this child. The cached
1299            // branch node may be the node at this child directly, or this child may be an
1300            // extension and the cached branch is the child of that extension.
1301
1302            // All trie nodes prior to `child_path` will not be modified further, so we can seek the
1303            // trie cursor to the next cached node at-or-after `child_path`.
1304            if trie_cursor_state.path().is_some_and(|path| path < &child_path) {
1305                trace!(target: TRACE_TARGET, ?child_path, "Seeking trie cursor to child path");
1306                *trie_cursor_state =
1307                    TrieCursorState::seeked(child_path, self.trie_cursor_seek(child_path)?);
1308            }
1309
1310            // If the next cached branch node is a child of `child_path` then we can assume it is
1311            // the cached branch for this child. We push it onto the `cached_branch_stack` and loop
1312            // back to the top.
1313            if let TrieCursorState::Available(next_cached_path, next_cached_branch) =
1314                &trie_cursor_state &&
1315                next_cached_path.starts_with(&child_path)
1316            {
1317                // Push the current cached branch back on before pushing its child and then looping
1318                self.cached_branch_stack.push((cached_path, cached_branch));
1319
1320                trace!(
1321                    target: TRACE_TARGET,
1322                    ?child_path,
1323                    ?next_cached_path,
1324                    ?next_cached_branch,
1325                    "Pushing cached branch for child",
1326                );
1327                self.cached_branch_stack.push(trie_cursor_state.take());
1328                continue;
1329            }
1330
1331            // There is no cached data for the sub-trie at this child, we must recalculate the
1332            // sub-trie root (this child) using the leaves. Return the range of keys based on the
1333            // child path.
1334            let child_path_upper = child_path.next_without_prefix();
1335            trace!(
1336                target: TRACE_TARGET,
1337                lower=?child_path,
1338                upper=?child_path_upper,
1339                "Returning sub-trie's key range to calculate",
1340            );
1341
1342            // Push the current cached branch back onto the stack before returning.
1343            self.cached_branch_stack.push((cached_path, cached_branch));
1344
1345            return Ok(Some((child_path, child_path_upper)));
1346        }
1347    }
1348
1349    /// Calculates trie nodes and retains proofs for targeted nodes within a sub-trie. The
1350    /// sub-trie's bounds are denoted by the `lower_bound` and `upper_bound` arguments,
1351    /// `upper_bound` is exclusive, None indicates unbounded.
1352    #[instrument(
1353        target = TRACE_TARGET,
1354        level = "trace",
1355        skip_all,
1356        fields(
1357            parent_prefix=?sub_trie_targets.parent_prefix,
1358            lower_bound=?sub_trie_targets.lower_bound,
1359            upper_bound=?sub_trie_targets.upper_bound,
1360        ),
1361    )]
1362    fn proof_subtrie<'a>(
1363        &mut self,
1364        value_encoder: &mut VE,
1365        trie_cursor_state: &mut TrieCursorState,
1366        hashed_cursor_state: &mut HashedCursorState<VE::DeferredEncoder>,
1367        sub_trie_targets: SubTrieTargets<'a>,
1368    ) -> Result<(), StateProofError> {
1369        let traversal_lower_bound = sub_trie_targets.lower_bound;
1370        let traversal_upper_bound = sub_trie_targets.upper_bound;
1371
1372        // Wrap targets into a `TargetsCursor`.  targets can be empty if we only want to calculate
1373        // the root, in which case we don't need a cursor.
1374        let mut targets = if sub_trie_targets.targets.is_empty() {
1375            None
1376        } else {
1377            Some(TargetsCursor::new(sub_trie_targets.targets))
1378        };
1379
1380        // Ensure initial state is cleared. By the end of the method call these should be empty once
1381        // again.
1382        debug_assert!(self.cached_branch_stack.is_empty());
1383        debug_assert!(self.branch_stack.is_empty());
1384        debug_assert!(self.branch_path.is_empty());
1385        debug_assert!(self.child_stack.is_empty());
1386
1387        // `next_uncached_key_range`, which will be called in the loop below, expects the trie
1388        // cursor to have already been positioned. Cursor resets for overlapping sub-tries are
1389        // handled by `proof_inner`, so a buffered entry at-or-after this disjoint range remains the
1390        // first unconsumed entry. Exhaustion is similarly stable across forward-only ranges.
1391        if trie_cursor_state.needs_seek_to(&traversal_lower_bound) {
1392            trace!(target: TRACE_TARGET, "Doing initial seek of trie cursor");
1393            *trie_cursor_state = TrieCursorState::seeked(
1394                traversal_lower_bound,
1395                self.trie_cursor_seek(traversal_lower_bound)?,
1396            );
1397        }
1398
1399        // `uncalculated_lower_bound` tracks the lower bound of node paths which have yet to be
1400        // visited, either via the hashed key cursor (`calculate_key_range`) or trie cursor
1401        // (`next_uncached_key_range`). If/when this becomes None then there are no further nodes
1402        // which could exist.
1403        let mut uncalculated_lower_bound = Some(traversal_lower_bound);
1404
1405        trace!(target: TRACE_TARGET, "Starting loop");
1406        loop {
1407            // Save the previous lower bound to detect forward progress.
1408            let prev_uncalculated_lower_bound = uncalculated_lower_bound;
1409
1410            // Determine the range of keys of the overall trie which need to be re-computed.
1411            let Some((calc_lower_bound, calc_upper_bound)) = self.next_uncached_key_range(
1412                &mut targets,
1413                trie_cursor_state,
1414                traversal_upper_bound.as_ref(),
1415                prev_uncalculated_lower_bound,
1416            )?
1417            else {
1418                // If `next_uncached_key_range` determines that there can be no more keys then
1419                // complete the computation.
1420                break;
1421            };
1422
1423            // Forward-progress guard: detect trie inconsistencies that would cause infinite loops.
1424            // If `next_uncached_key_range` returns a range that starts before the previous
1425            // lower bound, we've gone backwards and would loop forever.
1426            //
1427            // This can specifically happen when there is a cached branch which shouldn't exist, or
1428            // if state mask bit is set on a cached branch which shouldn't be.
1429            if let Some(prev_lower) = prev_uncalculated_lower_bound.as_ref() &&
1430                calc_lower_bound < *prev_lower
1431            {
1432                let msg = format!(
1433                    "next_uncached_key_range went backwards: calc_lower={calc_lower_bound:?} < \
1434                     prev_lower={prev_lower:?}, calc_upper={calc_upper_bound:?}, \
1435                     lower_bound={traversal_lower_bound:?}, \
1436                     upper_bound={traversal_upper_bound:?}",
1437                );
1438                error!(target: TRACE_TARGET, "{msg}");
1439                return Err(StateProofError::TrieInconsistency(msg));
1440            }
1441
1442            // Calculate the trie for that range of keys
1443            self.calculate_key_range(
1444                value_encoder,
1445                &mut targets,
1446                hashed_cursor_state,
1447                calc_lower_bound,
1448                calc_upper_bound,
1449            )?;
1450
1451            // Once outside `calculate_key_range`, `hashed_cursor_state` will be at the first key
1452            // after the range, or exhausted.
1453            //
1454            // If the hashed cursor is exhausted, or has reached the end of the traversal range,
1455            // then there are no more keys which can contribute to these target children.
1456            if hashed_cursor_state.path().is_none_or(|key| {
1457                traversal_upper_bound.is_some_and(|upper_bound| key >= &upper_bound)
1458            }) {
1459                break;
1460            }
1461
1462            // The upper bound of previous calculation becomes the lower bound of the uncalculated
1463            // range, for which we'll once again check for cached data.
1464            uncalculated_lower_bound = calc_upper_bound;
1465        }
1466
1467        // Once there's no more leaves we can pop the remaining branches, if any.
1468        trace!(target: TRACE_TARGET, "Exited loop, popping remaining branches");
1469        while !self.branch_stack.is_empty() {
1470            self.pop_branch(&mut targets)?;
1471        }
1472
1473        // At this point the branch stack should be empty. If the child stack is empty it means no
1474        // keys were ever iterated from the hashed cursor in the first place. Otherwise there should
1475        // only be a single node left: the root node.
1476        debug_assert!(self.branch_stack.is_empty());
1477        debug_assert!(self.branch_path.is_empty());
1478        debug_assert!(self.child_stack.len() < 2);
1479
1480        // The `cached_branch_stack` may still have cached branches on it, as it's not affected by
1481        // `pop_branch`, but it is no longer needed and should be cleared.
1482        self.cached_branch_stack.clear();
1483
1484        // We always pop the local root node off of the `child_stack` in order to empty it. If the
1485        // parent branch is already known, compressed roots need to be rebased into a direct child
1486        // of that parent before they can be attached to it.
1487        trace!(
1488            target: TRACE_TARGET,
1489            parent_prefix = ?sub_trie_targets.parent_prefix,
1490            child_stack_empty = self.child_stack.is_empty(),
1491            "Maybe retaining local root",
1492        );
1493        // Either there was only a single leaf node placed on the child stack, or the final branch
1494        // was popped off the branch stack and placed unencoded on the child stack. Either way the
1495        // child stack will not have an RlpNode at this point.
1496        let root_node = match self.child_stack.pop() {
1497            Some(ProofTrieBranchChild::RlpNode(_)) => {
1498                unreachable!("local root cannot be an encoded RLP node")
1499            }
1500            root_node => root_node,
1501        };
1502
1503        // A full-trie calculation always retains a root, using an empty root when traversal
1504        // produced no root node.
1505        let Some(parent_prefix) = sub_trie_targets.parent_prefix else {
1506            let root_node = if let Some(root_node) = root_node {
1507                self.rlp_encode_buf.clear();
1508                root_node.into_proof_trie_node(Nibbles::new(), &mut self.rlp_encode_buf)?
1509            } else {
1510                ProofTrieNodeV2::empty()
1511            };
1512            self.retained_proofs.push(root_node);
1513            return Ok(())
1514        };
1515
1516        // If there's no root node then the subtrie has no keys, return nothing.
1517        let Some(mut root_node) = root_node else { return Ok(()) };
1518
1519        let root_short_key = *root_node.short_key();
1520
1521        // An exact match reconstructed the already-revealed parent; its targeted children were
1522        // retained while that parent branch was popped.
1523        if root_short_key == parent_prefix {
1524            return Ok(())
1525        }
1526
1527        // At this point we have a "root" node which the calculator has based at 0x (empty path),
1528        // but the subtrie targets indicate that there is a known parent branch at parent_prefix
1529        // which this root should be rebased onto.
1530
1531        // The local root of a partial calculation must be at or below its known parent.
1532        if !root_short_key.starts_with(&parent_prefix) {
1533            return Err(StateProofError::TrieInconsistency(format!(
1534                "local root short key {root_short_key:?} does not start with parent prefix \
1535                 {parent_prefix:?}",
1536            )))
1537        }
1538
1539        // Keep the parent branch's child nibble in the proof path so the local root attaches
1540        // directly below that parent.
1541        let child_path_len = parent_prefix.len() + 1;
1542        let child_path = root_short_key.slice_unchecked(0, child_path_len);
1543
1544        // It's possible that the local root lies on a child which is not targeted.
1545        if !sub_trie_targets
1546            .targets
1547            .iter()
1548            .any(|target| target.key_nibbles.starts_with(&child_path))
1549        {
1550            return Ok(())
1551        }
1552
1553        // Retain the requested child with only the path below its parent edge in the short key.
1554        root_node.trim_short_key_prefix(child_path_len);
1555        self.rlp_encode_buf.clear();
1556        let root_node = root_node.into_proof_trie_node(child_path, &mut self.rlp_encode_buf)?;
1557        self.retained_proofs.push(root_node);
1558
1559        Ok(())
1560    }
1561
1562    /// Clears internal computation state. Called after errors to ensure the calculator is not
1563    /// left in a partially-computed state when reused.
1564    fn clear_computation_state(&mut self) {
1565        self.branch_stack.clear();
1566        self.branch_path = Nibbles::new();
1567        self.child_stack.clear();
1568        self.cached_branch_stack.clear();
1569        self.retained_proofs.clear();
1570    }
1571
1572    /// Internal implementation of proof calculation. Assumes both cursors have already been reset.
1573    /// See docs on [`Self::proof`] for expected behavior.
1574    fn proof_inner(
1575        &mut self,
1576        value_encoder: &mut VE,
1577        targets: &mut [ProofV2Target],
1578    ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1579        // If there are no targets then nothing could be returned, return early.
1580        if targets.is_empty() {
1581            trace!(target: TRACE_TARGET, "Empty targets, returning");
1582            return Ok(Vec::new())
1583        }
1584
1585        // Initialize the variables which track the state of the two cursors. Both indicate the
1586        // cursors are unseeked.
1587        let mut trie_cursor_state = TrieCursorState::unseeked();
1588        let mut hashed_cursor_state = HashedCursorState::unseeked();
1589        let mut previous_traversal_bounds: Option<(Nibbles, Option<Nibbles>)> = None;
1590
1591        // Divide targets into bounded ranges, each corresponding to the direct children of one
1592        // already-revealed parent, and handle all proofs within that range.
1593        for sub_trie_targets in iter_sub_trie_targets(targets) {
1594            let traversal_lower_bound = sub_trie_targets.lower_bound;
1595            let traversal_upper_bound = sub_trie_targets.upper_bound;
1596            if previous_traversal_bounds.is_some_and(|(_, previous_upper_bound)| {
1597                previous_upper_bound.is_none_or(|upper_bound| upper_bound > traversal_lower_bound)
1598            }) {
1599                if trie_cursor_state.needs_reset_before_seek(&traversal_lower_bound) {
1600                    trace!(
1601                        target: TRACE_TARGET,
1602                        ?previous_traversal_bounds,
1603                        ?traversal_lower_bound,
1604                        ?traversal_upper_bound,
1605                        "Resetting trie cursor before overlapping or backward traversal range",
1606                    );
1607                    self.trie_cursor.reset();
1608                    trie_cursor_state = TrieCursorState::unseeked();
1609                }
1610                if hashed_cursor_state.needs_reset_before_seek(&traversal_lower_bound) {
1611                    trace!(
1612                        target: TRACE_TARGET,
1613                        ?previous_traversal_bounds,
1614                        ?traversal_lower_bound,
1615                        ?traversal_upper_bound,
1616                        "Resetting hashed cursor before overlapping or backward traversal range",
1617                    );
1618                    self.hashed_cursor.reset();
1619                    hashed_cursor_state = HashedCursorState::unseeked();
1620                }
1621            }
1622
1623            if let Err(err) = self.proof_subtrie(
1624                value_encoder,
1625                &mut trie_cursor_state,
1626                &mut hashed_cursor_state,
1627                sub_trie_targets,
1628            ) {
1629                self.clear_computation_state();
1630                return Err(err);
1631            }
1632
1633            previous_traversal_bounds = Some((traversal_lower_bound, traversal_upper_bound));
1634        }
1635
1636        trace!(
1637            target: TRACE_TARGET,
1638            retained_proofs_len = ?self.retained_proofs.len(),
1639            "proof_inner: returning",
1640        );
1641        self.retained_proofs.sort_unstable_by(|a, b| depth_first::cmp(&a.path, &b.path));
1642        self.retained_proofs.dedup_by(|a, b| a.path == b.path);
1643        Ok(core::mem::take(&mut self.retained_proofs))
1644    }
1645
1646    /// Generate a proof for the given targets.
1647    ///
1648    /// Given a set of [`ProofV2Target`]s, returns nodes whose paths are a prefix of any target. The
1649    /// returned nodes will be sorted depth-first by path.
1650    ///
1651    /// # Panics
1652    ///
1653    /// In debug builds, panics if the targets are not sorted lexicographically.
1654    #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
1655    pub fn proof(
1656        &mut self,
1657        value_encoder: &mut VE,
1658        targets: &mut [ProofV2Target],
1659    ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1660        self.trie_cursor.reset();
1661        self.hashed_cursor.reset();
1662        self.proof_inner(value_encoder, targets)
1663    }
1664
1665    /// Computes the root hash from a set of proof nodes.
1666    ///
1667    /// Returns `None` if there is no root node (partial proof), otherwise returns the hash of the
1668    /// root node.
1669    ///
1670    /// This method reuses the internal RLP encode buffer for efficiency.
1671    pub fn compute_root_hash(
1672        &mut self,
1673        proof_nodes: &[ProofTrieNodeV2],
1674    ) -> Result<Option<B256>, StateProofError> {
1675        // Find the root node (node at empty path)
1676        let root_node = proof_nodes.iter().find(|node| node.path.is_empty());
1677
1678        let Some(root) = root_node else {
1679            return Ok(None);
1680        };
1681
1682        // Compute the hash of the root node
1683        self.rlp_encode_buf.clear();
1684        root.node.encode(&mut self.rlp_encode_buf);
1685        let root_hash = keccak256(&self.rlp_encode_buf);
1686
1687        Ok(Some(root_hash))
1688    }
1689
1690    /// Calculates the root node of the trie.
1691    ///
1692    /// This method does not accept targets nor retain proofs. Returns the root node which can
1693    /// be used to compute the root hash via [`Self::compute_root_hash`].
1694    #[instrument(target = TRACE_TARGET, level = "trace", skip(self, value_encoder))]
1695    pub fn root_node(
1696        &mut self,
1697        value_encoder: &mut VE,
1698    ) -> Result<ProofTrieNodeV2, StateProofError> {
1699        // Initialize the variables which track the state of the two cursors. Both indicate the
1700        // cursors are unseeked.
1701        let mut trie_cursor_state = TrieCursorState::unseeked();
1702        let mut hashed_cursor_state = HashedCursorState::unseeked();
1703
1704        static EMPTY_TARGETS: [ProofV2Target; 0] = [];
1705        let sub_trie_targets = SubTrieTargets {
1706            lower_bound: Nibbles::new(),
1707            upper_bound: None,
1708            parent_prefix: None,
1709            targets: &EMPTY_TARGETS,
1710        };
1711
1712        if let Err(err) = self.proof_subtrie(
1713            value_encoder,
1714            &mut trie_cursor_state,
1715            &mut hashed_cursor_state,
1716            sub_trie_targets,
1717        ) {
1718            self.clear_computation_state();
1719            return Err(err);
1720        }
1721
1722        // `proof_subtrie` retains the root node when there is no known parent, regardless of
1723        // whether there are any targets.
1724        let mut proofs = core::mem::take(&mut self.retained_proofs);
1725        trace!(
1726            target: TRACE_TARGET,
1727            proofs_len = ?proofs.len(),
1728            "root_node: extracting root",
1729        );
1730
1731        // The root node is at the empty path. Since there is no parent and targets is empty, there
1732        // should be no other retained proofs.
1733        debug_assert_eq!(
1734            proofs.len(), 1,
1735            "prefix is empty, parent path is None, and targets is empty, so there must be only the root node"
1736        );
1737
1738        // Find and remove the root node (node at empty path)
1739        let root_node = proofs.pop().expect("prefix is empty, parent path is None, and targets is empty, so there must be only the root node");
1740
1741        Ok(root_node)
1742    }
1743}
1744
1745/// A proof calculator for storage tries.
1746pub type StorageProofCalculator<TC, HC> = ProofCalculator<TC, HC, StorageValueEncoder>;
1747
1748impl<TC, HC> StorageProofCalculator<TC, HC>
1749where
1750    TC: TrieStorageCursor,
1751    HC: HashedStorageCursor<Value = U256>,
1752{
1753    /// Create a new [`StorageProofCalculator`] instance.
1754    pub fn new_storage(trie_cursor: TC, hashed_cursor: HC) -> Self {
1755        Self::new(trie_cursor, hashed_cursor)
1756    }
1757
1758    /// Generate a proof for a storage trie at the given hashed address.
1759    ///
1760    /// Given a set of [`ProofV2Target`]s, returns nodes whose paths are a prefix of any target. The
1761    /// returned nodes will be sorted depth-first by path.
1762    ///
1763    /// # Panics
1764    ///
1765    /// In debug builds, panics if the targets are not sorted lexicographically.
1766    #[instrument(target = TRACE_TARGET, level = "trace", skip(self, targets))]
1767    pub fn storage_proof(
1768        &mut self,
1769        hashed_address: B256,
1770        targets: &mut [ProofV2Target],
1771    ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1772        self.hashed_cursor.set_hashed_address(hashed_address);
1773
1774        // Shortcut: check if storage is empty
1775        if self.hashed_cursor.is_storage_empty()? {
1776            return Ok(if targets.iter().any(|target| !target.parent.is_known()) {
1777                vec![ProofTrieNodeV2 {
1778                    path: Nibbles::default(),
1779                    node: TrieNodeV2::EmptyRoot,
1780                    masks: None,
1781                }]
1782            } else {
1783                Vec::new()
1784            })
1785        }
1786
1787        // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has
1788        // been checked.
1789        self.trie_cursor.set_hashed_address(hashed_address);
1790
1791        // Create a mutable storage value encoder
1792        let mut storage_value_encoder = StorageValueEncoder;
1793        self.proof_inner(&mut storage_value_encoder, targets)
1794    }
1795
1796    /// Calculates the root node of a storage trie.
1797    ///
1798    /// This method does not accept targets nor retain proofs. Returns the root node which can
1799    /// be used to compute the root hash via [`Self::compute_root_hash`].
1800    #[instrument(target = TRACE_TARGET, level = "trace", skip(self))]
1801    pub fn storage_root_node(
1802        &mut self,
1803        hashed_address: B256,
1804    ) -> Result<ProofTrieNodeV2, StateProofError> {
1805        self.hashed_cursor.set_hashed_address(hashed_address);
1806
1807        if self.hashed_cursor.is_storage_empty()? {
1808            return Ok(ProofTrieNodeV2 {
1809                path: Nibbles::default(),
1810                node: TrieNodeV2::EmptyRoot,
1811                masks: None,
1812            })
1813        }
1814
1815        // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has
1816        // been checked.
1817        self.trie_cursor.set_hashed_address(hashed_address);
1818
1819        // Create a mutable storage value encoder
1820        let mut storage_value_encoder = StorageValueEncoder;
1821        self.root_node(&mut storage_value_encoder)
1822    }
1823}
1824
1825/// Helper type wrapping a slice of [`ProofV2Target`]s, primarily used to iterate through targets in
1826/// [`ProofCalculator::should_retain`].
1827///
1828/// It is assumed that the underlying slice is never empty, and that the iterator is never
1829/// exhausted.
1830struct TargetsCursor<'a> {
1831    targets: &'a [ProofV2Target],
1832    i: usize,
1833}
1834
1835impl<'a> TargetsCursor<'a> {
1836    /// Wraps a slice of [`ProofV2Target`]s with the `TargetsCursor`.
1837    ///
1838    /// # Panics
1839    ///
1840    /// Will panic in debug mode if called with an empty slice.
1841    fn new(targets: &'a [ProofV2Target]) -> Self {
1842        debug_assert!(!targets.is_empty());
1843        Self { targets, i: 0 }
1844    }
1845
1846    /// Returns the current and next [`ProofV2Target`] that the cursor is pointed at.
1847    fn current(&self) -> (&'a ProofV2Target, Option<&'a ProofV2Target>) {
1848        (&self.targets[self.i], self.targets.get(self.i + 1))
1849    }
1850
1851    /// Iterates the cursor forward.
1852    ///
1853    /// # Panics
1854    ///
1855    /// Will panic if the cursor is exhausted.
1856    fn next(&mut self) -> (&'a ProofV2Target, Option<&'a ProofV2Target>) {
1857        self.i += 1;
1858        debug_assert!(self.i < self.targets.len());
1859        self.current()
1860    }
1861
1862    // Iterate forwards over the slice, starting from the [`ProofV2Target`] after the current.
1863    fn skip_iter(&self) -> impl Iterator<Item = &'a ProofV2Target> {
1864        self.targets[self.i + 1..].iter()
1865    }
1866
1867    /// Iterated backwards over the slice, starting from the [`ProofV2Target`] previous to the
1868    /// current.
1869    fn rev_iter(&self) -> impl Iterator<Item = &'a ProofV2Target> {
1870        self.targets[..self.i].iter().rev()
1871    }
1872}
1873
1874/// Used to track the state of the trie cursor, allowing us to differentiate between a branch having
1875/// been taken (used as a cached branch) and the cursor having been exhausted.
1876#[derive(Debug)]
1877enum TrieCursorState {
1878    /// The initial state of the cursor, indicating it's never been seeked.
1879    Unseeked,
1880    /// Cursor is seeked to this path and the node has not been used yet.
1881    Available(Nibbles, BranchNodeCompact),
1882    /// Cursor is seeked to this path, but the node has been used.
1883    Taken(Nibbles),
1884    /// Cursor has been exhausted after seeking from the given lower bound.
1885    Exhausted(Nibbles),
1886}
1887
1888impl TrieCursorState {
1889    /// Creates a [`Self::Unseeked`] based on an entry returned from the cursor itself.
1890    const fn unseeked() -> Self {
1891        Self::Unseeked
1892    }
1893
1894    /// Creates a [`Self`] based on an entry returned from the cursor itself.
1895    fn seeked(key: Nibbles, entry: Option<(Nibbles, BranchNodeCompact)>) -> Self {
1896        entry.map_or(Self::Exhausted(key), |(path, node)| Self::Available(path, node))
1897    }
1898
1899    /// Returns the path the cursor is seeked to, or None if it's exhausted.
1900    ///
1901    /// # Panics
1902    ///
1903    /// Panics if the cursor is unseeked.
1904    const fn path(&self) -> Option<&Nibbles> {
1905        match self {
1906            Self::Unseeked => panic!("cursor is unseeked"),
1907            Self::Available(path, _) | Self::Taken(path) => Some(path),
1908            Self::Exhausted(_) => None,
1909        }
1910    }
1911
1912    /// Returns true if the cursor must seek to be usable for a range starting at `path`.
1913    fn needs_seek_to(&self, path: &Nibbles) -> bool {
1914        match self {
1915            Self::Unseeked | Self::Taken(_) => true,
1916            Self::Available(current_path, _) => current_path < path,
1917            Self::Exhausted(_) => false,
1918        }
1919    }
1920
1921    /// Returns true if seeking to `key` requires resetting the forward-only cursor.
1922    fn needs_reset_before_seek(&self, key: &Nibbles) -> bool {
1923        match self {
1924            Self::Unseeked => false,
1925            Self::Available(path, _) | Self::Taken(path) => path > key,
1926            Self::Exhausted(exhausted_at) => exhausted_at > key,
1927        }
1928    }
1929
1930    /// Takes the path and node from a [`Self::Available`]. Panics if not [`Self::Available`].
1931    fn take(&mut self) -> (Nibbles, BranchNodeCompact) {
1932        let Self::Available(path, _) = self else {
1933            panic!("take called on non-Available: {self:?}")
1934        };
1935
1936        let path = *path;
1937        let Self::Available(path, node) = core::mem::replace(self, Self::Taken(path)) else {
1938            unreachable!("already checked that self is Self::Available");
1939        };
1940
1941        (path, node)
1942    }
1943}
1944
1945/// Used to track the state of the hashed cursor, including the path that established exhaustion.
1946enum HashedCursorState<V> {
1947    /// The initial state of the cursor, indicating it's never been seeked.
1948    Unseeked,
1949    /// Cursor is seeked to this path and the value has not been used yet.
1950    Available(Nibbles, V),
1951    /// Cursor has been exhausted at or after the given path.
1952    Exhausted(Nibbles),
1953}
1954
1955impl<V> HashedCursorState<V> {
1956    /// Creates a [`Self::Unseeked`] state.
1957    const fn unseeked() -> Self {
1958        Self::Unseeked
1959    }
1960
1961    /// Creates a [`Self`] based on an entry returned from the cursor itself.
1962    fn seeked(key: Nibbles, entry: Option<(Nibbles, V)>) -> Self {
1963        entry.map_or(Self::Exhausted(key), |(path, value)| Self::Available(path, value))
1964    }
1965
1966    /// Returns the path the cursor is seeked to, or None if it's unseeked or exhausted.
1967    const fn path(&self) -> Option<&Nibbles> {
1968        match self {
1969            Self::Available(path, _) => Some(path),
1970            Self::Unseeked | Self::Exhausted(_) => None,
1971        }
1972    }
1973
1974    /// Returns true if the cursor must seek to be usable for a range starting at `key`.
1975    fn needs_seek_to(&self, key: &Nibbles) -> bool {
1976        match self {
1977            Self::Unseeked => true,
1978            Self::Available(path, _) => path < key,
1979            Self::Exhausted(exhausted_at) => exhausted_at > key,
1980        }
1981    }
1982
1983    /// Returns true if seeking to `key` requires resetting the forward-only cursor.
1984    fn needs_reset_before_seek(&self, key: &Nibbles) -> bool {
1985        match self {
1986            Self::Unseeked => false,
1987            Self::Available(path, _) => path > key,
1988            Self::Exhausted(exhausted_at) => exhausted_at > key,
1989        }
1990    }
1991
1992    /// Takes the path and value from a [`Self::Available`]. Panics if not [`Self::Available`].
1993    fn take(&mut self) -> (Nibbles, V) {
1994        match core::mem::replace(self, Self::Unseeked) {
1995            Self::Available(path, value) => (path, value),
1996            _ => panic!("take called on non-Available hashed cursor state"),
1997        }
1998    }
1999}
2000
2001/// Describes the state of the currently cached branch node (if any).
2002enum PopCachedBranchOutcome {
2003    /// Cached branch has been popped from the `cached_branch_stack` and is ready to be used.
2004    Popped((Nibbles, BranchNodeCompact)),
2005    /// All cached branches have been exhausted.
2006    Exhausted,
2007    /// Need to calculate leaves from this range (exclusive upper) before the cached branch
2008    /// (catch-up range). If None then
2009    CalculateLeaves((Nibbles, Option<Nibbles>)),
2010}
2011
2012#[cfg(test)]
2013mod tests {
2014    use super::*;
2015    use crate::{
2016        hashed_cursor::{mock::MockHashedCursorFactory, HashedCursorFactory},
2017        proof::StorageProof as LegacyStorageProof,
2018        test_utils::TrieTestHarness,
2019        trie_cursor::{depth_first, TrieCursorFactory},
2020    };
2021    use alloy_primitives::map::B256Set;
2022    use alloy_rlp::Decodable;
2023    use alloy_trie::proof::AddedRemovedKeys;
2024    use itertools::Itertools;
2025    use reth_trie_common::{
2026        prefix_set::PrefixSetMut, ProofTrieNode, ProofV2TargetParent, TrieNode, EMPTY_ROOT_HASH,
2027    };
2028    use std::collections::BTreeMap;
2029
2030    /// Converts legacy proofs to V2 proofs by combining extension nodes with their child branch
2031    /// nodes.
2032    ///
2033    /// In the legacy proof format, extension nodes and branch nodes are separate. In the V2 format,
2034    /// they are combined into a single `BranchNodeV2` where the extension's key becomes the
2035    /// branch's `key` field.
2036    ///
2037    /// Converts legacy proofs (sorted in depth-first order) to V2 format.
2038    ///
2039    /// In depth-first order, children come BEFORE parents. So when we encounter an extension node,
2040    /// its child branch has already been processed and is in the result. We need to pop it and
2041    /// combine it with the extension.
2042    fn convert_legacy_proofs_to_v2(legacy_proofs: &[ProofTrieNode]) -> Vec<ProofTrieNodeV2> {
2043        ProofTrieNodeV2::from_sorted_trie_nodes(
2044            legacy_proofs.iter().map(|p| (p.path, p.node.clone(), p.masks)),
2045        )
2046    }
2047
2048    /// Projects a legacy proof node into the representation requested by a V2 target.
2049    fn project_legacy_proof_node(
2050        node: &ProofTrieNodeV2,
2051        target: &ProofV2Target,
2052    ) -> Option<ProofTrieNodeV2> {
2053        let Some(parent_path_len) = target.parent.path_len() else {
2054            return target.key_nibbles.starts_with(&node.path).then(|| node.clone())
2055        };
2056
2057        if node.path.len() > parent_path_len {
2058            return target.key_nibbles.starts_with(&node.path).then(|| node.clone())
2059        }
2060
2061        let logical_path = match &node.node {
2062            TrieNodeV2::Leaf(leaf) => node.path.join(&leaf.key),
2063            TrieNodeV2::Branch(branch) => node.path.join(&branch.key),
2064            TrieNodeV2::EmptyRoot | TrieNodeV2::Extension(_) => return None,
2065        };
2066        let child_path_len = parent_path_len + 1;
2067        if logical_path.len() < child_path_len {
2068            return None
2069        }
2070
2071        let child_path = logical_path.slice(0..child_path_len);
2072        if !target.key_nibbles.starts_with(&child_path) {
2073            return None
2074        }
2075
2076        let trim_len = child_path_len - node.path.len();
2077        let mut projected = node.clone();
2078        projected.path = child_path;
2079        match &mut projected.node {
2080            TrieNodeV2::Leaf(leaf) => leaf.key = leaf.key.slice(trim_len..),
2081            TrieNodeV2::Branch(branch) => {
2082                branch.key = branch.key.slice(trim_len..);
2083                if branch.key.is_empty() {
2084                    branch.branch_rlp_node = None;
2085                }
2086            }
2087            TrieNodeV2::EmptyRoot | TrieNodeV2::Extension(_) => unreachable!(),
2088        }
2089        Some(projected)
2090    }
2091
2092    /// Builds the exact V2 representation expected for a legacy proof and set of targets.
2093    fn project_legacy_proof(
2094        legacy_nodes: &[ProofTrieNodeV2],
2095        targets: &[ProofV2Target],
2096    ) -> Vec<ProofTrieNodeV2> {
2097        let mut projected = targets
2098            .iter()
2099            .flat_map(|target| {
2100                legacy_nodes.iter().filter_map(move |node| project_legacy_proof_node(node, target))
2101            })
2102            .collect::<Vec<_>>();
2103        projected.sort_unstable_by(|a, b| depth_first::cmp(&a.path, &b.path));
2104        projected.dedup_by(|a, b| {
2105            if a.path != b.path {
2106                return false
2107            }
2108            assert_eq!(a, b, "target projections disagree at path {:?}", a.path);
2109            true
2110        });
2111        projected
2112    }
2113
2114    /// A test harness for comparing `StorageProofCalculator` and legacy `StorageProof`
2115    /// implementations.
2116    ///
2117    /// Wraps [`TrieTestHarness`] and adds a method to test that both proof implementations
2118    /// produce equivalent results for storage proofs.
2119    struct ProofTestHarness {
2120        inner: TrieTestHarness,
2121    }
2122
2123    impl std::ops::Deref for ProofTestHarness {
2124        type Target = TrieTestHarness;
2125        fn deref(&self) -> &Self::Target {
2126            &self.inner
2127        }
2128    }
2129
2130    impl ProofTestHarness {
2131        /// Creates a new test harness from a map of hashed storage slots to values.
2132        fn new(storage: BTreeMap<B256, U256>) -> Self {
2133            Self { inner: TrieTestHarness::new(storage) }
2134        }
2135
2136        /// Computes the storage root while treating the supplied prefixes as dirty.
2137        fn root_with_prefix_set(&self, prefix_set: PrefixSet) -> Option<B256> {
2138            let trie_cursor =
2139                self.trie_cursor_factory().storage_trie_cursor(self.hashed_address()).unwrap();
2140            let hashed_cursor =
2141                self.hashed_cursor_factory().hashed_storage_cursor(self.hashed_address()).unwrap();
2142            let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2143                .with_prefix_set(prefix_set);
2144
2145            let mut targets = [ProofV2Target::new(B256::ZERO)];
2146            let proof = calculator.storage_proof(self.hashed_address(), &mut targets).unwrap();
2147            calculator.compute_root_hash(&proof).unwrap()
2148        }
2149
2150        /// Asserts that `StorageProofCalculator` and legacy `StorageProof` produce equivalent
2151        /// results for storage proofs.
2152        fn assert_proof(
2153            &self,
2154            targets: impl IntoIterator<Item = ProofV2Target>,
2155        ) -> Result<(), StateProofError> {
2156            let mut targets_vec = targets.into_iter().collect::<Vec<_>>();
2157
2158            // Get v2 proof and root hash via harness
2159            let (proof_v2_result, root_hash) = self.proof_v2(&mut targets_vec);
2160
2161            // Verify the root hash matches the expected root (if the proof contains a root
2162            // node)
2163            if let Some(root_hash) = root_hash {
2164                pretty_assertions::assert_eq!(self.original_root(), root_hash);
2165            }
2166
2167            // Fully materialize the legacy proof so compressed branches can be projected across
2168            // arbitrary parent boundaries, including absence targets that diverge inside them.
2169            let legacy_targets = targets_vec
2170                .iter()
2171                .map(|target| B256::from_slice(&target.key_nibbles.pack()))
2172                .chain(self.storage().keys().copied())
2173                .collect::<B256Set>();
2174
2175            // Call legacy StorageProof::storage_multiproof
2176            let proof_legacy_result = LegacyStorageProof::new_hashed(
2177                self.trie_cursor_factory(),
2178                self.hashed_cursor_factory(),
2179                self.hashed_address(),
2180            )
2181            .with_branch_node_masks(true)
2182            .with_added_removed_keys(Some(AddedRemovedKeys::default().with_assume_added(true)))
2183            .storage_multiproof(legacy_targets)?;
2184
2185            // Decode and sort legacy proof nodes
2186            let proof_legacy_nodes = proof_legacy_result
2187                .subtree
2188                .iter()
2189                .map(|(path, node_enc)| {
2190                    let mut buf = node_enc.as_ref();
2191                    let node = TrieNode::decode(&mut buf)
2192                        .expect("legacy implementation should not produce malformed proof nodes");
2193
2194                    let masks = if path.is_empty() {
2195                        None
2196                    } else {
2197                        proof_legacy_result.branch_node_masks.get(path).copied()
2198                    };
2199
2200                    ProofTrieNode { path: *path, node, masks }
2201                })
2202                .sorted_by(|a, b| depth_first::cmp(&a.path, &b.path))
2203                .collect::<Vec<_>>();
2204
2205            // Convert legacy proofs to V2 proofs by combining extensions with their child branches
2206            let all_legacy_nodes_v2 = convert_legacy_proofs_to_v2(&proof_legacy_nodes);
2207
2208            let expected_v2 = project_legacy_proof(&all_legacy_nodes_v2, &targets_vec);
2209            pretty_assertions::assert_eq!(expected_v2, proof_v2_result);
2210
2211            Ok(())
2212        }
2213    }
2214
2215    /// Tests that `clear_computation_state` properly resets internal stacks, allowing a
2216    /// `StorageProofCalculator` to be reused after a mid-computation error left stale state.
2217    /// Before the fix, stale data in `branch_stack`, `child_stack`, and `branch_path`
2218    /// could cause a `usize` underflow panic in `pop_branch`.
2219    #[test]
2220    fn test_proof_calculator_reuse_after_error() {
2221        reth_tracing::init_test_tracing();
2222
2223        let slots = [
2224            B256::right_padding_from(&[0x10]),
2225            B256::right_padding_from(&[0x20]),
2226            B256::right_padding_from(&[0x30]),
2227            B256::right_padding_from(&[0x40]),
2228        ];
2229        let storage: BTreeMap<B256, U256> =
2230            slots.iter().map(|&s| (s, U256::from(100u64))).collect();
2231
2232        let harness = ProofTestHarness::new(storage);
2233
2234        let trie_cursor_factory = harness.trie_cursor_factory();
2235        let hashed_cursor_factory = harness.hashed_cursor_factory();
2236
2237        let hashed_address = harness.hashed_address();
2238        let trie_cursor = trie_cursor_factory.storage_trie_cursor(hashed_address).unwrap();
2239        let hashed_cursor = hashed_cursor_factory.hashed_storage_cursor(hashed_address).unwrap();
2240        let mut proof_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
2241
2242        // Simulate stale state left by a mid-computation error: push fake entries onto internal
2243        // stacks and set a non-empty branch_path.
2244        proof_calculator.branch_stack.push(ProofTrieBranch {
2245            ext_len: 2,
2246            state_mask: TrieMask::new(0b1111),
2247            masks: None,
2248        });
2249        proof_calculator.branch_stack.push(ProofTrieBranch {
2250            ext_len: 0,
2251            state_mask: TrieMask::new(0b11),
2252            masks: None,
2253        });
2254        proof_calculator
2255            .child_stack
2256            .push(ProofTrieBranchChild::RlpNode(RlpNode::word_rlp(&B256::ZERO)));
2257        proof_calculator.branch_path = Nibbles::from_nibbles([0x1, 0x2, 0x3]);
2258
2259        // clear_computation_state should reset everything so a subsequent call works.
2260        proof_calculator.clear_computation_state();
2261
2262        let mut sorted_slots = slots.to_vec();
2263        sorted_slots.sort();
2264        let mut targets: Vec<ProofV2Target> =
2265            sorted_slots.iter().copied().map(ProofV2Target::new).collect();
2266
2267        let result = proof_calculator.storage_proof(hashed_address, &mut targets).unwrap();
2268
2269        // Compare against a fresh calculator to verify correctness.
2270        let trie_cursor = trie_cursor_factory.storage_trie_cursor(hashed_address).unwrap();
2271        let hashed_cursor = hashed_cursor_factory.hashed_storage_cursor(hashed_address).unwrap();
2272        let mut fresh_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
2273        let fresh_result = fresh_calculator.storage_proof(hashed_address, &mut targets).unwrap();
2274
2275        pretty_assertions::assert_eq!(fresh_result, result);
2276    }
2277
2278    #[test]
2279    fn test_partial_storage_proof_after_root_calculation() {
2280        let slot_a = B256::right_padding_from(&[0xae, 0xd4, 0x00]);
2281        let slot_b = B256::right_padding_from(&[0xae, 0xd4, 0x10]);
2282        let harness = ProofTestHarness::new(BTreeMap::from([
2283            (slot_a, U256::from(1)),
2284            (slot_b, U256::from(2)),
2285        ]));
2286        let hashed_address = harness.hashed_address();
2287        let trie_cursor =
2288            harness.trie_cursor_factory().storage_trie_cursor(hashed_address).unwrap();
2289        let hashed_cursor =
2290            harness.hashed_cursor_factory().hashed_storage_cursor(hashed_address).unwrap();
2291        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
2292
2293        let root_node = calculator.storage_root_node(hashed_address).unwrap();
2294        assert_eq!(
2295            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap(),
2296            Some(harness.original_root())
2297        );
2298
2299        let target = ProofV2Target::new(slot_a).with_parent(ProofV2TargetParent::new(3));
2300        let mut actual_targets = [target];
2301        let actual = calculator.storage_proof(hashed_address, &mut actual_targets).unwrap();
2302        let mut expected_targets = [target];
2303        let (expected, root) = harness.proof_v2(&mut expected_targets);
2304
2305        assert!(root.is_none());
2306        pretty_assertions::assert_eq!(expected, actual);
2307    }
2308
2309    mod proptest_tests {
2310        use super::*;
2311        use proptest::prelude::*;
2312
2313        /// Generate a strategy for storage datasets (hashed slot → value).
2314        fn storage_strategy() -> impl Strategy<Value = BTreeMap<B256, U256>> {
2315            prop::collection::vec((any::<[u8; 32]>(), any::<u64>()), 0..=100).prop_map(|slots| {
2316                slots
2317                    .into_iter()
2318                    .map(|(slot_bytes, value)| (B256::from(slot_bytes), U256::from(value)))
2319                    .filter(|(_, v)| *v != U256::ZERO)
2320                    .collect()
2321            })
2322        }
2323
2324        /// Generate a strategy for proof targets that are 80% from existing storage slots
2325        /// and 20% random keys. Each target has a random parent path length of `None` or 0..15.
2326        fn proof_targets_strategy(
2327            slot_keys: Vec<B256>,
2328        ) -> impl Strategy<Value = Vec<ProofV2Target>> {
2329            let num_slots = slot_keys.len();
2330
2331            let target_count = 0..=(num_slots + 5);
2332
2333            target_count.prop_flat_map(move |count| {
2334                let slot_keys = slot_keys.clone();
2335                prop::collection::vec(
2336                    (
2337                        prop::bool::weighted(0.8).prop_flat_map(move |from_slots| {
2338                            if from_slots && !slot_keys.is_empty() {
2339                                prop::sample::select(slot_keys.clone()).boxed()
2340                            } else {
2341                                any::<[u8; 32]>().prop_map(B256::from).boxed()
2342                            }
2343                        }),
2344                        0u8..16u8,
2345                    )
2346                        .prop_map(|(key, encoded_parent_path_len)| {
2347                            let parent = encoded_parent_path_len.checked_sub(1).map_or(
2348                                ProofV2TargetParent::NONE,
2349                                |parent_path_len| {
2350                                    ProofV2TargetParent::new(usize::from(parent_path_len))
2351                                },
2352                            );
2353                            ProofV2Target::new(key).with_parent(parent)
2354                        }),
2355                    count,
2356                )
2357            })
2358        }
2359
2360        proptest! {
2361            #![proptest_config(ProptestConfig::with_cases(4000))]
2362            #[test]
2363            /// Tests that `StorageProofCalculator` produces valid proofs for randomly generated
2364            /// storage datasets with proof targets.
2365            fn proptest_proof_with_targets(
2366                (storage, targets) in storage_strategy()
2367                    .prop_flat_map(|storage| {
2368                        let mut slot_keys: Vec<B256> = storage.keys().copied().collect();
2369                        slot_keys.sort_unstable();
2370                        let targets_strategy = proof_targets_strategy(slot_keys);
2371                        (Just(storage), targets_strategy)
2372                    })
2373            ) {
2374                reth_tracing::init_test_tracing();
2375                let harness = ProofTestHarness::new(storage);
2376
2377                harness.assert_proof(targets).expect("Proof generation failed");
2378            }
2379        }
2380    }
2381
2382    #[test]
2383    fn test_exact_subtrie_targets_with_root_target() {
2384        reth_tracing::init_test_tracing();
2385
2386        let slot_80 = B256::right_padding_from(&[0x80]);
2387        let slot_82 = B256::right_padding_from(&[0x82]);
2388        let slot_f0 = B256::right_padding_from(&[0xf0]);
2389        let storage = BTreeMap::from([
2390            (slot_80, U256::from(1)),
2391            (slot_82, U256::from(2)),
2392            (slot_f0, U256::from(3)),
2393        ]);
2394        let targets = [
2395            ProofV2Target::new(B256::ZERO),
2396            ProofV2Target::new(slot_80).with_parent(ProofV2TargetParent::new(1)),
2397        ];
2398
2399        let harness = ProofTestHarness::new(storage);
2400        harness.assert_proof(targets).expect("Proof generation failed");
2401    }
2402
2403    #[test]
2404    fn test_rebases_singleton_subtrie_root_below_known_parent() {
2405        let slot = B256::right_padding_from(&[0xae, 0xd4, 0x09]);
2406        let slot_nibbles = Nibbles::unpack(slot);
2407        let harness = ProofTestHarness::new(BTreeMap::from([(slot, U256::from(1))]));
2408        let mut targets = [ProofV2Target::new(slot).with_parent(ProofV2TargetParent::new(3))];
2409
2410        let (proof, root) = harness.proof_v2(&mut targets);
2411
2412        assert!(root.is_none());
2413        assert_eq!(proof.len(), 1);
2414        assert_eq!(proof[0].path, slot_nibbles.slice(0..4));
2415        let TrieNodeV2::Leaf(leaf) = &proof[0].node else {
2416            panic!("singleton subtrie root should remain a leaf")
2417        };
2418        assert_eq!(leaf.key, slot_nibbles.slice(4..));
2419    }
2420
2421    #[test]
2422    fn test_rebases_singleton_leaf_at_max_parent_depth() {
2423        let slot = B256::repeat_byte(0xae);
2424        let slot_nibbles = Nibbles::unpack(slot);
2425        let harness = ProofTestHarness::new(BTreeMap::from([(slot, U256::from(1))]));
2426        let mut targets = [ProofV2Target::new(slot).with_parent(ProofV2TargetParent::new(63))];
2427
2428        let (proof, root) = harness.proof_v2(&mut targets);
2429
2430        assert!(root.is_none());
2431        assert_eq!(proof.len(), 1);
2432        assert_eq!(proof[0].path, slot_nibbles);
2433        let TrieNodeV2::Leaf(leaf) = &proof[0].node else {
2434            panic!("singleton subtrie root should remain a leaf")
2435        };
2436        assert!(leaf.key.is_empty());
2437    }
2438
2439    #[test]
2440    fn test_root_and_root_parent_targets_retain_both_singleton_representations() {
2441        let slot = B256::right_padding_from(&[0x20]);
2442        let slot_nibbles = Nibbles::unpack(slot);
2443        let harness = ProofTestHarness::new(BTreeMap::from([(slot, U256::from(1))]));
2444        let mut targets = [
2445            ProofV2Target::new(slot),
2446            ProofV2Target::new(slot).with_parent(ProofV2TargetParent::new(0)),
2447        ];
2448
2449        let (proof, root) = harness.proof_v2(&mut targets);
2450
2451        assert_eq!(root, Some(harness.original_root()));
2452        let root_node = proof.iter().find(|node| node.path.is_empty()).expect("root proof");
2453        let TrieNodeV2::Leaf(root_leaf) = &root_node.node else { panic!("root should be a leaf") };
2454        assert_eq!(root_leaf.key, slot_nibbles);
2455
2456        let child_path = slot_nibbles.slice(0..1);
2457        let child_node =
2458            proof.iter().find(|node| node.path == child_path).expect("rebased root child proof");
2459        let TrieNodeV2::Leaf(child_leaf) = &child_node.node else {
2460            panic!("root child should be a leaf")
2461        };
2462        assert_eq!(child_leaf.key, slot_nibbles.slice(1..));
2463    }
2464
2465    #[test]
2466    fn test_rebases_compressed_branch_subtrie_root() {
2467        let slot_a = B256::right_padding_from(&[0xae, 0xd4, 0x00]);
2468        let slot_b = B256::right_padding_from(&[0xae, 0xd4, 0x10]);
2469        let slot_nibbles = Nibbles::unpack(slot_a);
2470        let harness = ProofTestHarness::new(BTreeMap::from([
2471            (slot_a, U256::from(1)),
2472            (slot_b, U256::from(2)),
2473        ]));
2474        let mut targets = [ProofV2Target::new(slot_a).with_parent(ProofV2TargetParent::new(3))];
2475
2476        let (proof, root) = harness.proof_v2(&mut targets);
2477
2478        assert!(root.is_none());
2479        let branch_path = slot_nibbles.slice(0..4);
2480        let branch_node =
2481            proof.iter().find(|node| node.path == branch_path).expect("rebased compressed branch");
2482        let TrieNodeV2::Branch(branch) = &branch_node.node else {
2483            panic!("rebased node should be a branch")
2484        };
2485        assert!(branch.key.is_empty());
2486        assert!(branch.branch_rlp_node.is_none());
2487    }
2488
2489    #[test]
2490    fn test_discards_reconstructed_known_parent_branch() {
2491        let slot_a = B256::right_padding_from(&[0xae, 0xd2]);
2492        let slot_b = B256::right_padding_from(&[0xae, 0xd4]);
2493        let slot_nibbles = Nibbles::unpack(slot_a);
2494        let harness = ProofTestHarness::new(BTreeMap::from([
2495            (slot_a, U256::from(1)),
2496            (slot_b, U256::from(2)),
2497        ]));
2498        let mut targets = [ProofV2Target::new(slot_a).with_parent(ProofV2TargetParent::new(3))];
2499
2500        let (proof, root) = harness.proof_v2(&mut targets);
2501
2502        assert!(root.is_none());
2503        assert!(!proof.iter().any(|node| node.path == slot_nibbles.slice(0..3)));
2504        assert!(proof.iter().any(|node| node.path == slot_nibbles.slice(0..4)));
2505    }
2506
2507    #[test]
2508    fn test_rebased_root_matches_direct_child_not_full_short_key() {
2509        let stored_slot = B256::right_padding_from(&[0xae, 0xd4, 0x09]);
2510        let same_child_target = B256::right_padding_from(&[0xae, 0xd4, 0xff]);
2511        let other_child_target = B256::right_padding_from(&[0xae, 0xd5]);
2512        let harness = ProofTestHarness::new(BTreeMap::from([(stored_slot, U256::from(1))]));
2513
2514        let mut same_child =
2515            [ProofV2Target::new(same_child_target).with_parent(ProofV2TargetParent::new(3))];
2516        let (proof, _) = harness.proof_v2(&mut same_child);
2517        assert_eq!(proof.len(), 1, "divergent leaf proves absence below the same child");
2518
2519        let mut other_child =
2520            [ProofV2Target::new(other_child_target).with_parent(ProofV2TargetParent::new(3))];
2521        let (proof, _) = harness.proof_v2(&mut other_child);
2522        assert!(proof.is_empty(), "a different direct child is unrelated to the target");
2523    }
2524
2525    #[test]
2526    fn test_known_parent_sibling_span_retains_only_target_children() {
2527        let stored_slot_a = B256::right_padding_from(&[0xea, 0x53]);
2528        let stored_slot_b = B256::right_padding_from(&[0xeb, 0x53]);
2529        let stored_slot_c = B256::right_padding_from(&[0xec, 0x53]);
2530        let target_a = B256::right_padding_from(&[0xea, 0x1f]);
2531        let target_c = B256::right_padding_from(&[0xec, 0x1f]);
2532        let harness = ProofTestHarness::new(BTreeMap::from([
2533            (stored_slot_a, U256::from(1)),
2534            (stored_slot_b, U256::from(2)),
2535            (stored_slot_c, U256::from(3)),
2536        ]));
2537        let mut targets = [target_a, target_c]
2538            .map(|target| ProofV2Target::new(target).with_parent(ProofV2TargetParent::new(1)));
2539
2540        let (proof, root) = harness.proof_v2(&mut targets);
2541
2542        assert!(root.is_none());
2543        assert_eq!(
2544            proof.iter().map(|node| node.path).collect::<Vec<_>>(),
2545            [Nibbles::from_nibbles([0xe, 0xa]), Nibbles::from_nibbles([0xe, 0xc])]
2546        );
2547    }
2548
2549    #[test]
2550    fn test_known_parent_does_not_use_stale_parent_mask() {
2551        let stored_slot_a = B256::right_padding_from(&[0xea, 0x53]);
2552        let stored_slot = B256::right_padding_from(&[0xeb, 0x53]);
2553        let stored_slot_c = B256::right_padding_from(&[0xec, 0x53]);
2554        let target = B256::right_padding_from(&[0xeb, 0x1f]);
2555        let stored_slot_nibbles = Nibbles::unpack(stored_slot);
2556
2557        // The known parent at `e` is supplied by the sparse trie and may be stale in the database
2558        // when partial persistence masks that path. In particular, its state mask can omit the
2559        // live `eb` child while hashed state already contains that child's leaf.
2560        let stale_parent_mask = TrieMask::new((1 << 0xa) | (1 << 0xc));
2561        let stale_parent = BranchNodeCompact::new(
2562            stale_parent_mask,
2563            TrieMask::new(0),
2564            TrieMask::new(0),
2565            Vec::new(),
2566            None,
2567        );
2568        let storage_nodes = BTreeMap::from([(Nibbles::from_nibbles([0xe]), stale_parent)]);
2569
2570        let mut harness = TrieTestHarness::new(BTreeMap::from([
2571            (stored_slot_a, U256::from(1)),
2572            (stored_slot, U256::from(2)),
2573            (stored_slot_c, U256::from(3)),
2574        ]));
2575        harness.set_trie_nodes(storage_nodes);
2576
2577        let mut targets = [ProofV2Target::new(target).with_parent(ProofV2TargetParent::new(1))];
2578        let (proof, root) = harness.proof_v2(&mut targets);
2579
2580        assert!(root.is_none());
2581        assert_eq!(proof.len(), 1);
2582        assert_eq!(proof[0].path, stored_slot_nibbles.slice(0..2));
2583        let TrieNodeV2::Leaf(leaf) = &proof[0].node else {
2584            panic!("live direct child should be reconstructed as a leaf")
2585        };
2586        assert_eq!(leaf.key, stored_slot_nibbles.slice(2..));
2587    }
2588
2589    #[test]
2590    fn test_empty_storage_respects_parent_context() {
2591        let harness = ProofTestHarness::new(BTreeMap::new());
2592        let slot = B256::ZERO;
2593
2594        let mut partial_target =
2595            [ProofV2Target::new(slot).with_parent(ProofV2TargetParent::new(0))];
2596        let (partial_proof, partial_root) = harness.proof_v2(&mut partial_target);
2597        assert!(partial_proof.is_empty());
2598        assert!(partial_root.is_none());
2599
2600        let mut root_target = [ProofV2Target::new(slot)];
2601        let (root_proof, root) = harness.proof_v2(&mut root_target);
2602        assert_eq!(root_proof.len(), 1);
2603        assert!(matches!(root_proof[0].node, TrieNodeV2::EmptyRoot));
2604        assert_eq!(root, Some(EMPTY_ROOT_HASH));
2605    }
2606
2607    #[test]
2608    fn test_big_trie() {
2609        use rand::prelude::*;
2610
2611        reth_tracing::init_test_tracing();
2612        let mut rng = rand::rngs::SmallRng::seed_from_u64(1);
2613
2614        let mut rand_b256 = || {
2615            let mut buf: [u8; 32] = [0; 32];
2616            rng.fill_bytes(&mut buf);
2617            B256::from_slice(&buf)
2618        };
2619
2620        // Generate random storage dataset.
2621        let mut storage = BTreeMap::new();
2622        for _ in 0..10240 {
2623            let hashed_slot = rand_b256();
2624            storage.insert(hashed_slot, U256::from(1u64));
2625        }
2626
2627        // Collect targets; partially from real keys, partially random keys which probably won't
2628        // exist.
2629        let mut targets = storage.keys().copied().collect::<Vec<_>>();
2630        for _ in 0..storage.len() / 5 {
2631            targets.push(rand_b256());
2632        }
2633        targets.sort();
2634
2635        // Create test harness
2636        let harness = ProofTestHarness::new(storage);
2637
2638        harness
2639            .assert_proof(targets.into_iter().map(ProofV2Target::new))
2640            .expect("Proof generation failed");
2641    }
2642
2643    #[test]
2644    fn test_node_with_masked_empty_child() {
2645        reth_tracing::init_test_tracing();
2646
2647        let val = U256::from(42u64);
2648
2649        // All storage keys share a common first nibble (0x6), so the branch is at path 0x6. The
2650        // second nibble differentiates children: 0,1,3,5,7.
2651        let slot_60 = B256::right_padding_from(&[0x60]);
2652        let slot_61 = B256::right_padding_from(&[0x61]);
2653        let slot_65 = B256::right_padding_from(&[0x65]);
2654        let slot_67 = B256::right_padding_from(&[0x67]);
2655
2656        // Construct a branch node at path 0x6 with state_mask bits 0,1,3,5,7.
2657        // hash_mask has bits 0,1,5,7 (NOT 3) — nibble 3's hash is cleared because it's in the
2658        // prefix set. Hashes are dummy values.
2659        let state_mask = TrieMask::new(0b10101011); // bits 0,1,3,5,7
2660        let hash_mask = TrieMask::new(0b10100011); // bits 0,1,5,7 (NOT 3)
2661        let hashes = vec![B256::repeat_byte(0xaa); hash_mask.count_ones() as usize];
2662        let branch = BranchNodeCompact::new(state_mask, TrieMask::new(0), hash_mask, hashes, None);
2663
2664        let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2665            std::iter::once((Nibbles::from_nibbles([0x6]), branch)).collect();
2666
2667        // Hashed cursor has slots at children 0, 1, 5, 7 — but NOT child 3 (0x63).
2668        // This simulates the post-state overlay having deleted the slot at 0x63.
2669        let mut harness = TrieTestHarness::new(
2670            [slot_60, slot_61, slot_65, slot_67].iter().map(|s| (*s, val)).collect(),
2671        );
2672        harness.set_trie_nodes(storage_nodes);
2673
2674        let storage_trie_cursor =
2675            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2676        let hashed_storage_cursor = harness
2677            .hashed_cursor_factory()
2678            .hashed_storage_cursor(harness.hashed_address())
2679            .unwrap();
2680        let mut calculator =
2681            StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
2682        let root_node = calculator
2683            .storage_root_node(harness.hashed_address())
2684            .expect("storage_root_node should succeed with masked empty child");
2685
2686        let root_hash = calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap();
2687        assert!(root_hash.is_some(), "should produce a root hash");
2688    }
2689
2690    /// Tests that `root_node` handles the case where `uncalculated_lower_bound` has advanced
2691    /// entirely past a cached branch that still has unprocessed children in its `state_mask`.
2692    ///
2693    /// Branch at `0x6` has `state_mask` bits 0,1,5,f where nibble 5 has its `hash_mask`
2694    /// cleared and no leaf data. The last child (nibble f)
2695    /// causes `calculate_key_range` to be called with range `(0x6f, Some(0x7))`. After that range,
2696    /// the hashed cursor still has keys (at `0x70...`), so `proof_subtrie` does not break and
2697    /// re-enters `next_uncached_key_range` with `uncalculated_lower_bound = Some(0x7)`.
2698    /// Since `0x7` is past `0x6`, all remaining children are skipped and the branch is popped.
2699    #[test]
2700    fn test_node_with_masked_empty_child_lower_bound_past_branch() {
2701        reth_tracing::init_test_tracing();
2702
2703        let val = U256::from(42u64);
2704
2705        // Leaf keys under 0x6 and one beyond (0x70) to keep the cursor alive after 0x6.
2706        let slot_60 = B256::right_padding_from(&[0x60]);
2707        let slot_61 = B256::right_padding_from(&[0x61]);
2708        let slot_6f = B256::right_padding_from(&[0x6f]);
2709        let slot_70 = B256::right_padding_from(&[0x70]);
2710
2711        // Branch at 0x6: state_mask bits 0,1,5,f; hash_mask bits 0,1 (NOT 5, NOT f).
2712        // Nibble 5 has state_mask set but no hash and no leaf data (masked empty child).
2713        // Nibble f has state_mask set, no hash, but DOES have leaf data.
2714        let state_mask = TrieMask::new(0b1000_0000_0010_0011); // bits 0,1,5,f
2715        let hash_mask = TrieMask::new(0b0000_0000_0000_0011); // bits 0,1
2716        let hashes = vec![B256::repeat_byte(0xaa); hash_mask.count_ones() as usize];
2717        let branch = BranchNodeCompact::new(state_mask, TrieMask::new(0), hash_mask, hashes, None);
2718
2719        let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2720            std::iter::once((Nibbles::from_nibbles([0x6]), branch)).collect();
2721
2722        // Hashed cursor: slots at 0x60, 0x61, 0x6f, 0x70 — but NOT 0x65.
2723        let mut harness = TrieTestHarness::new(
2724            [slot_60, slot_61, slot_6f, slot_70].iter().map(|s| (*s, val)).collect(),
2725        );
2726        harness.set_trie_nodes(storage_nodes);
2727
2728        let storage_trie_cursor =
2729            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2730        let hashed_storage_cursor = harness
2731            .hashed_cursor_factory()
2732            .hashed_storage_cursor(harness.hashed_address())
2733            .unwrap();
2734        let mut calculator =
2735            StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
2736        let root_node = calculator
2737            .storage_root_node(harness.hashed_address())
2738            .expect("storage_root_node should succeed when lower bound advances past branch");
2739
2740        let root_hash = calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap();
2741        assert!(root_hash.is_some(), "should produce a root hash");
2742    }
2743
2744    /// Tests that the prefix set causes `next_uncached_key_range` to add child nibbles that are
2745    /// not present in the cached branch's `state_mask`.
2746    ///
2747    /// Setup: An original state with leaves at `0x60` and `0x61` produces a cached branch at
2748    /// `0x6` with children at nibbles 0 and 1 (both with real cached hashes from `StorageRoot`).
2749    /// A new leaf is then inserted at `0x63...`, which is NOT in the branch's `state_mask`.
2750    /// The prefix set contains the new key. Without prefix set support, the calculator would
2751    /// skip nibble 3 entirely and produce a stale root hash. With prefix set support, nibble 3
2752    /// is discovered and its subtrie is recalculated from leaves.
2753    #[test]
2754    fn test_prefix_set_adds_child_nibbles() {
2755        reth_tracing::init_test_tracing();
2756
2757        let val = U256::from(42u64);
2758        let slot_60 = B256::right_padding_from(&[0x60]);
2759        let slot_61 = B256::right_padding_from(&[0x61]);
2760        let slot_63 = B256::right_padding_from(&[0x63]);
2761
2762        let harness = TrieTestHarness::new([(slot_60, val), (slot_61, val)].into_iter().collect());
2763
2764        let changeset: BTreeMap<B256, U256> = std::iter::once((slot_63, val)).collect();
2765        let (expected_root, _) = harness.get_root_with_updates(&changeset);
2766
2767        let mut updated_storage = harness.storage().clone();
2768        updated_storage.insert(slot_63, val);
2769
2770        let updated_hashed = MockHashedCursorFactory::new(
2771            BTreeMap::new(),
2772            std::iter::once((harness.hashed_address(), updated_storage)).collect(),
2773        );
2774
2775        let mut prefix_set = PrefixSetMut::default();
2776        prefix_set.insert(Nibbles::unpack(slot_63));
2777
2778        let trie_cursor =
2779            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2780        let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
2781        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2782            .with_prefix_set(prefix_set.freeze());
2783        let root_node = calculator
2784            .storage_root_node(harness.hashed_address())
2785            .expect("storage_root_node should succeed with prefix set adding child nibbles");
2786        let got_root =
2787            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2788
2789        pretty_assertions::assert_eq!(
2790            expected_root,
2791            got_root,
2792            "Root hash with prefix set should match fresh computation"
2793        );
2794    }
2795
2796    /// Tests that `next_uncached_key_range` does not use a cached hash when the child's path
2797    /// is in the prefix set, forcing recalculation from leaves.
2798    ///
2799    /// Setup: A cached branch at `0x6` with children at nibbles 0,1,5 — all with cached hashes.
2800    /// The leaf at `0x65...` is changed (different value). The prefix set marks `0x65...` as
2801    /// dirty. Without prefix set support, the calculator would use the stale cached hash for
2802    /// nibble 5 and produce a wrong root. With prefix set support, the cached hash is skipped
2803    /// and the subtrie is recalculated from the updated leaf data.
2804    #[test]
2805    fn test_prefix_set_invalidates_cached_hash() {
2806        reth_tracing::init_test_tracing();
2807
2808        let original_val = U256::from(42u64);
2809        let updated_val = U256::from(9999u64);
2810        let slot_60 = B256::right_padding_from(&[0x60]);
2811        let slot_61 = B256::right_padding_from(&[0x61]);
2812        let slot_65 = B256::right_padding_from(&[0x65]);
2813
2814        let harness = TrieTestHarness::new(
2815            [(slot_60, original_val), (slot_61, original_val), (slot_65, original_val)]
2816                .into_iter()
2817                .collect(),
2818        );
2819
2820        let changeset: BTreeMap<B256, U256> = std::iter::once((slot_65, updated_val)).collect();
2821        let (expected_root, _) = harness.get_root_with_updates(&changeset);
2822
2823        let mut updated_storage = harness.storage().clone();
2824        updated_storage.insert(slot_65, updated_val);
2825
2826        let updated_hashed = MockHashedCursorFactory::new(
2827            BTreeMap::new(),
2828            std::iter::once((harness.hashed_address(), updated_storage)).collect(),
2829        );
2830
2831        let mut prefix_set = PrefixSetMut::default();
2832        prefix_set.insert(Nibbles::unpack(slot_65));
2833
2834        let trie_cursor =
2835            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2836        let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
2837        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2838            .with_prefix_set(prefix_set.freeze());
2839        let root_node = calculator
2840            .storage_root_node(harness.hashed_address())
2841            .expect("storage_root_node should succeed with prefix set invalidating cached hash");
2842        let got_root =
2843            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2844
2845        pretty_assertions::assert_eq!(
2846            expected_root,
2847            got_root,
2848            "Root hash with prefix set invalidation should match fresh computation"
2849        );
2850    }
2851
2852    fn b256(s: &str) -> B256 {
2853        B256::from_slice(&alloy_primitives::hex::decode(s).expect("valid hex string"))
2854    }
2855
2856    #[test]
2857    fn test_prefix_set_root_proof_processes_sibling_after_cached_descendant() {
2858        reth_tracing::init_test_tracing();
2859
2860        let storage = [
2861            ("1022c69e9d900e40775cd387c134899f465f291dbc3c97899ff6bfb8dc972b37", 45u64),
2862            ("1111ad8083c8a3a398b2b781217b989ff4d1ed182f46cc765eda49a7b316139d", 60),
2863            ("12012d20943649899b2fc0f87b9840b70ef68e93613aac17c269bf8c5a78a712", 17),
2864            ("12014b57b9a162c03d072eb6acd4e936f1c4bc23b803a054347c5ee9a9bcfb9a", 49),
2865            ("1203f800840af3f898ab4572f2750106a7c4bd2b3e844b6e7fa72704673cc2c6", 76),
2866            ("12208f18fbcd6971c92808721392acbf11d5af58e9143a374cc86e70bdd1f097", 10),
2867        ]
2868        .into_iter()
2869        .map(|(key, value)| (b256(key), U256::from(value)))
2870        .collect();
2871
2872        let dirty = b256("12208f18fbcd6971c92808721392acbf11d5af58e9143a374cc86e70bdd1f097");
2873        let harness = ProofTestHarness::new(storage);
2874        let expected_root = harness.original_root();
2875
2876        let mut prefix_set = PrefixSetMut::default();
2877        prefix_set.insert(Nibbles::unpack(dirty));
2878
2879        pretty_assertions::assert_eq!(
2880            Some(expected_root),
2881            harness.root_with_prefix_set(prefix_set.freeze()),
2882            "root proof must process a prefix-set sibling after a cached descendant",
2883        );
2884    }
2885
2886    #[test]
2887    fn test_prefix_set_root_proof_processes_trailing_dirty_sibling() {
2888        reth_tracing::init_test_tracing();
2889
2890        let keys = [
2891            "0022001020000000000000000000000000000000000000000000000000000000",
2892            "0110212112000000000000000000000000000000000000000000000000000000",
2893            "0202210210000000000000000000000000000000000000000000000000000000",
2894            "0211020211000000000000000000000000000000000000000000000000000000",
2895            "0211211002000000000000000000000000000000000000000000000000000000",
2896            "0212221010000000000000000000000000000000000000000000000000000000",
2897            "0222011102000000000000000000000000000000000000000000000000000000",
2898        ];
2899        let storage =
2900            keys.iter().enumerate().map(|(i, key)| (b256(key), U256::from(i as u64 + 1))).collect();
2901        let harness = ProofTestHarness::new(storage);
2902        let expected_root = harness.original_root();
2903
2904        // The dirty children straddle a clean cached descendant under branch 0x02. Traversal
2905        // must resume at the trailing dirty sibling after using the cached descendant.
2906        let mut prefix_set = PrefixSetMut::default();
2907        prefix_set.insert(Nibbles::unpack(b256(keys[2])));
2908        prefix_set.insert(Nibbles::unpack(b256(keys[6])));
2909
2910        pretty_assertions::assert_eq!(
2911            Some(expected_root),
2912            harness.root_with_prefix_set(prefix_set.freeze()),
2913        );
2914    }
2915
2916    /// Helper to compute the keccak256 hash of a storage leaf node. The `short_key` is the
2917    /// leaf's key after trimming all branch/extension nibbles consumed by ancestor nodes.
2918    fn storage_leaf_hash(short_key: &Nibbles, value: &U256) -> B256 {
2919        let mut buf = Vec::new();
2920        alloy_trie::nodes::LeafNodeRef::new(short_key, &alloy_rlp::encode_fixed_size(value))
2921            .encode(&mut buf);
2922        keccak256(&buf)
2923    }
2924
2925    /// Tests branch collapse when the removed child comes BEFORE the remaining child.
2926    ///
2927    /// Trie structure (3 hashed storage keys):
2928    ///   `key_a` = 0x20...  (root nibble 2, sub-nibble 0)
2929    ///   `key_b` = 0x21...  (root nibble 2, sub-nibble 1)
2930    ///   `key_c` = 0xb0...  (root nibble b)
2931    ///
2932    /// This creates:
2933    ///   root branch at nibbles {2, b}
2934    ///   sub-branch at path [2] at nibbles {0, 1}
2935    ///
2936    /// `key_a` is removed (prefix set marks it dirty, cursor has no value for it).
2937    /// The sub-branch at [2] collapses into its remaining child (`key_b`). The removed child
2938    /// (nibble 0) comes before the remaining child (nibble 1).
2939    #[test]
2940    fn test_branch_collapse_removed_child_before_remaining() {
2941        reth_tracing::init_test_tracing();
2942
2943        let val = U256::from(1u64);
2944
2945        let key_a = B256::right_padding_from(&[0x20]); // root nibble 2, sub-nibble 0
2946        let key_b = B256::right_padding_from(&[0x21]); // root nibble 2, sub-nibble 1
2947        let key_c = B256::right_padding_from(&[0xb0]); // root nibble b
2948
2949        // Compute leaf hashes for the sub-branch's children.
2950        // The sub-branch at path [2] consumes 2 nibbles from each key (root nibble + sub-nibble).
2951        let leaf_hash_a = storage_leaf_hash(&Nibbles::unpack(key_a).slice(2..), &val);
2952        let leaf_hash_b = storage_leaf_hash(&Nibbles::unpack(key_b).slice(2..), &val);
2953
2954        // Only cache the sub-branch at path [2] — the root will be built from leaves.
2955        // The sub-branch has children at nibbles 0 and 1, both with cached hashes.
2956        let sub_branch_state_mask = TrieMask::new((1 << 0) | (1 << 1));
2957        let cached_sub_branch = BranchNodeCompact::new(
2958            sub_branch_state_mask,
2959            TrieMask::new(0),
2960            sub_branch_state_mask,
2961            vec![leaf_hash_a, leaf_hash_b],
2962            None,
2963        );
2964
2965        let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2966            std::iter::once((Nibbles::from_nibbles([0x2]), cached_sub_branch)).collect();
2967
2968        // The hashed cursor contains key_b and key_c (the root's other child). key_a was removed
2969        // (not in cursor)
2970        let mut harness = TrieTestHarness::new([(key_b, val), (key_c, val)].into_iter().collect());
2971        harness.set_trie_nodes(storage_nodes);
2972
2973        // Prefix set marks key_a as dirty (removed).
2974        let mut prefix_set_mut = PrefixSetMut::default();
2975        prefix_set_mut.insert(Nibbles::unpack(key_a));
2976        let prefix_set = prefix_set_mut.freeze();
2977
2978        // Compute root with cached branches + prefix set — triggers sub-branch collapse.
2979        let storage_trie_cursor =
2980            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2981        let hashed_storage_cursor = harness
2982            .hashed_cursor_factory()
2983            .hashed_storage_cursor(harness.hashed_address())
2984            .unwrap();
2985        let mut calculator =
2986            StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor)
2987                .with_prefix_set(prefix_set);
2988        let root_node = calculator
2989            .storage_root_node(harness.hashed_address())
2990            .expect("storage_root_node should succeed after branch collapse");
2991        let root_with_collapse =
2992            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2993
2994        // Compute reference root from scratch (no cached branches) using the full final state.
2995        let mut fresh_harness =
2996            TrieTestHarness::new([(key_b, val), (key_c, val)].into_iter().collect());
2997        fresh_harness.set_trie_nodes(BTreeMap::new());
2998        let storage_trie_cursor = fresh_harness
2999            .trie_cursor_factory()
3000            .storage_trie_cursor(fresh_harness.hashed_address())
3001            .unwrap();
3002        let hashed_storage_cursor = fresh_harness
3003            .hashed_cursor_factory()
3004            .hashed_storage_cursor(fresh_harness.hashed_address())
3005            .unwrap();
3006        let mut fresh_calculator =
3007            StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
3008        let fresh_root_node = fresh_calculator
3009            .storage_root_node(fresh_harness.hashed_address())
3010            .expect("fresh storage_root_node should succeed");
3011        let expected_root = fresh_calculator
3012            .compute_root_hash(core::slice::from_ref(&fresh_root_node))
3013            .unwrap()
3014            .unwrap();
3015
3016        pretty_assertions::assert_eq!(
3017            expected_root,
3018            root_with_collapse,
3019            "Root hash after collapsing branch (removed child before remaining) should match fresh computation"
3020        );
3021    }
3022
3023    /// Tests branch collapse when the removed child comes AFTER the remaining child.
3024    ///
3025    /// Same trie structure as "before" test, but with nibbles 4 and 9 instead of 0 and 1 for
3026    /// the sub-branch, and nibble 9 is removed. The removed child (nibble 9) comes after the
3027    /// remaining child (nibble 4).
3028    #[test]
3029    fn test_branch_collapse_removed_child_after_remaining() {
3030        reth_tracing::init_test_tracing();
3031
3032        let val = U256::from(1u64);
3033
3034        // key_a at sub-nibble 4, key_b at sub-nibble 9 (under root nibble 2).
3035        let key_a = B256::right_padding_from(&[0x24]); // root nibble 2, sub-nibble 4
3036        let key_b = B256::right_padding_from(&[0x29]); // root nibble 2, sub-nibble 9
3037        let key_c = B256::right_padding_from(&[0xb0]); // root nibble b
3038
3039        let leaf_hash_a = storage_leaf_hash(&Nibbles::unpack(key_a).slice(2..), &val);
3040        let leaf_hash_b = storage_leaf_hash(&Nibbles::unpack(key_b).slice(2..), &val);
3041
3042        // Only cache the sub-branch at path [2] — the root will be built from leaves.
3043        let sub_branch_state_mask = TrieMask::new((1 << 4) | (1 << 9));
3044        let cached_sub_branch = BranchNodeCompact::new(
3045            sub_branch_state_mask,
3046            TrieMask::new(0),
3047            sub_branch_state_mask,
3048            vec![leaf_hash_a, leaf_hash_b],
3049            None,
3050        );
3051
3052        let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
3053            std::iter::once((Nibbles::from_nibbles([0x2]), cached_sub_branch)).collect();
3054
3055        // The hashed cursor contains key_a and key_c. key_b was removed (not in cursor)
3056        let mut harness = TrieTestHarness::new([(key_a, val), (key_c, val)].into_iter().collect());
3057        harness.set_trie_nodes(storage_nodes);
3058
3059        // Prefix set marks key_b as dirty (removed).
3060        let mut prefix_set_mut = PrefixSetMut::default();
3061        prefix_set_mut.insert(Nibbles::unpack(key_b));
3062        let prefix_set = prefix_set_mut.freeze();
3063
3064        // Compute root with cached branches + prefix set — triggers sub-branch collapse.
3065        let storage_trie_cursor =
3066            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3067        let hashed_storage_cursor = harness
3068            .hashed_cursor_factory()
3069            .hashed_storage_cursor(harness.hashed_address())
3070            .unwrap();
3071        let mut calculator =
3072            StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor)
3073                .with_prefix_set(prefix_set);
3074        let root_node = calculator
3075            .storage_root_node(harness.hashed_address())
3076            .expect("storage_root_node should succeed after branch collapse");
3077        let root_with_collapse =
3078            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
3079
3080        // Compute reference root from scratch (no cached branches) using the full final state.
3081        let mut fresh_harness =
3082            TrieTestHarness::new([(key_a, val), (key_c, val)].into_iter().collect());
3083        fresh_harness.set_trie_nodes(BTreeMap::new());
3084        let storage_trie_cursor = fresh_harness
3085            .trie_cursor_factory()
3086            .storage_trie_cursor(fresh_harness.hashed_address())
3087            .unwrap();
3088        let hashed_storage_cursor = fresh_harness
3089            .hashed_cursor_factory()
3090            .hashed_storage_cursor(fresh_harness.hashed_address())
3091            .unwrap();
3092        let mut fresh_calculator =
3093            StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
3094        let fresh_root_node = fresh_calculator
3095            .storage_root_node(fresh_harness.hashed_address())
3096            .expect("fresh storage_root_node should succeed");
3097        let expected_root = fresh_calculator
3098            .compute_root_hash(core::slice::from_ref(&fresh_root_node))
3099            .unwrap()
3100            .unwrap();
3101
3102        pretty_assertions::assert_eq!(
3103            expected_root,
3104            root_with_collapse,
3105            "Root hash after collapsing branch (removed child after remaining) should match fresh computation"
3106        );
3107    }
3108
3109    #[test]
3110    fn test_cached_branch_extension_skips_diverging_target() {
3111        reth_tracing::init_test_tracing();
3112
3113        let val = U256::from(100u64);
3114
3115        // Keys whose first bytes directly set the nibble paths we need.
3116        let key_a0 = B256::right_padding_from(&[0x6a, 0x30]); // nibbles: 6,a,3,0,...
3117        let key_a1 = B256::right_padding_from(&[0x6a, 0x31]); // nibbles: 6,a,3,1,...
3118        let key_c = B256::right_padding_from(&[0x6a, 0x80]); // nibbles: 6,a,8,0,...
3119        let key_d = B256::right_padding_from(&[0x6b, 0x00]); // nibbles: 6,b,0,0,...
3120        let key_e = B256::right_padding_from(&[0x6c, 0x00]); // nibbles: 6,c,0,0,...
3121
3122        // Build a correct trie from all five leaves to get the expected root and real hashes.
3123        let all_storage: BTreeMap<B256, U256> =
3124            [(key_a0, val), (key_a1, val), (key_c, val), (key_d, val), (key_e, val)]
3125                .into_iter()
3126                .collect();
3127        let correct_harness = TrieTestHarness::new(all_storage.clone());
3128        let expected_root = correct_harness.original_root();
3129
3130        // Compute leaf hashes for constructing manual cached branch nodes.
3131        let leaf_hash_a0 = storage_leaf_hash(&Nibbles::unpack(key_a0).slice(4..), &val);
3132        let leaf_hash_a1 = storage_leaf_hash(&Nibbles::unpack(key_a1).slice(4..), &val);
3133        let leaf_hash_d = storage_leaf_hash(&Nibbles::unpack(key_d).slice(2..), &val);
3134        let leaf_hash_e = storage_leaf_hash(&Nibbles::unpack(key_e).slice(2..), &val);
3135
3136        // ── Construct cached branch at [6] ─────────────────────────────────────
3137        // state_mask: bits a, b, and c set.
3138        // hash_mask:  bits b and c — both have cached leaf hashes.  Bit a has no hash, so the
3139        //             calculator will seek the trie cursor to find a deeper cached branch.
3140        //
3141        // Having three children with two (b, c) NOT in the prefix set ensures
3142        // `should_skip_cached_branch` does NOT skip this branch (num_unmatched >= 2).
3143        let branch_6_state_mask = TrieMask::new((1 << 0xa) | (1 << 0xb) | (1 << 0xc));
3144        let branch_6_hash_mask = TrieMask::new((1 << 0xb) | (1 << 0xc));
3145        let branch_6 = BranchNodeCompact::new(
3146            branch_6_state_mask,
3147            TrieMask::new(0),
3148            branch_6_hash_mask,
3149            vec![leaf_hash_d, leaf_hash_e],
3150            None,
3151        );
3152
3153        // ── Construct cached branch at [6,a,3] ────────────────────────────────
3154        // state_mask: bits 0 and 1 set (children key_a0 and key_a1).
3155        // hash_mask:  both bits set — both children have cached hashes.
3156        let branch_6a3_state_mask = TrieMask::new((1 << 0) | (1 << 1));
3157        let branch_6a3 = BranchNodeCompact::new(
3158            branch_6a3_state_mask,
3159            TrieMask::new(0),
3160            branch_6a3_state_mask,
3161            vec![leaf_hash_a0, leaf_hash_a1],
3162            None,
3163        );
3164
3165        // Intentionally omit the branch at [6,a] — this is the inconsistency.
3166        let inconsistent_nodes: BTreeMap<Nibbles, BranchNodeCompact> = [
3167            (Nibbles::from_nibbles([0x6]), branch_6),
3168            (Nibbles::from_nibbles([0x6, 0xa, 0x3]), branch_6a3),
3169        ]
3170        .into_iter()
3171        .collect();
3172
3173        // Create harness with all five leaves but the inconsistent trie nodes.
3174        let mut harness = TrieTestHarness::new(all_storage);
3175        harness.set_trie_nodes(inconsistent_nodes);
3176
3177        // Mark key_c as dirty — in the real scenario the leaf was touched by execution.
3178        // The prefix set contains only key_c's full path. `should_skip_cached_branch` will
3179        // NOT skip branch [6] because two of its three children (b, c) are not in the set
3180        // (num_unmatched = 2 > 1). It also will not skip branch [6,a,3] because
3181        // `contains([6,a,3])` is false (key_c's nibbles 6,a,8,... do not start with 6,a,3).
3182        let mut prefix_set = PrefixSetMut::default();
3183        prefix_set.insert(Nibbles::unpack(key_c));
3184
3185        // ── Verify root hash ───────────────────────────────────────────────────
3186        let trie_cursor =
3187            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3188        let hashed_cursor = harness
3189            .hashed_cursor_factory()
3190            .hashed_storage_cursor(harness.hashed_address())
3191            .unwrap();
3192        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3193            .with_prefix_set(prefix_set.freeze());
3194
3195        let root_node = calculator
3196            .storage_root_node(harness.hashed_address())
3197            .expect("storage_root_node should succeed");
3198        let got_root = calculator
3199            .compute_root_hash(core::slice::from_ref(&root_node))
3200            .unwrap()
3201            .expect("should produce a root hash");
3202
3203        // With the bug, the calculator skips key_c and produces a wrong root.
3204        pretty_assertions::assert_eq!(
3205            expected_root,
3206            got_root,
3207            "Root hash should match correct trie; cached extension must not skip diverging leaves"
3208        );
3209
3210        // ── Verify proof for key_c contains nodes on its path ──────────────────
3211        let mut targets = vec![ProofV2Target::new(key_c)];
3212        let proofs = calculator
3213            .storage_proof(harness.hashed_address(), &mut targets)
3214            .expect("storage_proof should succeed");
3215
3216        let key_c_nibbles = Nibbles::unpack(key_c);
3217        let has_matching_node = proofs.iter().any(|node| key_c_nibbles.starts_with(&node.path));
3218        assert!(
3219            has_matching_node,
3220            "Proof for key_c should contain at least one node on key_c's path, got: {proofs:?}"
3221        );
3222    }
3223
3224    #[test]
3225    fn test_cached_branch_extension_skips_diverging_target_before() {
3226        reth_tracing::init_test_tracing();
3227
3228        let val = U256::from(100u64);
3229
3230        // Keys whose first bytes directly set the nibble paths we need.
3231        let key_a0 = B256::right_padding_from(&[0x6a, 0x80]); // nibbles: 6,a,8,0,...
3232        let key_a1 = B256::right_padding_from(&[0x6a, 0x81]); // nibbles: 6,a,8,1,...
3233        let key_c = B256::right_padding_from(&[0x6a, 0x30]); // nibbles: 6,a,3,0,... (BEFORE
3234                                                             // [6,a,8])
3235        let key_d = B256::right_padding_from(&[0x6b, 0x00]); // nibbles: 6,b,0,0,...
3236        let key_e = B256::right_padding_from(&[0x6c, 0x00]); // nibbles: 6,c,0,0,...
3237
3238        // Build a correct trie from all five leaves to get the expected root and real hashes.
3239        let all_storage: BTreeMap<B256, U256> =
3240            [(key_a0, val), (key_a1, val), (key_c, val), (key_d, val), (key_e, val)]
3241                .into_iter()
3242                .collect();
3243        let correct_harness = TrieTestHarness::new(all_storage.clone());
3244        let expected_root = correct_harness.original_root();
3245
3246        // Compute leaf hashes for constructing manual cached branch nodes.
3247        let leaf_hash_a0 = storage_leaf_hash(&Nibbles::unpack(key_a0).slice(4..), &val);
3248        let leaf_hash_a1 = storage_leaf_hash(&Nibbles::unpack(key_a1).slice(4..), &val);
3249        let leaf_hash_d = storage_leaf_hash(&Nibbles::unpack(key_d).slice(2..), &val);
3250        let leaf_hash_e = storage_leaf_hash(&Nibbles::unpack(key_e).slice(2..), &val);
3251
3252        // ── Construct cached branch at [6] ─────────────────────────────────────
3253        // state_mask: bits a, b, and c set.
3254        // hash_mask:  bits b and c — both have cached leaf hashes.  Bit a has no hash, so the
3255        //             calculator will seek the trie cursor to find a deeper cached branch.
3256        //
3257        // Having three children with two (b, c) NOT in the prefix set ensures
3258        // `should_skip_cached_branch` does NOT skip this branch (num_unmatched >= 2).
3259        let branch_6_state_mask = TrieMask::new((1 << 0xa) | (1 << 0xb) | (1 << 0xc));
3260        let branch_6_hash_mask = TrieMask::new((1 << 0xb) | (1 << 0xc));
3261        let branch_6 = BranchNodeCompact::new(
3262            branch_6_state_mask,
3263            TrieMask::new(0),
3264            branch_6_hash_mask,
3265            vec![leaf_hash_d, leaf_hash_e],
3266            None,
3267        );
3268
3269        // ── Construct cached branch at [6,a,8] ────────────────────────────────
3270        // state_mask: bits 0 and 1 set (children key_a0 and key_a1).
3271        // hash_mask:  both bits set — both children have cached hashes.
3272        let branch_6a8_state_mask = TrieMask::new((1 << 0) | (1 << 1));
3273        let branch_6a8 = BranchNodeCompact::new(
3274            branch_6a8_state_mask,
3275            TrieMask::new(0),
3276            branch_6a8_state_mask,
3277            vec![leaf_hash_a0, leaf_hash_a1],
3278            None,
3279        );
3280
3281        // Intentionally omit the branch at [6,a] — this is the inconsistency.
3282        let inconsistent_nodes: BTreeMap<Nibbles, BranchNodeCompact> = [
3283            (Nibbles::from_nibbles([0x6]), branch_6),
3284            (Nibbles::from_nibbles([0x6, 0xa, 0x8]), branch_6a8),
3285        ]
3286        .into_iter()
3287        .collect();
3288
3289        // Create harness with all five leaves but the inconsistent trie nodes.
3290        let mut harness = TrieTestHarness::new(all_storage);
3291        harness.set_trie_nodes(inconsistent_nodes);
3292
3293        // Mark key_c as dirty — it comes BEFORE the cached branch [6,a,8] in nibble order.
3294        let mut prefix_set = PrefixSetMut::default();
3295        prefix_set.insert(Nibbles::unpack(key_c));
3296
3297        // ── Verify root hash ───────────────────────────────────────────────────
3298        let trie_cursor =
3299            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3300        let hashed_cursor = harness
3301            .hashed_cursor_factory()
3302            .hashed_storage_cursor(harness.hashed_address())
3303            .unwrap();
3304        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3305            .with_prefix_set(prefix_set.freeze());
3306
3307        let root_node = calculator
3308            .storage_root_node(harness.hashed_address())
3309            .expect("storage_root_node should succeed");
3310        let got_root = calculator
3311            .compute_root_hash(core::slice::from_ref(&root_node))
3312            .unwrap()
3313            .expect("should produce a root hash");
3314
3315        // With the bug, the calculator skips key_c and produces a wrong root.
3316        pretty_assertions::assert_eq!(
3317            expected_root,
3318            got_root,
3319            "Root hash should match correct trie; cached extension must not skip diverging leaves before cached branch"
3320        );
3321
3322        // ── Verify proof for key_c contains nodes on its path ──────────────────
3323        let mut targets = vec![ProofV2Target::new(key_c)];
3324        let proofs = calculator
3325            .storage_proof(harness.hashed_address(), &mut targets)
3326            .expect("storage_proof should succeed");
3327
3328        let key_c_nibbles = Nibbles::unpack(key_c);
3329        let has_matching_node = proofs.iter().any(|node| key_c_nibbles.starts_with(&node.path));
3330        assert!(
3331            has_matching_node,
3332            "Proof for key_c should contain at least one node on key_c's path, got: {proofs:?}"
3333        );
3334    }
3335
3336    #[test]
3337    fn test_skipped_parent_branch_with_unskipped_child() {
3338        reth_tracing::init_test_tracing();
3339
3340        let val = U256::from(1u64);
3341        let updated_val = U256::from(2u64);
3342
3343        // We need cached branches at [2], [2,f], and [3] in the trie DB.
3344        let key_2 = B256::right_padding_from(&[0x20]);
3345        let key_2f00 = B256::right_padding_from(&[0x2f, 0x00]);
3346        let key_2f01 = B256::right_padding_from(&[0x2f, 0x01]);
3347        let key_2f10 = B256::right_padding_from(&[0x2f, 0x10]);
3348        let key_2f11 = B256::right_padding_from(&[0x2f, 0x11]);
3349        let key_300 = B256::right_padding_from(&[0x30, 0x00]);
3350        let key_301 = B256::right_padding_from(&[0x30, 0x10]);
3351        let key_310 = B256::right_padding_from(&[0x31, 0x00]);
3352        let key_311 = B256::right_padding_from(&[0x31, 0x10]);
3353        let key_500 = B256::right_padding_from(&[0x50, 0x00]);
3354        let key_501 = B256::right_padding_from(&[0x50, 0x10]);
3355        let key_510 = B256::right_padding_from(&[0x51, 0x00]);
3356        let key_511 = B256::right_padding_from(&[0x51, 0x10]);
3357
3358        let all_keys = [
3359            key_2, key_2f00, key_2f01, key_2f10, key_2f11, key_300, key_301, key_310, key_311,
3360            key_500, key_501, key_510, key_511,
3361        ];
3362
3363        let original_storage: BTreeMap<B256, U256> = all_keys.iter().map(|k| (*k, val)).collect();
3364        let harness = TrieTestHarness::new(original_storage);
3365
3366        // Verify that the expected branches exist in the trie.
3367        let trie_updates = harness.storage_trie_updates();
3368        assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x2])));
3369        assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x2, 0xf])));
3370        assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x3])));
3371
3372        // Change only key_2 — triggers skip of parent branch [2] while child [2,f] is not
3373        // skipped.
3374        let changeset: BTreeMap<B256, U256> = std::iter::once((key_2, updated_val)).collect();
3375        let (expected_root, _) = harness.get_root_with_updates(&changeset);
3376
3377        let mut updated_storage = harness.storage().clone();
3378        updated_storage.insert(key_2, updated_val);
3379
3380        let updated_hashed = MockHashedCursorFactory::new(
3381            BTreeMap::new(),
3382            std::iter::once((harness.hashed_address(), updated_storage)).collect(),
3383        );
3384
3385        let mut prefix_set = PrefixSetMut::default();
3386        prefix_set.insert(Nibbles::unpack(key_2));
3387
3388        let trie_cursor =
3389            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3390        let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
3391        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3392            .with_prefix_set(prefix_set.freeze());
3393        let root_node = calculator
3394            .storage_root_node(harness.hashed_address())
3395            .expect("storage_root_node should succeed");
3396
3397        let got_root = calculator
3398            .compute_root_hash(&[root_node])
3399            .expect("root hash should succeed")
3400            .expect("root should get hashed");
3401        pretty_assertions::assert_eq!(expected_root, got_root);
3402    }
3403
3404    #[test]
3405    fn test_cached_hash_with_deleted_leaf() {
3406        reth_tracing::init_test_tracing();
3407
3408        // Use different values to ensure distinct leaf hashes.
3409        let val_3 = U256::from(111u64);
3410        let val_5 = U256::from(222u64);
3411        let val_8 = U256::from(333u64);
3412
3413        // Keys under a common prefix `0x6_` to create a branch at path [6].
3414        // Use second byte to distinguish short keys (so they differ after position 2).
3415        let key_63 = B256::right_padding_from(&[0x63, 0xaa]); // nibble path: 6,3,a,a,...
3416        let key_65 = B256::right_padding_from(&[0x65, 0xbb]); // nibble path: 6,5,b,b,...
3417        let key_68 = B256::right_padding_from(&[0x68, 0xcc]); // nibble path: 6,8,c,c,...
3418
3419        // Compute leaf hashes. The branch at [6] consumes 2 nibbles (the branch path [6]
3420        // plus the child nibble), so each leaf's short key starts at position 2.
3421        let leaf_hash_3 = storage_leaf_hash(&Nibbles::unpack(key_63).slice(2..), &val_3);
3422        let leaf_hash_5 = storage_leaf_hash(&Nibbles::unpack(key_65).slice(2..), &val_5);
3423        let leaf_hash_8 = storage_leaf_hash(&Nibbles::unpack(key_68).slice(2..), &val_8);
3424
3425        // Build cached branch at [6] with state_mask and hash_mask bits for nibbles 3, 5, 8.
3426        let state_mask = TrieMask::new((1 << 3) | (1 << 5) | (1 << 8));
3427        let cached_branch = BranchNodeCompact::new(
3428            state_mask,
3429            TrieMask::new(0),
3430            state_mask, // hash_mask = state_mask (all children have cached hashes)
3431            vec![leaf_hash_3, leaf_hash_5, leaf_hash_8],
3432            None,
3433        );
3434
3435        let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
3436            std::iter::once((Nibbles::from_nibbles([0x6]), cached_branch)).collect();
3437
3438        // Compute the expected root from a fresh trie with just key_65 and key_68.
3439        let mut harness =
3440            TrieTestHarness::new([(key_65, val_5), (key_68, val_8)].into_iter().collect());
3441        let expected_root = harness.original_root();
3442
3443        // Update the harness with a cached trie node which will reference key_63 by hash.
3444        harness.set_trie_nodes(storage_nodes);
3445
3446        // Mark key_63 as dirty in the prefix set — in the real scenario the leaf was
3447        // deleted and the HashedPostState overlay masks it out.
3448        let mut prefix_set = PrefixSetMut::default();
3449        prefix_set.insert(Nibbles::unpack(key_63));
3450
3451        // Request a proof for key_63 (absence proof — no leaf exists).
3452        // Because the prefix set marks nibble 3's child path as dirty, the cached hash for
3453        // nibble 3 is skipped.
3454        let mut targets = vec![ProofV2Target::new(key_63)];
3455
3456        let trie_cursor =
3457            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3458        let hashed_cursor = harness
3459            .hashed_cursor_factory()
3460            .hashed_storage_cursor(harness.hashed_address())
3461            .unwrap();
3462        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3463            .with_prefix_set(prefix_set.freeze());
3464
3465        let proofs = calculator
3466            .storage_proof(harness.hashed_address(), &mut targets)
3467            .expect("storage_proof should succeed");
3468        assert_eq!(1, proofs.len());
3469        let got_root = calculator
3470            .compute_root_hash(&proofs)
3471            .expect("compute_root_hash should succeed")
3472            .expect("should produce a root hash (proof contains root node)");
3473
3474        // With the bug, nibble 5 gets hashes[0] (nibble 3's hash) and nibble 8 gets
3475        // hashes[1] (nibble 5's hash), producing a wrong root.
3476        pretty_assertions::assert_eq!(
3477            expected_root,
3478            got_root,
3479            "Root hash should match trie without key_63; cached hash index is off when \
3480             an earlier hashed child has no leaves (absence proof target)"
3481        );
3482    }
3483}