Skip to main content

reth_trie/proof_v2/
calculator.rs

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