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