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        let Some(parent_prefix) = sub_trie_targets.parent_prefix else {
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
1367        // An exact match reconstructed the already-revealed parent; its targeted children were
1368        // retained while that parent branch was popped.
1369        if root_full_path == parent_prefix {
1370            return Ok(())
1371        }
1372
1373        // At this point we have a "root" node which the calculator has based at 0x (empty path),
1374        // but the subtrie targets indicate that there is a known parent branch at parent_prefix
1375        // which this root should be rebased onto.
1376
1377        // The local root of a partial calculation must be at or below its known parent.
1378        if !root_full_path.starts_with(&parent_prefix) {
1379            return Err(StateProofError::TrieInconsistency(format!(
1380                "local root path {root_full_path:?} does not start with parent prefix \
1381                 {parent_prefix:?}",
1382            )))
1383        }
1384
1385        // Keep the parent branch's child nibble in the proof path so the local root attaches
1386        // directly below that parent.
1387        let child_path_len = parent_prefix.len() + 1;
1388        let child_path = root_full_path.slice_unchecked(0, child_path_len);
1389
1390        // It's possible that the local root lies on a child which is not targeted.
1391        if !sub_trie_targets
1392            .targets
1393            .iter()
1394            .any(|target| target.key_nibbles.starts_with(&child_path))
1395        {
1396            return Ok(())
1397        }
1398
1399        // Retain the requested child with only the path below its parent edge in the short key.
1400        root_node.trim_short_key_prefix(child_path_len);
1401        self.rlp_encode_buf.clear();
1402        let root_node = root_node.into_proof_trie_node(child_path, &mut self.rlp_encode_buf)?;
1403        self.retained_proofs.push(root_node);
1404
1405        Ok(())
1406    }
1407
1408    /// Clears internal computation state. Called after errors to ensure the calculator is not
1409    /// left in a partially-computed state when reused.
1410    fn clear_computation_state(&mut self) {
1411        self.branch_stack.clear();
1412        self.branch_path = Nibbles::new();
1413        self.child_stack.clear();
1414        self.cached_branch_stack.clear();
1415        self.retained_proofs.clear();
1416    }
1417
1418    /// Internal implementation of proof calculation. Assumes both cursors have already been reset.
1419    /// See docs on [`Self::proof`] for expected behavior.
1420    fn proof_inner(
1421        &mut self,
1422        value_encoder: &mut VE,
1423        targets: &mut [ProofV2Target],
1424    ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1425        // If there are no targets then nothing could be returned, return early.
1426        if targets.is_empty() {
1427            trace!(target: TRACE_TARGET, "Empty targets, returning");
1428            return Ok(Vec::new())
1429        }
1430
1431        // Initialize the variables which track the state of the two cursors. Both indicate the
1432        // cursors are unseeked.
1433        let mut trie_cursor_state = TrieCursorState::unseeked();
1434        let mut hashed_cursor_state = HashedCursorState::unseeked();
1435        let mut previous_traversal_bounds: Option<(Nibbles, Option<Nibbles>)> = None;
1436
1437        // Divide targets into bounded ranges, each corresponding to the direct children of one
1438        // already-revealed parent, and handle all proofs within that range.
1439        for sub_trie_targets in iter_sub_trie_targets(targets) {
1440            let traversal_lower_bound = sub_trie_targets.lower_bound;
1441            let traversal_upper_bound = sub_trie_targets.upper_bound;
1442            if previous_traversal_bounds.is_some_and(|(_, previous_upper_bound)| {
1443                previous_upper_bound.is_none_or(|upper_bound| upper_bound > traversal_lower_bound)
1444            }) {
1445                if trie_cursor_state.needs_reset_before_seek(&traversal_lower_bound) {
1446                    trace!(
1447                        target: TRACE_TARGET,
1448                        ?previous_traversal_bounds,
1449                        ?traversal_lower_bound,
1450                        ?traversal_upper_bound,
1451                        "Resetting trie cursor before overlapping or backward traversal range",
1452                    );
1453                    self.trie_cursor.reset();
1454                    trie_cursor_state = TrieCursorState::unseeked();
1455                }
1456                if hashed_cursor_state.needs_reset_before_seek(&traversal_lower_bound) {
1457                    trace!(
1458                        target: TRACE_TARGET,
1459                        ?previous_traversal_bounds,
1460                        ?traversal_lower_bound,
1461                        ?traversal_upper_bound,
1462                        "Resetting hashed cursor before overlapping or backward traversal range",
1463                    );
1464                    self.hashed_cursor.reset();
1465                    hashed_cursor_state = HashedCursorState::unseeked();
1466                }
1467            }
1468
1469            if let Err(err) = self.proof_subtrie(
1470                value_encoder,
1471                &mut trie_cursor_state,
1472                &mut hashed_cursor_state,
1473                sub_trie_targets,
1474            ) {
1475                self.clear_computation_state();
1476                return Err(err);
1477            }
1478
1479            previous_traversal_bounds = Some((traversal_lower_bound, traversal_upper_bound));
1480        }
1481
1482        trace!(
1483            target: TRACE_TARGET,
1484            retained_proofs_len = ?self.retained_proofs.len(),
1485            "proof_inner: returning",
1486        );
1487        self.retained_proofs.sort_unstable_by(|a, b| depth_first::cmp(&a.path, &b.path));
1488        self.retained_proofs.dedup_by(|a, b| a.path == b.path);
1489        Ok(core::mem::take(&mut self.retained_proofs))
1490    }
1491
1492    /// Generate a proof for the given targets.
1493    ///
1494    /// Given a set of [`ProofV2Target`]s, returns nodes whose paths are a prefix of any target. The
1495    /// returned nodes are sorted in depth-first post-order (children before parents).
1496    ///
1497    /// Targets may be supplied in any order. Calculation sorts the slice in place by known parent
1498    /// context, then by key within each group.
1499    #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
1500    pub fn proof(
1501        &mut self,
1502        value_encoder: &mut VE,
1503        targets: &mut [ProofV2Target],
1504    ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1505        self.trie_cursor.reset();
1506        self.hashed_cursor.reset();
1507        self.proof_inner(value_encoder, targets)
1508    }
1509
1510    /// Computes the root hash from a set of proof nodes.
1511    ///
1512    /// Returns `None` if there is no root node (partial proof), otherwise returns the hash of the
1513    /// root node.
1514    ///
1515    /// This method reuses the internal RLP encode buffer for efficiency.
1516    pub fn compute_root_hash(
1517        &mut self,
1518        proof_nodes: &[ProofTrieNodeV2],
1519    ) -> Result<Option<B256>, StateProofError> {
1520        // Find the root node (node at empty path)
1521        let root_node = proof_nodes.iter().find(|node| node.path.is_empty());
1522
1523        let Some(root) = root_node else {
1524            return Ok(None);
1525        };
1526
1527        // Compute the hash of the root node
1528        self.rlp_encode_buf.clear();
1529        root.node.encode(&mut self.rlp_encode_buf);
1530        let root_hash = keccak256(&self.rlp_encode_buf);
1531
1532        Ok(Some(root_hash))
1533    }
1534
1535    /// Calculates the root node of the trie.
1536    ///
1537    /// This method does not accept targets nor retain proofs. Returns the root node which can
1538    /// be used to compute the root hash via [`Self::compute_root_hash`].
1539    #[instrument(target = TRACE_TARGET, level = "trace", skip(self, value_encoder))]
1540    pub fn root_node(
1541        &mut self,
1542        value_encoder: &mut VE,
1543    ) -> Result<ProofTrieNodeV2, StateProofError> {
1544        self.trie_cursor.reset();
1545        self.hashed_cursor.reset();
1546
1547        // Initialize the variables which track the state of the two cursors. Both indicate the
1548        // cursors are unseeked.
1549        let mut trie_cursor_state = TrieCursorState::unseeked();
1550        let mut hashed_cursor_state = HashedCursorState::unseeked();
1551
1552        static EMPTY_TARGETS: [ProofV2Target; 0] = [];
1553        let sub_trie_targets = SubTrieTargets {
1554            lower_bound: Nibbles::new(),
1555            upper_bound: None,
1556            parent_prefix: None,
1557            targets: &EMPTY_TARGETS,
1558        };
1559
1560        if let Err(err) = self.proof_subtrie(
1561            value_encoder,
1562            &mut trie_cursor_state,
1563            &mut hashed_cursor_state,
1564            sub_trie_targets,
1565        ) {
1566            self.clear_computation_state();
1567            return Err(err);
1568        }
1569
1570        // `proof_subtrie` retains the root node when there is no known parent, regardless of
1571        // whether there are any targets.
1572        let mut proofs = core::mem::take(&mut self.retained_proofs);
1573        trace!(
1574            target: TRACE_TARGET,
1575            proofs_len = ?proofs.len(),
1576            "root_node: extracting root",
1577        );
1578
1579        // The root node is at the empty path. Since there is no parent and targets is empty, there
1580        // should be no other retained proofs.
1581        debug_assert_eq!(
1582            proofs.len(), 1,
1583            "prefix is empty, parent path is None, and targets is empty, so there must be only the root node"
1584        );
1585
1586        // Find and remove the root node (node at empty path)
1587        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");
1588
1589        Ok(root_node)
1590    }
1591}
1592
1593/// A proof calculator for storage tries.
1594pub type StorageProofCalculator<TC, HC> = ProofCalculator<TC, HC, StorageValueEncoder>;
1595
1596impl<TC, HC> StorageProofCalculator<TC, HC>
1597where
1598    TC: TrieStorageCursor,
1599    HC: HashedStorageCursor<Value = U256>,
1600{
1601    /// Create a new [`StorageProofCalculator`] instance.
1602    pub fn new_storage(trie_cursor: TC, hashed_cursor: HC) -> Self {
1603        Self::new(trie_cursor, hashed_cursor)
1604    }
1605
1606    /// Generate a proof for a storage trie at the given hashed address.
1607    ///
1608    /// Given a set of [`ProofV2Target`]s, returns nodes whose paths are a prefix of any target. The
1609    /// returned nodes are sorted in depth-first post-order (children before parents).
1610    ///
1611    /// Targets may be supplied in any order. Calculation sorts the slice in place by known parent
1612    /// context, then by key within each group.
1613    #[instrument(target = TRACE_TARGET, level = "trace", skip(self, targets))]
1614    pub fn storage_proof(
1615        &mut self,
1616        hashed_address: B256,
1617        targets: &mut [ProofV2Target],
1618    ) -> Result<Vec<ProofTrieNodeV2>, StateProofError> {
1619        self.hashed_cursor.set_hashed_address(hashed_address);
1620
1621        // Shortcut: check if storage is empty
1622        if self.hashed_cursor.is_storage_empty()? {
1623            return Ok(if targets.iter().any(|target| !target.parent.is_known()) {
1624                vec![ProofTrieNodeV2 {
1625                    path: Nibbles::default(),
1626                    node: TrieNodeV2::EmptyRoot,
1627                    masks: None,
1628                }]
1629            } else {
1630                Vec::new()
1631            })
1632        }
1633
1634        // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has
1635        // been checked.
1636        self.trie_cursor.set_hashed_address(hashed_address);
1637
1638        // Create a mutable storage value encoder
1639        let mut storage_value_encoder = StorageValueEncoder;
1640        self.proof_inner(&mut storage_value_encoder, targets)
1641    }
1642
1643    /// Calculates the root node of a storage trie.
1644    ///
1645    /// This method does not accept targets nor retain proofs. Returns the root node which can
1646    /// be used to compute the root hash via [`Self::compute_root_hash`].
1647    #[instrument(target = TRACE_TARGET, level = "trace", skip(self))]
1648    pub fn storage_root_node(
1649        &mut self,
1650        hashed_address: B256,
1651    ) -> Result<ProofTrieNodeV2, StateProofError> {
1652        self.hashed_cursor.set_hashed_address(hashed_address);
1653
1654        if self.hashed_cursor.is_storage_empty()? {
1655            return Ok(ProofTrieNodeV2 {
1656                path: Nibbles::default(),
1657                node: TrieNodeV2::EmptyRoot,
1658                masks: None,
1659            })
1660        }
1661
1662        // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has
1663        // been checked.
1664        self.trie_cursor.set_hashed_address(hashed_address);
1665
1666        // Create a mutable storage value encoder
1667        let mut storage_value_encoder = StorageValueEncoder;
1668        self.root_node(&mut storage_value_encoder)
1669    }
1670
1671    /// Calculates the storage root hash of the account at `hashed_address`.
1672    pub fn storage_root(&mut self, hashed_address: B256) -> Result<B256, StateProofError> {
1673        let root_node = self.storage_root_node(hashed_address)?;
1674        Ok(self
1675            .compute_root_hash(core::slice::from_ref(&root_node))?
1676            .expect("storage_root_node returns a node at empty path"))
1677    }
1678}
1679
1680/// Helper type wrapping a slice of [`ProofV2Target`]s, primarily used to iterate through targets in
1681/// [`ProofCalculator::should_retain`].
1682///
1683/// It is assumed that the underlying slice is never empty, and that the iterator is never
1684/// exhausted.
1685#[derive(Clone)]
1686struct TargetsCursor<'a> {
1687    targets: &'a [ProofV2Target],
1688    i: usize,
1689}
1690
1691impl<'a> TargetsCursor<'a> {
1692    /// Wraps a slice of [`ProofV2Target`]s with the `TargetsCursor`.
1693    ///
1694    /// # Panics
1695    ///
1696    /// Will panic in debug mode if called with an empty slice.
1697    fn new(targets: &'a [ProofV2Target]) -> Self {
1698        debug_assert!(!targets.is_empty());
1699        Self { targets, i: 0 }
1700    }
1701
1702    /// Returns the current and next [`ProofV2Target`] that the cursor is pointed at.
1703    fn current(&self) -> (&'a ProofV2Target, Option<&'a ProofV2Target>) {
1704        (&self.targets[self.i], self.targets.get(self.i + 1))
1705    }
1706
1707    /// Iterates the cursor forward.
1708    ///
1709    /// # Panics
1710    ///
1711    /// Will panic if the cursor is exhausted.
1712    fn next(&mut self) -> (&'a ProofV2Target, Option<&'a ProofV2Target>) {
1713        self.i += 1;
1714        debug_assert!(self.i < self.targets.len());
1715        self.current()
1716    }
1717
1718    // Iterate forwards over the slice, starting from the [`ProofV2Target`] after the current.
1719    fn skip_iter(&self) -> impl Iterator<Item = &'a ProofV2Target> {
1720        self.targets[self.i + 1..].iter()
1721    }
1722
1723    /// Iterated backwards over the slice, starting from the [`ProofV2Target`] previous to the
1724    /// current.
1725    fn rev_iter(&self) -> impl Iterator<Item = &'a ProofV2Target> {
1726        self.targets[..self.i].iter().rev()
1727    }
1728}
1729
1730/// Used to track the state of the trie cursor, allowing us to differentiate between a branch having
1731/// been taken (used as a cached branch) and the cursor having been exhausted.
1732#[derive(Debug)]
1733enum TrieCursorState {
1734    /// The initial state of the cursor, indicating it's never been seeked.
1735    Unseeked,
1736    /// Cursor is seeked to this path and the node has not been used yet.
1737    Available(Nibbles, BranchNodeCompact),
1738    /// Cursor is seeked to this path, but the node has been used.
1739    Taken(Nibbles),
1740    /// Cursor has been exhausted after seeking from the given lower bound.
1741    Exhausted(Nibbles),
1742}
1743
1744impl TrieCursorState {
1745    /// Creates a [`Self::Unseeked`] based on an entry returned from the cursor itself.
1746    const fn unseeked() -> Self {
1747        Self::Unseeked
1748    }
1749
1750    /// Creates a [`Self`] based on an entry returned from the cursor itself.
1751    fn seeked(key: Nibbles, entry: Option<(Nibbles, BranchNodeCompact)>) -> Self {
1752        entry.map_or(Self::Exhausted(key), |(path, node)| Self::Available(path, node))
1753    }
1754
1755    /// Returns the path the cursor is seeked to, or None if it's exhausted.
1756    ///
1757    /// # Panics
1758    ///
1759    /// Panics if the cursor is unseeked.
1760    const fn path(&self) -> Option<&Nibbles> {
1761        match self {
1762            Self::Unseeked => panic!("cursor is unseeked"),
1763            Self::Available(path, _) | Self::Taken(path) => Some(path),
1764            Self::Exhausted(_) => None,
1765        }
1766    }
1767
1768    /// Returns true if the cursor must seek to be usable for a range starting at `path`.
1769    fn needs_seek_to(&self, path: &Nibbles) -> bool {
1770        match self {
1771            Self::Unseeked | Self::Taken(_) => true,
1772            Self::Available(current_path, _) => current_path < path,
1773            Self::Exhausted(_) => false,
1774        }
1775    }
1776
1777    /// Returns true if seeking to `key` requires resetting the forward-only cursor.
1778    fn needs_reset_before_seek(&self, key: &Nibbles) -> bool {
1779        match self {
1780            Self::Unseeked => false,
1781            Self::Available(path, _) | Self::Taken(path) => path > key,
1782            Self::Exhausted(exhausted_at) => exhausted_at > key,
1783        }
1784    }
1785
1786    /// Takes the path and node from a [`Self::Available`]. Panics if not [`Self::Available`].
1787    fn take(&mut self) -> (Nibbles, BranchNodeCompact) {
1788        let Self::Available(path, _) = self else {
1789            panic!("take called on non-Available: {self:?}")
1790        };
1791
1792        let path = *path;
1793        let Self::Available(path, node) = core::mem::replace(self, Self::Taken(path)) else {
1794            unreachable!("already checked that self is Self::Available");
1795        };
1796
1797        (path, node)
1798    }
1799}
1800
1801/// Used to track the state of the hashed cursor, including the path that established exhaustion.
1802enum HashedCursorState<V> {
1803    /// The initial state of the cursor, indicating it's never been seeked.
1804    Unseeked,
1805    /// Cursor is seeked to this path and the value has not been used yet.
1806    Available(Nibbles, V),
1807    /// Cursor has been exhausted at or after the given path.
1808    Exhausted(Nibbles),
1809}
1810
1811impl<V> HashedCursorState<V> {
1812    /// Creates a [`Self::Unseeked`] state.
1813    const fn unseeked() -> Self {
1814        Self::Unseeked
1815    }
1816
1817    /// Creates a [`Self`] based on an entry returned from the cursor itself.
1818    fn seeked(key: Nibbles, entry: Option<(Nibbles, V)>) -> Self {
1819        entry.map_or(Self::Exhausted(key), |(path, value)| Self::Available(path, value))
1820    }
1821
1822    /// Returns the path the cursor is seeked to, or None if it's unseeked or exhausted.
1823    const fn path(&self) -> Option<&Nibbles> {
1824        match self {
1825            Self::Available(path, _) => Some(path),
1826            Self::Unseeked | Self::Exhausted(_) => None,
1827        }
1828    }
1829
1830    /// Returns true if the cursor must seek to be usable for a range starting at `key`.
1831    fn needs_seek_to(&self, key: &Nibbles) -> bool {
1832        match self {
1833            Self::Unseeked => true,
1834            Self::Available(path, _) => path < key,
1835            Self::Exhausted(exhausted_at) => exhausted_at > key,
1836        }
1837    }
1838
1839    /// Returns true if seeking to `key` requires resetting the forward-only cursor.
1840    fn needs_reset_before_seek(&self, key: &Nibbles) -> bool {
1841        match self {
1842            Self::Unseeked => false,
1843            Self::Available(path, _) => path > key,
1844            Self::Exhausted(exhausted_at) => exhausted_at >= key,
1845        }
1846    }
1847
1848    /// Takes the path and value from a [`Self::Available`]. Panics if not [`Self::Available`].
1849    fn take(&mut self) -> (Nibbles, V) {
1850        match core::mem::replace(self, Self::Unseeked) {
1851            Self::Available(path, value) => (path, value),
1852            _ => panic!("take called on non-Available hashed cursor state"),
1853        }
1854    }
1855}
1856
1857/// Describes the state of the currently cached branch node (if any).
1858enum PopCachedBranchOutcome {
1859    /// Cached branch has been popped from the `cached_branch_stack` and is ready to be used.
1860    Popped((Nibbles, BranchNodeCompact)),
1861    /// All cached branches have been exhausted.
1862    Exhausted,
1863    /// Need to calculate leaves from this range (exclusive upper) before the cached branch
1864    /// (catch-up range). If None then
1865    CalculateLeaves((Nibbles, Option<Nibbles>)),
1866}
1867
1868#[cfg(test)]
1869mod tests {
1870    use super::*;
1871    use crate::{
1872        hashed_cursor::{
1873            mock::MockHashedCursorFactory, noop::NoopHashedCursor, HashedCursorFactory,
1874            HashedPostStateCursor,
1875        },
1876        proof::StorageProof as LegacyStorageProof,
1877        test_utils::TrieTestHarness,
1878        trie_cursor::{
1879            depth_first, noop::NoopStorageTrieCursor, InMemoryTrieCursor, TrieCursorFactory,
1880        },
1881    };
1882    use alloy_primitives::map::B256Set;
1883    use alloy_rlp::Decodable;
1884    use alloy_trie::proof::AddedRemovedKeys;
1885    use itertools::Itertools;
1886    use reth_trie_common::{
1887        prefix_set::{PrefixSet, PrefixSetMut},
1888        updates::TrieUpdatesSorted,
1889        HashedPostState, HashedStorage, ProofTrieNode, ProofV2TargetParent, TrieNode,
1890        EMPTY_ROOT_HASH,
1891    };
1892    use std::collections::BTreeMap;
1893
1894    /// Converts legacy proofs to V2 proofs by combining extension nodes with their child branch
1895    /// nodes.
1896    ///
1897    /// In the legacy proof format, extension nodes and branch nodes are separate. In the V2 format,
1898    /// they are combined into a single `BranchNodeV2` where the extension's key becomes the
1899    /// branch's `key` field.
1900    ///
1901    /// Converts legacy proofs (sorted in depth-first order) to V2 format.
1902    ///
1903    /// In depth-first order, children come BEFORE parents. So when we encounter an extension node,
1904    /// its child branch has already been processed and is in the result. We need to pop it and
1905    /// combine it with the extension.
1906    fn convert_legacy_proofs_to_v2(legacy_proofs: &[ProofTrieNode]) -> Vec<ProofTrieNodeV2> {
1907        ProofTrieNodeV2::from_sorted_trie_nodes(
1908            legacy_proofs.iter().map(|p| (p.path, p.node.clone(), p.masks)),
1909        )
1910    }
1911
1912    /// Projects a legacy proof node into the representation requested by a V2 target.
1913    fn project_legacy_proof_node(
1914        node: &ProofTrieNodeV2,
1915        target: &ProofV2Target,
1916    ) -> Option<ProofTrieNodeV2> {
1917        let Some(parent_path_len) = target.parent.path_len() else {
1918            return target.key_nibbles.starts_with(&node.path).then(|| node.clone())
1919        };
1920
1921        if node.path.len() > parent_path_len {
1922            return target.key_nibbles.starts_with(&node.path).then(|| node.clone())
1923        }
1924
1925        let logical_path = match &node.node {
1926            TrieNodeV2::Leaf(leaf) => node.path.join(&leaf.key),
1927            TrieNodeV2::Branch(branch) => node.path.join(&branch.key),
1928            TrieNodeV2::EmptyRoot | TrieNodeV2::Extension(_) => return None,
1929        };
1930        let child_path_len = parent_path_len + 1;
1931        if logical_path.len() < child_path_len {
1932            return None
1933        }
1934
1935        let child_path = logical_path.slice(0..child_path_len);
1936        if !target.key_nibbles.starts_with(&child_path) {
1937            return None
1938        }
1939
1940        let trim_len = child_path_len - node.path.len();
1941        let mut projected = node.clone();
1942        projected.path = child_path;
1943        match &mut projected.node {
1944            TrieNodeV2::Leaf(leaf) => leaf.key = leaf.key.slice(trim_len..),
1945            TrieNodeV2::Branch(branch) => {
1946                branch.key = branch.key.slice(trim_len..);
1947                if branch.key.is_empty() {
1948                    branch.branch_rlp_node = None;
1949                }
1950            }
1951            TrieNodeV2::EmptyRoot | TrieNodeV2::Extension(_) => unreachable!(),
1952        }
1953        Some(projected)
1954    }
1955
1956    /// Builds the exact V2 representation expected for a legacy proof and set of targets.
1957    fn project_legacy_proof(
1958        legacy_nodes: &[ProofTrieNodeV2],
1959        targets: &[ProofV2Target],
1960    ) -> Vec<ProofTrieNodeV2> {
1961        let mut projected = targets
1962            .iter()
1963            .flat_map(|target| {
1964                legacy_nodes.iter().filter_map(move |node| project_legacy_proof_node(node, target))
1965            })
1966            .collect::<Vec<_>>();
1967        projected.sort_unstable_by(|a, b| depth_first::cmp(&a.path, &b.path));
1968        projected.dedup_by(|a, b| {
1969            if a.path != b.path {
1970                return false
1971            }
1972            assert_eq!(a, b, "target projections disagree at path {:?}", a.path);
1973            true
1974        });
1975        projected
1976    }
1977
1978    /// A test harness for comparing `StorageProofCalculator` and legacy `StorageProof`
1979    /// implementations.
1980    ///
1981    /// Wraps [`TrieTestHarness`] and adds a method to test that both proof implementations
1982    /// produce equivalent results for storage proofs.
1983    struct ProofTestHarness {
1984        inner: TrieTestHarness,
1985    }
1986
1987    impl std::ops::Deref for ProofTestHarness {
1988        type Target = TrieTestHarness;
1989        fn deref(&self) -> &Self::Target {
1990            &self.inner
1991        }
1992    }
1993
1994    impl ProofTestHarness {
1995        /// Creates a new test harness from a map of hashed storage slots to values.
1996        fn new(storage: BTreeMap<B256, U256>) -> Self {
1997            Self { inner: TrieTestHarness::new(storage) }
1998        }
1999
2000        /// Computes the storage root while treating the supplied prefixes as dirty.
2001        fn root_with_prefix_set(&self, prefix_set: PrefixSet) -> Option<B256> {
2002            let trie_cursor =
2003                self.trie_cursor_factory().storage_trie_cursor(self.hashed_address()).unwrap();
2004            let hashed_cursor =
2005                self.hashed_cursor_factory().hashed_storage_cursor(self.hashed_address()).unwrap();
2006            let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2007                .with_prefix_set(prefix_set);
2008
2009            let mut targets = [ProofV2Target::new(B256::ZERO)];
2010            let proof = calculator.storage_proof(self.hashed_address(), &mut targets).unwrap();
2011            calculator.compute_root_hash(&proof).unwrap()
2012        }
2013
2014        /// Asserts that `StorageProofCalculator` and legacy `StorageProof` produce equivalent
2015        /// results for storage proofs.
2016        fn assert_proof(
2017            &self,
2018            targets: impl IntoIterator<Item = ProofV2Target>,
2019        ) -> Result<(), StateProofError> {
2020            let mut targets_vec = targets.into_iter().collect::<Vec<_>>();
2021
2022            // Get v2 proof and root hash via harness
2023            let (proof_v2_result, root_hash) = self.proof_v2(&mut targets_vec);
2024
2025            // Verify the root hash matches the expected root (if the proof contains a root
2026            // node)
2027            if let Some(root_hash) = root_hash {
2028                pretty_assertions::assert_eq!(self.original_root(), root_hash);
2029            }
2030
2031            // Fully materialize the legacy proof so compressed branches can be projected across
2032            // arbitrary parent boundaries, including absence targets that diverge inside them.
2033            let legacy_targets = targets_vec
2034                .iter()
2035                .map(|target| B256::from_slice(&target.key_nibbles.pack()))
2036                .chain(self.storage().keys().copied())
2037                .collect::<B256Set>();
2038
2039            // Call legacy StorageProof::storage_multiproof
2040            let proof_legacy_result = LegacyStorageProof::new_hashed(
2041                self.trie_cursor_factory(),
2042                self.hashed_cursor_factory(),
2043                self.hashed_address(),
2044            )
2045            .with_branch_node_masks(true)
2046            .with_added_removed_keys(Some(AddedRemovedKeys::default().with_assume_added(true)))
2047            .storage_multiproof(legacy_targets)?;
2048
2049            // Decode and sort legacy proof nodes
2050            let proof_legacy_nodes = proof_legacy_result
2051                .subtree
2052                .iter()
2053                .map(|(path, node_enc)| {
2054                    let mut buf = node_enc.as_ref();
2055                    let node = TrieNode::decode(&mut buf)
2056                        .expect("legacy implementation should not produce malformed proof nodes");
2057
2058                    let masks = if path.is_empty() {
2059                        None
2060                    } else {
2061                        proof_legacy_result.branch_node_masks.get(path).copied()
2062                    };
2063
2064                    ProofTrieNode { path: *path, node, masks }
2065                })
2066                .sorted_by(|a, b| depth_first::cmp(&a.path, &b.path))
2067                .collect::<Vec<_>>();
2068
2069            // Convert legacy proofs to V2 proofs by combining extensions with their child branches
2070            let all_legacy_nodes_v2 = convert_legacy_proofs_to_v2(&proof_legacy_nodes);
2071
2072            let expected_v2 = project_legacy_proof(&all_legacy_nodes_v2, &targets_vec);
2073            pretty_assertions::assert_eq!(expected_v2, proof_v2_result);
2074
2075            Ok(())
2076        }
2077    }
2078
2079    /// Tests that `clear_computation_state` properly resets internal stacks, allowing a
2080    /// `StorageProofCalculator` to be reused after a mid-computation error left stale state.
2081    /// Before the fix, stale data in `branch_stack`, `child_stack`, and `branch_path`
2082    /// could cause a `usize` underflow panic in `pop_branch`.
2083    #[test]
2084    fn test_proof_calculator_reuse_after_error() {
2085        reth_tracing::init_test_tracing();
2086
2087        let slots = [
2088            B256::right_padding_from(&[0x10]),
2089            B256::right_padding_from(&[0x20]),
2090            B256::right_padding_from(&[0x30]),
2091            B256::right_padding_from(&[0x40]),
2092        ];
2093        let storage: BTreeMap<B256, U256> =
2094            slots.iter().map(|&s| (s, U256::from(100u64))).collect();
2095
2096        let harness = ProofTestHarness::new(storage);
2097
2098        let trie_cursor_factory = harness.trie_cursor_factory();
2099        let hashed_cursor_factory = harness.hashed_cursor_factory();
2100
2101        let hashed_address = harness.hashed_address();
2102        let trie_cursor = trie_cursor_factory.storage_trie_cursor(hashed_address).unwrap();
2103        let hashed_cursor = hashed_cursor_factory.hashed_storage_cursor(hashed_address).unwrap();
2104        let mut proof_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
2105
2106        // Simulate stale state left by a mid-computation error: push fake entries onto internal
2107        // stacks and set a non-empty branch_path.
2108        proof_calculator
2109            .branch_stack
2110            .push(ProofTrieBranch { ext_len: 2, state_mask: TrieMask::new(0b1111) });
2111        proof_calculator
2112            .branch_stack
2113            .push(ProofTrieBranch { ext_len: 0, state_mask: TrieMask::new(0b11) });
2114        proof_calculator.child_stack.push(ProofTrieBranchChild::RlpNode {
2115            node: RlpNode::word_rlp(&B256::ZERO),
2116            short_key: Nibbles::new(),
2117            hash_mask_bit: false,
2118            tree_mask_bit: false,
2119        });
2120        proof_calculator.branch_path = Nibbles::from_nibbles([0x1, 0x2, 0x3]);
2121
2122        // clear_computation_state should reset everything so a subsequent call works.
2123        proof_calculator.clear_computation_state();
2124
2125        let mut sorted_slots = slots.to_vec();
2126        sorted_slots.sort();
2127        let mut targets: Vec<ProofV2Target> =
2128            sorted_slots.iter().copied().map(ProofV2Target::new).collect();
2129
2130        let result = proof_calculator.storage_proof(hashed_address, &mut targets).unwrap();
2131
2132        // Compare against a fresh calculator to verify correctness.
2133        let trie_cursor = trie_cursor_factory.storage_trie_cursor(hashed_address).unwrap();
2134        let hashed_cursor = hashed_cursor_factory.hashed_storage_cursor(hashed_address).unwrap();
2135        let mut fresh_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
2136        let fresh_result = fresh_calculator.storage_proof(hashed_address, &mut targets).unwrap();
2137
2138        pretty_assertions::assert_eq!(fresh_result, result);
2139    }
2140
2141    #[test]
2142    fn test_root_node_reuse_with_overlay() {
2143        let storage = BTreeMap::from([
2144            (B256::right_padding_from(&[0x10]), U256::ONE),
2145            (B256::right_padding_from(&[0x20]), U256::from(2)),
2146        ]);
2147        let harness = ProofTestHarness::new(storage.clone());
2148        let hashed_address = harness.hashed_address();
2149        let post_state =
2150            HashedPostState::from_hashed_storage(hashed_address, HashedStorage::from_iter(storage))
2151                .into_sorted();
2152        let trie_updates = TrieUpdatesSorted::default();
2153        let trie_cursor = InMemoryTrieCursor::new_storage(
2154            NoopStorageTrieCursor::default(),
2155            &trie_updates,
2156            hashed_address,
2157        );
2158        let hashed_cursor = HashedPostStateCursor::new_storage(
2159            NoopHashedCursor::default(),
2160            &post_state,
2161            hashed_address,
2162        );
2163        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
2164
2165        let first_root = calculator.root_node(&mut StorageValueEncoder).unwrap();
2166        assert!(matches!(first_root.node, TrieNodeV2::Branch(_)));
2167        assert_eq!(
2168            calculator.compute_root_hash(core::slice::from_ref(&first_root)).unwrap(),
2169            Some(harness.original_root())
2170        );
2171
2172        let second_root = calculator.root_node(&mut StorageValueEncoder).unwrap();
2173        pretty_assertions::assert_eq!(first_root, second_root);
2174    }
2175
2176    #[test]
2177    fn test_partial_storage_proof_after_root_calculation() {
2178        let slot_a = B256::right_padding_from(&[0xae, 0xd4, 0x00]);
2179        let slot_b = B256::right_padding_from(&[0xae, 0xd4, 0x10]);
2180        let harness =
2181            ProofTestHarness::new(BTreeMap::from([(slot_a, U256::ONE), (slot_b, U256::from(2))]));
2182        let hashed_address = harness.hashed_address();
2183        let trie_cursor =
2184            harness.trie_cursor_factory().storage_trie_cursor(hashed_address).unwrap();
2185        let hashed_cursor =
2186            harness.hashed_cursor_factory().hashed_storage_cursor(hashed_address).unwrap();
2187        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
2188
2189        let root_node = calculator.storage_root_node(hashed_address).unwrap();
2190        assert_eq!(
2191            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap(),
2192            Some(harness.original_root())
2193        );
2194
2195        let target = ProofV2Target::new(slot_a).with_parent(ProofV2TargetParent::new(3));
2196        let mut actual_targets = [target];
2197        let actual = calculator.storage_proof(hashed_address, &mut actual_targets).unwrap();
2198        let mut expected_targets = [target];
2199        let (expected, root) = harness.proof_v2(&mut expected_targets);
2200
2201        assert!(root.is_none());
2202        pretty_assertions::assert_eq!(expected, actual);
2203    }
2204
2205    mod proptest_tests {
2206        use super::*;
2207        use proptest::prelude::*;
2208
2209        /// Generate a strategy for storage datasets (hashed slot → value).
2210        fn storage_strategy() -> impl Strategy<Value = BTreeMap<B256, U256>> {
2211            prop::collection::vec((any::<[u8; 32]>(), any::<u64>()), 0..=100).prop_map(|slots| {
2212                slots
2213                    .into_iter()
2214                    .map(|(slot_bytes, value)| (B256::from(slot_bytes), U256::from(value)))
2215                    .filter(|(_, v)| !v.is_zero())
2216                    .collect()
2217            })
2218        }
2219
2220        /// Generate a strategy for proof targets that are 80% from existing storage slots
2221        /// and 20% random keys. Each target has a random parent path length of `None` or 0..15.
2222        fn proof_targets_strategy(
2223            slot_keys: Vec<B256>,
2224        ) -> impl Strategy<Value = Vec<ProofV2Target>> {
2225            let num_slots = slot_keys.len();
2226
2227            let target_count = 0..=(num_slots + 5);
2228
2229            target_count.prop_flat_map(move |count| {
2230                let slot_keys = slot_keys.clone();
2231                prop::collection::vec(
2232                    (
2233                        prop::bool::weighted(0.8).prop_flat_map(move |from_slots| {
2234                            if from_slots && !slot_keys.is_empty() {
2235                                prop::sample::select(slot_keys.clone()).boxed()
2236                            } else {
2237                                any::<[u8; 32]>().prop_map(B256::from).boxed()
2238                            }
2239                        }),
2240                        0u8..16u8,
2241                    )
2242                        .prop_map(|(key, encoded_parent_path_len)| {
2243                            let parent = encoded_parent_path_len.checked_sub(1).map_or(
2244                                ProofV2TargetParent::NONE,
2245                                |parent_path_len| {
2246                                    ProofV2TargetParent::new(usize::from(parent_path_len))
2247                                },
2248                            );
2249                            ProofV2Target::new(key).with_parent(parent)
2250                        }),
2251                    count,
2252                )
2253            })
2254        }
2255
2256        fn dirty_key_strategy() -> impl Strategy<Value = B256> {
2257            // Vary both nibbles of each byte to cover adjacent branches and extensions.
2258            prop::array::uniform8(0u8..4).prop_map(|nibbles| {
2259                B256::right_padding_from(&Nibbles::from_nibbles(nibbles).pack())
2260            })
2261        }
2262
2263        proptest! {
2264            #![proptest_config(ProptestConfig::with_cases(4000))]
2265            #[test]
2266            /// Tests that `StorageProofCalculator` produces valid proofs for randomly generated
2267            /// storage datasets with proof targets.
2268            fn proptest_proof_with_targets(
2269                (storage, targets) in storage_strategy()
2270                    .prop_flat_map(|storage| {
2271                        let mut slot_keys: Vec<B256> = storage.keys().copied().collect();
2272                        slot_keys.sort_unstable();
2273                        let targets_strategy = proof_targets_strategy(slot_keys);
2274                        (Just(storage), targets_strategy)
2275                    })
2276            ) {
2277                reth_tracing::init_test_tracing();
2278                let harness = ProofTestHarness::new(storage);
2279
2280                harness.assert_proof(targets).expect("Proof generation failed");
2281            }
2282
2283            #[test]
2284            fn proptest_proof_with_dirty_prefixes(
2285                before in prop::collection::btree_map(dirty_key_strategy(), 1u64..100, 0..80),
2286                changes in prop::collection::btree_map(dirty_key_strategy(), 0u64..100, 0..8),
2287                retain_unchanged in any::<bool>(),
2288                target_indices in prop::collection::vec((0usize..100, 0usize..9), 0..10),
2289            ) {
2290                let before = before.into_iter()
2291                    .map(|(key, value)| (key, U256::from(value)))
2292                    .collect::<BTreeMap<_, _>>();
2293                let mut after = if retain_unchanged { before.clone() } else { BTreeMap::new() };
2294                for (key, value) in changes {
2295                    if value == 0 {
2296                        after.remove(&key);
2297                    } else {
2298                        after.insert(key, U256::from(value));
2299                    }
2300                }
2301                let prefix_set = PrefixSetMut::from(
2302                    before.keys().chain(after.keys())
2303                        .filter(|key| before.get(*key) != after.get(*key))
2304                        .map(Nibbles::unpack),
2305                ).freeze();
2306                let expected_root = crate::test_utils::storage_root_prehashed(
2307                    after.iter().map(|(key, value)| (*key, *value)),
2308                );
2309                let mut targets = vec![ProofV2Target::new(B256::ZERO)];
2310                let keys = before.keys().chain(after.keys()).copied().collect::<Vec<_>>();
2311                if !keys.is_empty() {
2312                    for (index, len) in target_indices {
2313                        targets.push(ProofV2Target::new(keys[index % keys.len()])
2314                            .with_parent(ProofV2TargetParent::new(len)));
2315                    }
2316                }
2317                let harness = TrieTestHarness::new(before);
2318                let hashed_factory = MockHashedCursorFactory::new(
2319                    BTreeMap::new(),
2320                    std::iter::once((harness.hashed_address(), after)).collect(),
2321                );
2322                let trie_cursor = harness.trie_cursor_factory()
2323                    .storage_trie_cursor(harness.hashed_address()).unwrap();
2324                let hashed_cursor = hashed_factory
2325                    .hashed_storage_cursor(harness.hashed_address()).unwrap();
2326                let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2327                    .with_prefix_set(prefix_set);
2328                let proof = calculator.storage_proof(harness.hashed_address(), &mut targets).unwrap();
2329
2330                prop_assert_eq!(calculator.compute_root_hash(&proof).unwrap(), Some(expected_root));
2331            }
2332        }
2333    }
2334
2335    #[test]
2336    fn test_exact_subtrie_targets_with_root_target() {
2337        reth_tracing::init_test_tracing();
2338
2339        let slot_80 = B256::right_padding_from(&[0x80]);
2340        let slot_82 = B256::right_padding_from(&[0x82]);
2341        let slot_f0 = B256::right_padding_from(&[0xf0]);
2342        let storage = BTreeMap::from([
2343            (slot_80, U256::ONE),
2344            (slot_82, U256::from(2)),
2345            (slot_f0, U256::from(3)),
2346        ]);
2347        let targets = [
2348            ProofV2Target::new(B256::ZERO),
2349            ProofV2Target::new(slot_80).with_parent(ProofV2TargetParent::new(1)),
2350        ];
2351
2352        let harness = ProofTestHarness::new(storage);
2353        harness.assert_proof(targets).expect("Proof generation failed");
2354    }
2355
2356    #[test]
2357    fn test_rebases_singleton_subtrie_root_below_known_parent() {
2358        let slot = B256::right_padding_from(&[0xae, 0xd4, 0x09]);
2359        let slot_nibbles = Nibbles::unpack(slot);
2360        let harness = ProofTestHarness::new(BTreeMap::from([(slot, U256::ONE)]));
2361        let mut targets = [ProofV2Target::new(slot).with_parent(ProofV2TargetParent::new(3))];
2362
2363        let (proof, root) = harness.proof_v2(&mut targets);
2364
2365        assert!(root.is_none());
2366        assert_eq!(proof.len(), 1);
2367        assert_eq!(proof[0].path, slot_nibbles.slice(0..4));
2368        let TrieNodeV2::Leaf(leaf) = &proof[0].node else {
2369            panic!("singleton subtrie root should remain a leaf")
2370        };
2371        assert_eq!(leaf.key, slot_nibbles.slice(4..));
2372    }
2373
2374    #[test]
2375    fn test_rebases_singleton_leaf_at_max_parent_depth() {
2376        let slot = B256::repeat_byte(0xae);
2377        let slot_nibbles = Nibbles::unpack(slot);
2378        let harness = ProofTestHarness::new(BTreeMap::from([(slot, U256::ONE)]));
2379        let mut targets = [ProofV2Target::new(slot).with_parent(ProofV2TargetParent::new(63))];
2380
2381        let (proof, root) = harness.proof_v2(&mut targets);
2382
2383        assert!(root.is_none());
2384        assert_eq!(proof.len(), 1);
2385        assert_eq!(proof[0].path, slot_nibbles);
2386        let TrieNodeV2::Leaf(leaf) = &proof[0].node else {
2387            panic!("singleton subtrie root should remain a leaf")
2388        };
2389        assert!(leaf.key.is_empty());
2390    }
2391
2392    #[test]
2393    fn test_root_and_root_parent_targets_retain_both_singleton_representations() {
2394        let slot = B256::right_padding_from(&[0x20]);
2395        let slot_nibbles = Nibbles::unpack(slot);
2396        let harness = ProofTestHarness::new(BTreeMap::from([(slot, U256::ONE)]));
2397        let mut targets = [
2398            ProofV2Target::new(slot),
2399            ProofV2Target::new(slot).with_parent(ProofV2TargetParent::new(0)),
2400        ];
2401
2402        let (proof, root) = harness.proof_v2(&mut targets);
2403
2404        assert_eq!(root, Some(harness.original_root()));
2405        let root_node = proof.iter().find(|node| node.path.is_empty()).expect("root proof");
2406        let TrieNodeV2::Leaf(root_leaf) = &root_node.node else { panic!("root should be a leaf") };
2407        assert_eq!(root_leaf.key, slot_nibbles);
2408
2409        let child_path = slot_nibbles.slice(0..1);
2410        let child_node =
2411            proof.iter().find(|node| node.path == child_path).expect("rebased root child proof");
2412        let TrieNodeV2::Leaf(child_leaf) = &child_node.node else {
2413            panic!("root child should be a leaf")
2414        };
2415        assert_eq!(child_leaf.key, slot_nibbles.slice(1..));
2416    }
2417
2418    #[test]
2419    fn test_exhausted_cursor_resets_at_equal_target_boundary() {
2420        let first = B256::ZERO;
2421        let last = B256::with_last_byte(1);
2422        let last_nibbles = Nibbles::unpack(last);
2423        let harness =
2424            ProofTestHarness::new(BTreeMap::from([(first, U256::ONE), (last, U256::from(2))]));
2425        let mut targets = [
2426            ProofV2Target::new(first),
2427            ProofV2Target::new(last).with_parent(ProofV2TargetParent::new(63)),
2428        ];
2429
2430        let (proof, root) = harness.proof_v2(&mut targets);
2431
2432        assert_eq!(root, Some(harness.original_root()));
2433        let child = proof
2434            .iter()
2435            .find(|node| node.path == last_nibbles)
2436            .expect("depth-63 target child proof");
2437        let TrieNodeV2::Leaf(leaf) = &child.node else { panic!("target child should be a leaf") };
2438        assert!(leaf.key.is_empty());
2439    }
2440
2441    #[test]
2442    fn test_rebases_compressed_branch_subtrie_root() {
2443        let slot_a = B256::right_padding_from(&[0xae, 0xd4, 0x00]);
2444        let slot_b = B256::right_padding_from(&[0xae, 0xd4, 0x10]);
2445        let slot_nibbles = Nibbles::unpack(slot_a);
2446        let harness =
2447            ProofTestHarness::new(BTreeMap::from([(slot_a, U256::ONE), (slot_b, U256::from(2))]));
2448        let mut targets = [ProofV2Target::new(slot_a).with_parent(ProofV2TargetParent::new(3))];
2449
2450        let (proof, root) = harness.proof_v2(&mut targets);
2451
2452        assert!(root.is_none());
2453        let branch_path = slot_nibbles.slice(0..4);
2454        let branch_node =
2455            proof.iter().find(|node| node.path == branch_path).expect("rebased compressed branch");
2456        let TrieNodeV2::Branch(branch) = &branch_node.node else {
2457            panic!("rebased node should be a branch")
2458        };
2459        assert!(branch.key.is_empty());
2460        assert!(branch.branch_rlp_node.is_none());
2461    }
2462
2463    #[test]
2464    fn test_discards_reconstructed_known_parent_branch() {
2465        let slot_a = B256::right_padding_from(&[0xae, 0xd2]);
2466        let slot_b = B256::right_padding_from(&[0xae, 0xd4]);
2467        let slot_nibbles = Nibbles::unpack(slot_a);
2468        let harness =
2469            ProofTestHarness::new(BTreeMap::from([(slot_a, U256::ONE), (slot_b, U256::from(2))]));
2470        let mut targets = [ProofV2Target::new(slot_a).with_parent(ProofV2TargetParent::new(3))];
2471
2472        let (proof, root) = harness.proof_v2(&mut targets);
2473
2474        assert!(root.is_none());
2475        assert!(!proof.iter().any(|node| node.path == slot_nibbles.slice(0..3)));
2476        assert!(proof.iter().any(|node| node.path == slot_nibbles.slice(0..4)));
2477    }
2478
2479    #[test]
2480    fn test_rebased_root_matches_direct_child_not_full_short_key() {
2481        let stored_slot = B256::right_padding_from(&[0xae, 0xd4, 0x09]);
2482        let same_child_target = B256::right_padding_from(&[0xae, 0xd4, 0xff]);
2483        let other_child_target = B256::right_padding_from(&[0xae, 0xd5]);
2484        let harness = ProofTestHarness::new(BTreeMap::from([(stored_slot, U256::ONE)]));
2485
2486        let mut same_child =
2487            [ProofV2Target::new(same_child_target).with_parent(ProofV2TargetParent::new(3))];
2488        let (proof, _) = harness.proof_v2(&mut same_child);
2489        assert_eq!(proof.len(), 1, "divergent leaf proves absence below the same child");
2490
2491        let mut other_child =
2492            [ProofV2Target::new(other_child_target).with_parent(ProofV2TargetParent::new(3))];
2493        let (proof, _) = harness.proof_v2(&mut other_child);
2494        assert!(proof.is_empty(), "a different direct child is unrelated to the target");
2495    }
2496
2497    #[test]
2498    fn test_known_parent_sibling_span_retains_only_target_children() {
2499        let stored_slot_a = B256::right_padding_from(&[0xea, 0x53]);
2500        let stored_slot_b = B256::right_padding_from(&[0xeb, 0x53]);
2501        let stored_slot_c = B256::right_padding_from(&[0xec, 0x53]);
2502        let target_a = B256::right_padding_from(&[0xea, 0x1f]);
2503        let target_c = B256::right_padding_from(&[0xec, 0x1f]);
2504        let harness = ProofTestHarness::new(BTreeMap::from([
2505            (stored_slot_a, U256::ONE),
2506            (stored_slot_b, U256::from(2)),
2507            (stored_slot_c, U256::from(3)),
2508        ]));
2509        let mut targets = [target_a, target_c]
2510            .map(|target| ProofV2Target::new(target).with_parent(ProofV2TargetParent::new(1)));
2511
2512        let (proof, root) = harness.proof_v2(&mut targets);
2513
2514        assert!(root.is_none());
2515        assert_eq!(
2516            proof.iter().map(|node| node.path).collect::<Vec<_>>(),
2517            [Nibbles::from_nibbles([0xe, 0xa]), Nibbles::from_nibbles([0xe, 0xc])]
2518        );
2519    }
2520
2521    #[test]
2522    fn test_known_parent_does_not_use_stale_parent_mask() {
2523        let stored_slot_a = B256::right_padding_from(&[0xea, 0x53]);
2524        let stored_slot = B256::right_padding_from(&[0xeb, 0x53]);
2525        let stored_slot_c = B256::right_padding_from(&[0xec, 0x53]);
2526        let target = B256::right_padding_from(&[0xeb, 0x1f]);
2527        let stored_slot_nibbles = Nibbles::unpack(stored_slot);
2528
2529        // The known parent at `e` is supplied by the sparse trie and may be stale in the database
2530        // when partial persistence masks that path. In particular, its state mask can omit the
2531        // live `eb` child while hashed state already contains that child's leaf.
2532        let stale_parent_mask = TrieMask::new((1 << 0xa) | (1 << 0xc));
2533        let stale_parent = BranchNodeCompact::new(
2534            stale_parent_mask,
2535            TrieMask::new(0),
2536            TrieMask::new(0),
2537            Vec::new(),
2538            None,
2539        );
2540        let storage_nodes = BTreeMap::from([(Nibbles::from_nibbles([0xe]), stale_parent)]);
2541
2542        let mut harness = TrieTestHarness::new(BTreeMap::from([
2543            (stored_slot_a, U256::ONE),
2544            (stored_slot, U256::from(2)),
2545            (stored_slot_c, U256::from(3)),
2546        ]));
2547        harness.set_trie_nodes(storage_nodes);
2548
2549        let mut targets = [ProofV2Target::new(target).with_parent(ProofV2TargetParent::new(1))];
2550        let (proof, root) = harness.proof_v2(&mut targets);
2551
2552        assert!(root.is_none());
2553        assert_eq!(proof.len(), 1);
2554        assert_eq!(proof[0].path, stored_slot_nibbles.slice(0..2));
2555        let TrieNodeV2::Leaf(leaf) = &proof[0].node else {
2556            panic!("live direct child should be reconstructed as a leaf")
2557        };
2558        assert_eq!(leaf.key, stored_slot_nibbles.slice(2..));
2559    }
2560
2561    #[test]
2562    fn test_empty_storage_respects_parent_context() {
2563        let harness = ProofTestHarness::new(BTreeMap::new());
2564        let slot = B256::ZERO;
2565
2566        let mut partial_target =
2567            [ProofV2Target::new(slot).with_parent(ProofV2TargetParent::new(0))];
2568        let (partial_proof, partial_root) = harness.proof_v2(&mut partial_target);
2569        assert!(partial_proof.is_empty());
2570        assert!(partial_root.is_none());
2571
2572        let mut root_target = [ProofV2Target::new(slot)];
2573        let (root_proof, root) = harness.proof_v2(&mut root_target);
2574        assert_eq!(root_proof.len(), 1);
2575        assert!(matches!(root_proof[0].node, TrieNodeV2::EmptyRoot));
2576        assert_eq!(root, Some(EMPTY_ROOT_HASH));
2577    }
2578
2579    #[test]
2580    fn test_big_trie() {
2581        use rand::prelude::*;
2582
2583        reth_tracing::init_test_tracing();
2584        let mut rng = rand::rngs::SmallRng::seed_from_u64(1);
2585
2586        let mut rand_b256 = || {
2587            let mut buf: [u8; 32] = [0; 32];
2588            rng.fill_bytes(&mut buf);
2589            B256::from_slice(&buf)
2590        };
2591
2592        // Generate random storage dataset.
2593        let mut storage = BTreeMap::new();
2594        for _ in 0..10240 {
2595            let hashed_slot = rand_b256();
2596            storage.insert(hashed_slot, U256::ONE);
2597        }
2598
2599        // Collect targets; partially from real keys, partially random keys which probably won't
2600        // exist.
2601        let mut targets = storage.keys().copied().collect::<Vec<_>>();
2602        for _ in 0..storage.len() / 5 {
2603            targets.push(rand_b256());
2604        }
2605        targets.sort();
2606
2607        // Create test harness
2608        let harness = ProofTestHarness::new(storage);
2609
2610        harness
2611            .assert_proof(targets.into_iter().map(ProofV2Target::new))
2612            .expect("Proof generation failed");
2613    }
2614
2615    #[test]
2616    fn test_node_with_masked_empty_child() {
2617        reth_tracing::init_test_tracing();
2618
2619        let val = U256::from(42u64);
2620
2621        // All storage keys share a common first nibble (0x6), so the branch is at path 0x6. The
2622        // second nibble differentiates children: 0,1,3,5,7.
2623        let slot_60 = B256::right_padding_from(&[0x60]);
2624        let slot_61 = B256::right_padding_from(&[0x61]);
2625        let slot_65 = B256::right_padding_from(&[0x65]);
2626        let slot_67 = B256::right_padding_from(&[0x67]);
2627
2628        // Construct a branch node at path 0x6 with state_mask bits 0,1,3,5,7.
2629        // hash_mask has bits 0,1,5,7 (NOT 3) — nibble 3's hash is cleared because it's in the
2630        // prefix set. Hashes are dummy values.
2631        let state_mask = TrieMask::new(0b10101011); // bits 0,1,3,5,7
2632        let hash_mask = TrieMask::new(0b10100011); // bits 0,1,5,7 (NOT 3)
2633        let hashes = vec![B256::repeat_byte(0xaa); hash_mask.count_ones() as usize];
2634        let branch = BranchNodeCompact::new(state_mask, TrieMask::new(0), hash_mask, hashes, None);
2635
2636        let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2637            std::iter::once((Nibbles::from_nibbles([0x6]), branch)).collect();
2638
2639        // Hashed cursor has slots at children 0, 1, 5, 7 — but NOT child 3 (0x63).
2640        // This simulates the post-state overlay having deleted the slot at 0x63.
2641        let mut harness = TrieTestHarness::new(
2642            [slot_60, slot_61, slot_65, slot_67].iter().map(|s| (*s, val)).collect(),
2643        );
2644        harness.set_trie_nodes(storage_nodes);
2645
2646        let storage_trie_cursor =
2647            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2648        let hashed_storage_cursor = harness
2649            .hashed_cursor_factory()
2650            .hashed_storage_cursor(harness.hashed_address())
2651            .unwrap();
2652        let mut calculator =
2653            StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
2654        let root_node = calculator
2655            .storage_root_node(harness.hashed_address())
2656            .expect("storage_root_node should succeed with masked empty child");
2657
2658        let root_hash = calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap();
2659        assert!(root_hash.is_some(), "should produce a root hash");
2660    }
2661
2662    /// Tests that `root_node` handles the case where `uncalculated_lower_bound` has advanced
2663    /// entirely past a cached branch that still has unprocessed children in its `state_mask`.
2664    ///
2665    /// Branch at `0x6` has `state_mask` bits 0,1,5,f where nibble 5 has its `hash_mask`
2666    /// cleared and no leaf data. The last child (nibble f)
2667    /// causes `calculate_key_range` to be called with range `(0x6f, Some(0x7))`. After that range,
2668    /// the hashed cursor still has keys (at `0x70...`), so `proof_subtrie` does not break and
2669    /// re-enters `next_uncached_key_range` with `uncalculated_lower_bound = Some(0x7)`.
2670    /// Since `0x7` is past `0x6`, all remaining children are skipped and the branch is popped.
2671    #[test]
2672    fn test_node_with_masked_empty_child_lower_bound_past_branch() {
2673        reth_tracing::init_test_tracing();
2674
2675        let val = U256::from(42u64);
2676
2677        // Leaf keys under 0x6 and one beyond (0x70) to keep the cursor alive after 0x6.
2678        let slot_60 = B256::right_padding_from(&[0x60]);
2679        let slot_61 = B256::right_padding_from(&[0x61]);
2680        let slot_6f = B256::right_padding_from(&[0x6f]);
2681        let slot_70 = B256::right_padding_from(&[0x70]);
2682
2683        // Branch at 0x6: state_mask bits 0,1,5,f; hash_mask bits 0,1 (NOT 5, NOT f).
2684        // Nibble 5 has state_mask set but no hash and no leaf data (masked empty child).
2685        // Nibble f has state_mask set, no hash, but DOES have leaf data.
2686        let state_mask = TrieMask::new(0b1000_0000_0010_0011); // bits 0,1,5,f
2687        let hash_mask = TrieMask::new(0b0000_0000_0000_0011); // bits 0,1
2688        let hashes = vec![B256::repeat_byte(0xaa); hash_mask.count_ones() as usize];
2689        let branch = BranchNodeCompact::new(state_mask, TrieMask::new(0), hash_mask, hashes, None);
2690
2691        let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
2692            std::iter::once((Nibbles::from_nibbles([0x6]), branch)).collect();
2693
2694        // Hashed cursor: slots at 0x60, 0x61, 0x6f, 0x70 — but NOT 0x65.
2695        let mut harness = TrieTestHarness::new(
2696            [slot_60, slot_61, slot_6f, slot_70].iter().map(|s| (*s, val)).collect(),
2697        );
2698        harness.set_trie_nodes(storage_nodes);
2699
2700        let storage_trie_cursor =
2701            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2702        let hashed_storage_cursor = harness
2703            .hashed_cursor_factory()
2704            .hashed_storage_cursor(harness.hashed_address())
2705            .unwrap();
2706        let mut calculator =
2707            StorageProofCalculator::new_storage(storage_trie_cursor, hashed_storage_cursor);
2708        let root_node = calculator
2709            .storage_root_node(harness.hashed_address())
2710            .expect("storage_root_node should succeed when lower bound advances past branch");
2711
2712        let root_hash = calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap();
2713        assert!(root_hash.is_some(), "should produce a root hash");
2714    }
2715
2716    /// Tests that the prefix set causes `next_uncached_key_range` to add child nibbles that are
2717    /// not present in the cached branch's `state_mask`.
2718    ///
2719    /// Setup: An original state with leaves at `0x60` and `0x61` produces a cached branch at
2720    /// `0x6` with children at nibbles 0 and 1 (both with real cached hashes from `StorageRoot`).
2721    /// A new leaf is then inserted at `0x63...`, which is NOT in the branch's `state_mask`.
2722    /// The prefix set contains the new key. Without prefix set support, the calculator would
2723    /// skip nibble 3 entirely and produce a stale root hash. With prefix set support, nibble 3
2724    /// is discovered and its subtrie is recalculated from leaves.
2725    #[test]
2726    fn test_prefix_set_adds_child_nibbles() {
2727        reth_tracing::init_test_tracing();
2728
2729        let val = U256::from(42u64);
2730        let slot_60 = B256::right_padding_from(&[0x60]);
2731        let slot_61 = B256::right_padding_from(&[0x61]);
2732        let slot_63 = B256::right_padding_from(&[0x63]);
2733
2734        let harness = TrieTestHarness::new([(slot_60, val), (slot_61, val)].into_iter().collect());
2735
2736        let changeset: BTreeMap<B256, U256> = std::iter::once((slot_63, val)).collect();
2737        let (expected_root, _) = harness.get_root_with_updates(&changeset);
2738
2739        let mut updated_storage = harness.storage().clone();
2740        updated_storage.insert(slot_63, val);
2741
2742        let updated_hashed = MockHashedCursorFactory::new(
2743            BTreeMap::new(),
2744            std::iter::once((harness.hashed_address(), updated_storage)).collect(),
2745        );
2746
2747        let mut prefix_set = PrefixSetMut::default();
2748        prefix_set.insert(Nibbles::unpack(slot_63));
2749
2750        let trie_cursor =
2751            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2752        let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
2753        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2754            .with_prefix_set(prefix_set.freeze());
2755        let root_node = calculator
2756            .storage_root_node(harness.hashed_address())
2757            .expect("storage_root_node should succeed with prefix set adding child nibbles");
2758        let got_root =
2759            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2760
2761        pretty_assertions::assert_eq!(
2762            expected_root,
2763            got_root,
2764            "Root hash with prefix set should match fresh computation"
2765        );
2766    }
2767
2768    /// Tests that `next_uncached_key_range` does not use a cached hash when the child's path
2769    /// is in the prefix set, forcing recalculation from leaves.
2770    ///
2771    /// Setup: A cached branch at `0x6` with children at nibbles 0,1,5 — all with cached hashes.
2772    /// The leaf at `0x65...` is changed (different value). The prefix set marks `0x65...` as
2773    /// dirty. Without prefix set support, the calculator would use the stale cached hash for
2774    /// nibble 5 and produce a wrong root. With prefix set support, the cached hash is skipped
2775    /// and the subtrie is recalculated from the updated leaf data.
2776    #[test]
2777    fn test_prefix_set_invalidates_cached_hash() {
2778        reth_tracing::init_test_tracing();
2779
2780        let original_val = U256::from(42u64);
2781        let updated_val = U256::from(9999u64);
2782        let slot_60 = B256::right_padding_from(&[0x60]);
2783        let slot_61 = B256::right_padding_from(&[0x61]);
2784        let slot_65 = B256::right_padding_from(&[0x65]);
2785
2786        let harness = TrieTestHarness::new(
2787            [(slot_60, original_val), (slot_61, original_val), (slot_65, original_val)]
2788                .into_iter()
2789                .collect(),
2790        );
2791
2792        let changeset: BTreeMap<B256, U256> = std::iter::once((slot_65, updated_val)).collect();
2793        let (expected_root, _) = harness.get_root_with_updates(&changeset);
2794
2795        let mut updated_storage = harness.storage().clone();
2796        updated_storage.insert(slot_65, updated_val);
2797
2798        let updated_hashed = MockHashedCursorFactory::new(
2799            BTreeMap::new(),
2800            std::iter::once((harness.hashed_address(), updated_storage)).collect(),
2801        );
2802
2803        let mut prefix_set = PrefixSetMut::default();
2804        prefix_set.insert(Nibbles::unpack(slot_65));
2805
2806        let trie_cursor =
2807            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2808        let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
2809        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2810            .with_prefix_set(prefix_set.freeze());
2811        let root_node = calculator
2812            .storage_root_node(harness.hashed_address())
2813            .expect("storage_root_node should succeed with prefix set invalidating cached hash");
2814        let got_root =
2815            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
2816
2817        pretty_assertions::assert_eq!(
2818            expected_root,
2819            got_root,
2820            "Root hash with prefix set invalidation should match fresh computation"
2821        );
2822    }
2823
2824    fn b256(s: &str) -> B256 {
2825        B256::from_slice(&alloy_primitives::hex::decode(s).expect("valid hex string"))
2826    }
2827
2828    #[test]
2829    fn test_prefix_set_root_proof_processes_sibling_after_cached_descendant() {
2830        reth_tracing::init_test_tracing();
2831
2832        let storage = [
2833            ("1022c69e9d900e40775cd387c134899f465f291dbc3c97899ff6bfb8dc972b37", 45u64),
2834            ("1111ad8083c8a3a398b2b781217b989ff4d1ed182f46cc765eda49a7b316139d", 60),
2835            ("12012d20943649899b2fc0f87b9840b70ef68e93613aac17c269bf8c5a78a712", 17),
2836            ("12014b57b9a162c03d072eb6acd4e936f1c4bc23b803a054347c5ee9a9bcfb9a", 49),
2837            ("1203f800840af3f898ab4572f2750106a7c4bd2b3e844b6e7fa72704673cc2c6", 76),
2838            ("12208f18fbcd6971c92808721392acbf11d5af58e9143a374cc86e70bdd1f097", 10),
2839        ]
2840        .into_iter()
2841        .map(|(key, value)| (b256(key), U256::from(value)))
2842        .collect();
2843
2844        let dirty = b256("12208f18fbcd6971c92808721392acbf11d5af58e9143a374cc86e70bdd1f097");
2845        let harness = ProofTestHarness::new(storage);
2846        let expected_root = harness.original_root();
2847
2848        let mut prefix_set = PrefixSetMut::default();
2849        prefix_set.insert(Nibbles::unpack(dirty));
2850
2851        pretty_assertions::assert_eq!(
2852            Some(expected_root),
2853            harness.root_with_prefix_set(prefix_set.freeze()),
2854            "root proof must process a prefix-set sibling after a cached descendant",
2855        );
2856    }
2857
2858    #[test]
2859    fn test_prefix_set_root_proof_processes_trailing_dirty_sibling() {
2860        reth_tracing::init_test_tracing();
2861
2862        let keys = [
2863            "0022001020000000000000000000000000000000000000000000000000000000",
2864            "0110212112000000000000000000000000000000000000000000000000000000",
2865            "0202210210000000000000000000000000000000000000000000000000000000",
2866            "0211020211000000000000000000000000000000000000000000000000000000",
2867            "0211211002000000000000000000000000000000000000000000000000000000",
2868            "0212221010000000000000000000000000000000000000000000000000000000",
2869            "0222011102000000000000000000000000000000000000000000000000000000",
2870        ];
2871        let storage =
2872            keys.iter().enumerate().map(|(i, key)| (b256(key), U256::from(i as u64 + 1))).collect();
2873        let harness = ProofTestHarness::new(storage);
2874        let expected_root = harness.original_root();
2875
2876        // The dirty children straddle a clean cached descendant under branch 0x02. Traversal
2877        // must resume at the trailing dirty sibling after using the cached descendant.
2878        let mut prefix_set = PrefixSetMut::default();
2879        prefix_set.insert(Nibbles::unpack(b256(keys[2])));
2880        prefix_set.insert(Nibbles::unpack(b256(keys[6])));
2881
2882        pretty_assertions::assert_eq!(
2883            Some(expected_root),
2884            harness.root_with_prefix_set(prefix_set.freeze()),
2885        );
2886    }
2887
2888    #[test]
2889    fn test_prefix_set_preserves_cached_siblings() {
2890        let storage = (0..4096u16)
2891            .map(|key| (B256::right_padding_from(&key.to_be_bytes()), U256::from(1)))
2892            .collect();
2893        let harness = TrieTestHarness::new(storage);
2894        let changes = BTreeMap::from([
2895            (B256::right_padding_from(&[0x04, 0x00]), U256::from(2)),
2896            (B256::right_padding_from(&[0x04, 0xf0]), U256::from(2)),
2897        ]);
2898        let (expected_root, _) = harness.get_root_with_updates(&changes);
2899        let mut updated_storage = harness.storage().clone();
2900        updated_storage.extend(changes.clone());
2901        let hashed_factory = MockHashedCursorFactory::new(
2902            BTreeMap::new(),
2903            std::iter::once((harness.hashed_address(), updated_storage)).collect(),
2904        );
2905        let prefix_set = PrefixSetMut::from(changes.keys().map(Nibbles::unpack)).freeze();
2906        let trie_cursor =
2907            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2908        let hashed_cursor = hashed_factory.hashed_storage_cursor(harness.hashed_address()).unwrap();
2909        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2910            .with_prefix_set(prefix_set);
2911        let mut targets = [ProofV2Target::new(B256::ZERO)];
2912        let proof = calculator.storage_proof(harness.hashed_address(), &mut targets).unwrap();
2913
2914        assert_eq!(calculator.compute_root_hash(&proof).unwrap(), Some(expected_root));
2915
2916        // Returning from 0x040 to cached parent 0x04 must not scan all leaves up to 0x05.
2917        let clean_key = B256::right_padding_from(&[0x04, 0x88]);
2918        assert!(!hashed_factory
2919            .visited_storage_keys(harness.hashed_address())
2920            .iter()
2921            .any(|visit| visit.visited_key == Some(clean_key)));
2922    }
2923
2924    #[test]
2925    fn test_prefix_set_finishes_branch_before_returning_to_ancestor() {
2926        let storage =
2927            [[0x10, 0x00, 0x00], [0x12, 0x10, 0x00], [0x12, 0x10, 0x20], [0x12, 0x11, 0x00]]
2928                .into_iter()
2929                .map(|key| (B256::right_padding_from(&key), U256::from(1)))
2930                .collect();
2931        let harness = TrieTestHarness::new(storage);
2932        let changes = BTreeMap::from([
2933            (B256::right_padding_from(&[0x12, 0x00]), U256::from(1)),
2934            (B256::right_padding_from(&[0x12, 0x20]), U256::from(1)),
2935        ]);
2936        let mut after = harness.storage().clone();
2937        after.extend(changes.clone());
2938        let expected_root = crate::test_utils::storage_root_prehashed(
2939            after.iter().map(|(key, value)| (*key, *value)),
2940        );
2941        let hashed_factory = MockHashedCursorFactory::new(
2942            BTreeMap::new(),
2943            std::iter::once((harness.hashed_address(), after)).collect(),
2944        );
2945        let trie_cursor =
2946            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
2947        let hashed_cursor = hashed_factory.hashed_storage_cursor(harness.hashed_address()).unwrap();
2948        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
2949            .with_prefix_set(PrefixSetMut::from(changes.keys().map(Nibbles::unpack)).freeze());
2950        let mut targets = [
2951            ProofV2Target::new(B256::ZERO),
2952            ProofV2Target::new(B256::right_padding_from(&[0x10]))
2953                .with_parent(ProofV2TargetParent::new(0)),
2954        ];
2955
2956        // Returning to cached ancestor 0x1 must finish dirty sibling 0x122 before encoding 0x12.
2957        let proof = calculator.storage_proof(harness.hashed_address(), &mut targets).unwrap();
2958        assert_eq!(calculator.compute_root_hash(&proof).unwrap(), Some(expected_root));
2959    }
2960
2961    /// Helper to compute the keccak256 hash of a storage leaf node. The `short_key` is the
2962    /// leaf's key after trimming all branch/extension nibbles consumed by ancestor nodes.
2963    fn storage_leaf_hash(short_key: &Nibbles, value: &U256) -> B256 {
2964        let mut buf = Vec::new();
2965        alloy_trie::nodes::LeafNodeRef::new(short_key, &alloy_rlp::encode_fixed_size(value))
2966            .encode(&mut buf);
2967        keccak256(&buf)
2968    }
2969
2970    /// Checks collapsing a cached branch after removing one of its two direct branch children.
2971    fn assert_branch_collapse(remaining_nibble: u8, removed_nibble: u8) {
2972        reth_tracing::init_test_tracing();
2973
2974        let val = U256::ONE;
2975        let child_keys = |nibble| {
2976            [
2977                B256::right_padding_from(&[0x20 | nibble, 0x00]),
2978                B256::right_padding_from(&[0x20 | nibble, 0x10]),
2979            ]
2980        };
2981        let [remaining_a, remaining_b] = child_keys(remaining_nibble);
2982        let [removed_a, removed_b] = child_keys(removed_nibble);
2983
2984        // Build the cached trie through the production HashBuilder path. Each child at
2985        // `remaining_nibble` and `removed_nibble` is a direct branch with two leaf children.
2986        let initial_storage = BTreeMap::from([
2987            (remaining_a, val),
2988            (remaining_b, val),
2989            (removed_a, val),
2990            (removed_b, val),
2991        ]);
2992        let harness = TrieTestHarness::new(initial_storage);
2993
2994        let cached_branch = &harness
2995            .storage_trie_updates()
2996            .storage_nodes
2997            .get(&Nibbles::from_nibbles([0x2]))
2998            .expect("branch at 0x2");
2999        let child_mask =
3000            TrieMask::from_nibble(remaining_nibble) | TrieMask::from_nibble(removed_nibble);
3001        assert_eq!(cached_branch.state_mask, child_mask);
3002        assert_eq!(cached_branch.hash_mask, child_mask);
3003        assert!(cached_branch.tree_mask.is_empty());
3004
3005        let final_storage = BTreeMap::from([(remaining_a, val), (remaining_b, val)]);
3006        let expected_root = TrieTestHarness::new(final_storage.clone()).original_root();
3007        let updated_hashed = MockHashedCursorFactory::new(
3008            BTreeMap::new(),
3009            std::iter::once((harness.hashed_address(), final_storage)).collect(),
3010        );
3011
3012        // The prefix set marks the entire removed child subtree dirty.
3013        let mut prefix_set = PrefixSetMut::default();
3014        prefix_set.insert(Nibbles::unpack(removed_a));
3015        prefix_set.insert(Nibbles::unpack(removed_b));
3016
3017        let trie_cursor =
3018            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3019        let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
3020        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3021            .with_prefix_set(prefix_set.freeze());
3022        let root_node = calculator
3023            .storage_root_node(harness.hashed_address())
3024            .expect("storage_root_node should succeed after branch collapse");
3025        let root =
3026            calculator.compute_root_hash(core::slice::from_ref(&root_node)).unwrap().unwrap();
3027
3028        pretty_assertions::assert_eq!(expected_root, root);
3029    }
3030
3031    #[test]
3032    fn test_branch_collapse_removed_child_before_remaining() {
3033        assert_branch_collapse(1, 0);
3034    }
3035
3036    #[test]
3037    fn test_branch_collapse_removed_child_after_remaining() {
3038        assert_branch_collapse(4, 9);
3039    }
3040
3041    #[test]
3042    fn test_prefix_set_root_proof_preserves_clean_sibling_after_cached_branch_collapse() {
3043        reth_tracing::init_test_tracing();
3044
3045        let dirty = B256::right_padding_from(&[0x10, 0x00, 0x10]);
3046        let clean_sibling = B256::right_padding_from(&[0x10, 0x10]);
3047        let storage = [
3048            B256::right_padding_from(&[0x10]),
3049            dirty,
3050            B256::right_padding_from(&[0x10, 0x01]),
3051            B256::right_padding_from(&[0x10, 0x02]),
3052            clean_sibling,
3053            B256::right_padding_from(&[0x11]),
3054            B256::right_padding_from(&[0x12]),
3055        ]
3056        .into_iter()
3057        .map(|key| (key, U256::ONE))
3058        .collect();
3059
3060        let harness = ProofTestHarness::new(storage);
3061        let expected_root = harness.original_root();
3062
3063        let mut prefix_set = PrefixSetMut::default();
3064        prefix_set.insert(Nibbles::unpack(dirty));
3065
3066        let mut prefix_set_with_sibling = PrefixSetMut::default();
3067        prefix_set_with_sibling.insert(Nibbles::unpack(dirty));
3068        prefix_set_with_sibling.insert(Nibbles::unpack(clean_sibling));
3069
3070        pretty_assertions::assert_eq!(
3071            Some(expected_root),
3072            harness.root_with_prefix_set(prefix_set_with_sibling.freeze()),
3073        );
3074        pretty_assertions::assert_eq!(
3075            Some(expected_root),
3076            harness.root_with_prefix_set(prefix_set.freeze()),
3077            "a dirty prefix must not omit a clean sibling after collapsing a cached branch",
3078        );
3079    }
3080
3081    #[test]
3082    fn test_prefix_set_range_skips_covered_cached_branch() {
3083        reth_tracing::init_test_tracing();
3084
3085        let before = B256::right_padding_from(&[0x30]);
3086        let cached_a = B256::right_padding_from(&[0x80, 0x10]);
3087        let cached_b = B256::right_padding_from(&[0x80, 0x15]);
3088        let dirty = B256::right_padding_from(&[0x80, 0xc0]);
3089        let after = B256::right_padding_from(&[0x90]);
3090
3091        // The cached branch at 0x80 remains parked while its children are rebuilt. Processing the
3092        // dirty child advances the lower bound to 0x9, past the cached branch's entire range.
3093        let storage = [before, cached_a, cached_b, dirty, after]
3094            .into_iter()
3095            .enumerate()
3096            .map(|(i, key)| (key, U256::from(i + 1)))
3097            .collect();
3098
3099        let harness = ProofTestHarness::new(storage);
3100        let expected_root = harness.original_root();
3101        let mut prefix_set = PrefixSetMut::default();
3102        prefix_set.insert(Nibbles::unpack(dirty));
3103
3104        pretty_assertions::assert_eq!(
3105            Some(expected_root),
3106            harness.root_with_prefix_set(prefix_set.freeze()),
3107        );
3108    }
3109
3110    #[test]
3111    fn test_cached_branch_extension_skips_diverging_target() {
3112        reth_tracing::init_test_tracing();
3113
3114        let val = U256::from(100u64);
3115
3116        // Keys whose first bytes directly set the nibble paths we need.
3117        let key_a0 = B256::right_padding_from(&[0x6a, 0x30]); // nibbles: 6,a,3,0,...
3118        let key_a1 = B256::right_padding_from(&[0x6a, 0x31]); // nibbles: 6,a,3,1,...
3119        let key_c = B256::right_padding_from(&[0x6a, 0x80]); // nibbles: 6,a,8,0,...
3120        let key_d = B256::right_padding_from(&[0x6b, 0x00]); // nibbles: 6,b,0,0,...
3121        let key_e = B256::right_padding_from(&[0x6c, 0x00]); // nibbles: 6,c,0,0,...
3122
3123        // Build a correct trie from all five leaves to get the expected root and real hashes.
3124        let all_storage: BTreeMap<B256, U256> =
3125            [(key_a0, val), (key_a1, val), (key_c, val), (key_d, val), (key_e, val)]
3126                .into_iter()
3127                .collect();
3128        let correct_harness = TrieTestHarness::new(all_storage.clone());
3129        let expected_root = correct_harness.original_root();
3130
3131        // Compute leaf hashes for constructing manual cached branch nodes.
3132        let leaf_hash_a0 = storage_leaf_hash(&Nibbles::unpack(key_a0).slice(4..), &val);
3133        let leaf_hash_a1 = storage_leaf_hash(&Nibbles::unpack(key_a1).slice(4..), &val);
3134        let leaf_hash_d = storage_leaf_hash(&Nibbles::unpack(key_d).slice(2..), &val);
3135        let leaf_hash_e = storage_leaf_hash(&Nibbles::unpack(key_e).slice(2..), &val);
3136
3137        // ── Construct cached branch at [6] ─────────────────────────────────────
3138        // state_mask: bits a, b, and c set.
3139        // hash_mask:  bits b and c — both have cached leaf hashes.  Bit a has no hash, so the
3140        //             calculator will seek the trie cursor to find a deeper cached branch.
3141        //
3142        // Children b and c remain clean and can still use their cached hashes.
3143        let branch_6_state_mask = TrieMask::new((1 << 0xa) | (1 << 0xb) | (1 << 0xc));
3144        let branch_6_hash_mask = TrieMask::new((1 << 0xb) | (1 << 0xc));
3145        let branch_6 = BranchNodeCompact::new(
3146            branch_6_state_mask,
3147            TrieMask::new(0),
3148            branch_6_hash_mask,
3149            vec![leaf_hash_d, leaf_hash_e],
3150            None,
3151        );
3152
3153        // ── Construct cached branch at [6,a,3] ────────────────────────────────
3154        // state_mask: bits 0 and 1 set (children key_a0 and key_a1).
3155        // hash_mask:  both bits set — both children have cached hashes.
3156        let branch_6a3_state_mask = TrieMask::new((1 << 0) | (1 << 1));
3157        let branch_6a3 = BranchNodeCompact::new(
3158            branch_6a3_state_mask,
3159            TrieMask::new(0),
3160            branch_6a3_state_mask,
3161            vec![leaf_hash_a0, leaf_hash_a1],
3162            None,
3163        );
3164
3165        // Intentionally omit the branch at [6,a] — this is the inconsistency.
3166        let inconsistent_nodes: BTreeMap<Nibbles, BranchNodeCompact> = [
3167            (Nibbles::from_nibbles([0x6]), branch_6),
3168            (Nibbles::from_nibbles([0x6, 0xa, 0x3]), branch_6a3),
3169        ]
3170        .into_iter()
3171        .collect();
3172
3173        // Create harness with all five leaves but the inconsistent trie nodes.
3174        let mut harness = TrieTestHarness::new(all_storage);
3175        harness.set_trie_nodes(inconsistent_nodes);
3176
3177        // Mark key_c as dirty — in the real scenario the leaf was touched by execution. The
3178        // nested branch [6,a,3] remains clean because key_c diverges at [6,a,8].
3179        let mut prefix_set = PrefixSetMut::default();
3180        prefix_set.insert(Nibbles::unpack(key_c));
3181
3182        // ── Verify root hash ───────────────────────────────────────────────────
3183        let trie_cursor =
3184            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3185        let hashed_cursor = harness
3186            .hashed_cursor_factory()
3187            .hashed_storage_cursor(harness.hashed_address())
3188            .unwrap();
3189        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3190            .with_prefix_set(prefix_set.freeze());
3191
3192        let root_node = calculator
3193            .storage_root_node(harness.hashed_address())
3194            .expect("storage_root_node should succeed");
3195        let got_root = calculator
3196            .compute_root_hash(core::slice::from_ref(&root_node))
3197            .unwrap()
3198            .expect("should produce a root hash");
3199
3200        // With the bug, the calculator skips key_c and produces a wrong root.
3201        pretty_assertions::assert_eq!(
3202            expected_root,
3203            got_root,
3204            "Root hash should match correct trie; cached extension must not skip diverging leaves"
3205        );
3206
3207        // ── Verify proof for key_c contains nodes on its path ──────────────────
3208        let mut targets = vec![ProofV2Target::new(key_c)];
3209        let proofs = calculator
3210            .storage_proof(harness.hashed_address(), &mut targets)
3211            .expect("storage_proof should succeed");
3212
3213        let key_c_nibbles = Nibbles::unpack(key_c);
3214        let has_matching_node = proofs.iter().any(|node| key_c_nibbles.starts_with(&node.path));
3215        assert!(
3216            has_matching_node,
3217            "Proof for key_c should contain at least one node on key_c's path, got: {proofs:?}"
3218        );
3219    }
3220
3221    #[test]
3222    fn test_cached_branch_extension_skips_diverging_target_before() {
3223        reth_tracing::init_test_tracing();
3224
3225        let val = U256::from(100u64);
3226
3227        // Keys whose first bytes directly set the nibble paths we need.
3228        let key_a0 = B256::right_padding_from(&[0x6a, 0x80]); // nibbles: 6,a,8,0,...
3229        let key_a1 = B256::right_padding_from(&[0x6a, 0x81]); // nibbles: 6,a,8,1,...
3230        let key_c = B256::right_padding_from(&[0x6a, 0x30]); // nibbles: 6,a,3,0,... (BEFORE
3231                                                             // [6,a,8])
3232        let key_d = B256::right_padding_from(&[0x6b, 0x00]); // nibbles: 6,b,0,0,...
3233        let key_e = B256::right_padding_from(&[0x6c, 0x00]); // nibbles: 6,c,0,0,...
3234
3235        // Build a correct trie from all five leaves to get the expected root and real hashes.
3236        let all_storage: BTreeMap<B256, U256> =
3237            [(key_a0, val), (key_a1, val), (key_c, val), (key_d, val), (key_e, val)]
3238                .into_iter()
3239                .collect();
3240        let correct_harness = TrieTestHarness::new(all_storage.clone());
3241        let expected_root = correct_harness.original_root();
3242
3243        // Compute leaf hashes for constructing manual cached branch nodes.
3244        let leaf_hash_a0 = storage_leaf_hash(&Nibbles::unpack(key_a0).slice(4..), &val);
3245        let leaf_hash_a1 = storage_leaf_hash(&Nibbles::unpack(key_a1).slice(4..), &val);
3246        let leaf_hash_d = storage_leaf_hash(&Nibbles::unpack(key_d).slice(2..), &val);
3247        let leaf_hash_e = storage_leaf_hash(&Nibbles::unpack(key_e).slice(2..), &val);
3248
3249        // ── Construct cached branch at [6] ─────────────────────────────────────
3250        // state_mask: bits a, b, and c set.
3251        // hash_mask:  bits b and c — both have cached leaf hashes.  Bit a has no hash, so the
3252        //             calculator will seek the trie cursor to find a deeper cached branch.
3253        //
3254        // Children b and c remain clean and can still use their cached hashes.
3255        let branch_6_state_mask = TrieMask::new((1 << 0xa) | (1 << 0xb) | (1 << 0xc));
3256        let branch_6_hash_mask = TrieMask::new((1 << 0xb) | (1 << 0xc));
3257        let branch_6 = BranchNodeCompact::new(
3258            branch_6_state_mask,
3259            TrieMask::new(0),
3260            branch_6_hash_mask,
3261            vec![leaf_hash_d, leaf_hash_e],
3262            None,
3263        );
3264
3265        // ── Construct cached branch at [6,a,8] ────────────────────────────────
3266        // state_mask: bits 0 and 1 set (children key_a0 and key_a1).
3267        // hash_mask:  both bits set — both children have cached hashes.
3268        let branch_6a8_state_mask = TrieMask::new((1 << 0) | (1 << 1));
3269        let branch_6a8 = BranchNodeCompact::new(
3270            branch_6a8_state_mask,
3271            TrieMask::new(0),
3272            branch_6a8_state_mask,
3273            vec![leaf_hash_a0, leaf_hash_a1],
3274            None,
3275        );
3276
3277        // Intentionally omit the branch at [6,a] — this is the inconsistency.
3278        let inconsistent_nodes: BTreeMap<Nibbles, BranchNodeCompact> = [
3279            (Nibbles::from_nibbles([0x6]), branch_6),
3280            (Nibbles::from_nibbles([0x6, 0xa, 0x8]), branch_6a8),
3281        ]
3282        .into_iter()
3283        .collect();
3284
3285        // Create harness with all five leaves but the inconsistent trie nodes.
3286        let mut harness = TrieTestHarness::new(all_storage);
3287        harness.set_trie_nodes(inconsistent_nodes);
3288
3289        // Mark key_c as dirty — it comes BEFORE the cached branch [6,a,8] in nibble order.
3290        let mut prefix_set = PrefixSetMut::default();
3291        prefix_set.insert(Nibbles::unpack(key_c));
3292
3293        // ── Verify root hash ───────────────────────────────────────────────────
3294        let trie_cursor =
3295            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3296        let hashed_cursor = harness
3297            .hashed_cursor_factory()
3298            .hashed_storage_cursor(harness.hashed_address())
3299            .unwrap();
3300        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3301            .with_prefix_set(prefix_set.freeze());
3302
3303        let root_node = calculator
3304            .storage_root_node(harness.hashed_address())
3305            .expect("storage_root_node should succeed");
3306        let got_root = calculator
3307            .compute_root_hash(core::slice::from_ref(&root_node))
3308            .unwrap()
3309            .expect("should produce a root hash");
3310
3311        // With the bug, the calculator skips key_c and produces a wrong root.
3312        pretty_assertions::assert_eq!(
3313            expected_root,
3314            got_root,
3315            "Root hash should match correct trie; cached extension must not skip diverging leaves before cached branch"
3316        );
3317
3318        // ── Verify proof for key_c contains nodes on its path ──────────────────
3319        let mut targets = vec![ProofV2Target::new(key_c)];
3320        let proofs = calculator
3321            .storage_proof(harness.hashed_address(), &mut targets)
3322            .expect("storage_proof should succeed");
3323
3324        let key_c_nibbles = Nibbles::unpack(key_c);
3325        let has_matching_node = proofs.iter().any(|node| key_c_nibbles.starts_with(&node.path));
3326        assert!(
3327            has_matching_node,
3328            "Proof for key_c should contain at least one node on key_c's path, got: {proofs:?}"
3329        );
3330    }
3331
3332    #[test]
3333    fn test_skipped_parent_branch_with_unskipped_child() {
3334        reth_tracing::init_test_tracing();
3335
3336        let val = U256::ONE;
3337        let updated_val = U256::from(2u64);
3338
3339        // We need cached branches at [2], [2,f], and [3] in the trie DB.
3340        let key_2 = B256::right_padding_from(&[0x20]);
3341        let key_2f00 = B256::right_padding_from(&[0x2f, 0x00]);
3342        let key_2f01 = B256::right_padding_from(&[0x2f, 0x01]);
3343        let key_2f10 = B256::right_padding_from(&[0x2f, 0x10]);
3344        let key_2f11 = B256::right_padding_from(&[0x2f, 0x11]);
3345        let key_300 = B256::right_padding_from(&[0x30, 0x00]);
3346        let key_301 = B256::right_padding_from(&[0x30, 0x10]);
3347        let key_310 = B256::right_padding_from(&[0x31, 0x00]);
3348        let key_311 = B256::right_padding_from(&[0x31, 0x10]);
3349        let key_500 = B256::right_padding_from(&[0x50, 0x00]);
3350        let key_501 = B256::right_padding_from(&[0x50, 0x10]);
3351        let key_510 = B256::right_padding_from(&[0x51, 0x00]);
3352        let key_511 = B256::right_padding_from(&[0x51, 0x10]);
3353
3354        let all_keys = [
3355            key_2, key_2f00, key_2f01, key_2f10, key_2f11, key_300, key_301, key_310, key_311,
3356            key_500, key_501, key_510, key_511,
3357        ];
3358
3359        let original_storage: BTreeMap<B256, U256> = all_keys.iter().map(|k| (*k, val)).collect();
3360        let harness = TrieTestHarness::new(original_storage);
3361
3362        // Verify that the expected branches exist in the trie.
3363        let trie_updates = harness.storage_trie_updates();
3364        assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x2])));
3365        assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x2, 0xf])));
3366        assert!(trie_updates.storage_nodes.contains_key(&Nibbles::from_nibbles([0x3])));
3367
3368        // Change only key_2 — triggers skip of parent branch [2] while child [2,f] is not
3369        // skipped.
3370        let changeset: BTreeMap<B256, U256> = std::iter::once((key_2, updated_val)).collect();
3371        let (expected_root, _) = harness.get_root_with_updates(&changeset);
3372
3373        let mut updated_storage = harness.storage().clone();
3374        updated_storage.insert(key_2, updated_val);
3375
3376        let updated_hashed = MockHashedCursorFactory::new(
3377            BTreeMap::new(),
3378            std::iter::once((harness.hashed_address(), updated_storage)).collect(),
3379        );
3380
3381        let mut prefix_set = PrefixSetMut::default();
3382        prefix_set.insert(Nibbles::unpack(key_2));
3383
3384        let trie_cursor =
3385            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3386        let hashed_cursor = updated_hashed.hashed_storage_cursor(harness.hashed_address()).unwrap();
3387        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3388            .with_prefix_set(prefix_set.freeze());
3389        let root_node = calculator
3390            .storage_root_node(harness.hashed_address())
3391            .expect("storage_root_node should succeed");
3392
3393        let got_root = calculator
3394            .compute_root_hash(&[root_node])
3395            .expect("root hash should succeed")
3396            .expect("root should get hashed");
3397        pretty_assertions::assert_eq!(expected_root, got_root);
3398    }
3399
3400    #[test]
3401    fn test_blinded_local_root_returns_trie_inconsistency() {
3402        let key = B256::right_padding_from(&[0x63, 0xaa]);
3403        let value = U256::ONE;
3404        let hash = storage_leaf_hash(&Nibbles::unpack(key).slice(2..), &value);
3405        let mask = TrieMask::from_nibble(3);
3406        let cached_branch =
3407            BranchNodeCompact::new(mask, TrieMask::default(), mask, vec![hash], None);
3408
3409        let mut harness = TrieTestHarness::new(BTreeMap::from([(key, value)]));
3410        harness.set_trie_nodes(BTreeMap::from([(Nibbles::from_nibbles([6]), cached_branch)]));
3411
3412        let trie_cursor =
3413            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3414        let hashed_cursor = harness
3415            .hashed_cursor_factory()
3416            .hashed_storage_cursor(harness.hashed_address())
3417            .unwrap();
3418        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
3419
3420        assert!(matches!(
3421            calculator.storage_root_node(harness.hashed_address()),
3422            Err(StateProofError::TrieInconsistency(_))
3423        ));
3424    }
3425
3426    #[test]
3427    fn test_cached_hash_with_deleted_leaf() {
3428        reth_tracing::init_test_tracing();
3429
3430        // Use different values to ensure distinct leaf hashes.
3431        let val_3 = U256::from(111u64);
3432        let val_5 = U256::from(222u64);
3433        let val_8 = U256::from(333u64);
3434
3435        // Keys under a common prefix `0x6_` to create a branch at path [6].
3436        // Use second byte to distinguish short keys (so they differ after position 2).
3437        let key_63 = B256::right_padding_from(&[0x63, 0xaa]); // nibble path: 6,3,a,a,...
3438        let key_65 = B256::right_padding_from(&[0x65, 0xbb]); // nibble path: 6,5,b,b,...
3439        let key_68 = B256::right_padding_from(&[0x68, 0xcc]); // nibble path: 6,8,c,c,...
3440
3441        // Compute leaf hashes. The branch at [6] consumes 2 nibbles (the branch path [6]
3442        // plus the child nibble), so each leaf's short key starts at position 2.
3443        let leaf_hash_3 = storage_leaf_hash(&Nibbles::unpack(key_63).slice(2..), &val_3);
3444        let leaf_hash_5 = storage_leaf_hash(&Nibbles::unpack(key_65).slice(2..), &val_5);
3445        let leaf_hash_8 = storage_leaf_hash(&Nibbles::unpack(key_68).slice(2..), &val_8);
3446
3447        // Build cached branch at [6] with state_mask and hash_mask bits for nibbles 3, 5, 8.
3448        let state_mask = TrieMask::new((1 << 3) | (1 << 5) | (1 << 8));
3449        let cached_branch = BranchNodeCompact::new(
3450            state_mask,
3451            TrieMask::new(0),
3452            state_mask, // hash_mask = state_mask (all children have cached hashes)
3453            vec![leaf_hash_3, leaf_hash_5, leaf_hash_8],
3454            None,
3455        );
3456
3457        let storage_nodes: BTreeMap<Nibbles, BranchNodeCompact> =
3458            std::iter::once((Nibbles::from_nibbles([0x6]), cached_branch)).collect();
3459
3460        // Compute the expected root from a fresh trie with just key_65 and key_68.
3461        let mut harness =
3462            TrieTestHarness::new([(key_65, val_5), (key_68, val_8)].into_iter().collect());
3463        let expected_root = harness.original_root();
3464
3465        // Update the harness with a cached trie node which will reference key_63 by hash.
3466        harness.set_trie_nodes(storage_nodes);
3467
3468        // Mark key_63 as dirty in the prefix set — in the real scenario the leaf was
3469        // deleted and the HashedPostState overlay masks it out.
3470        let mut prefix_set = PrefixSetMut::default();
3471        prefix_set.insert(Nibbles::unpack(key_63));
3472
3473        // Request a proof for key_63 (absence proof — no leaf exists).
3474        // Because the prefix set marks nibble 3's child path as dirty, the cached hash for
3475        // nibble 3 is skipped.
3476        let mut targets = vec![ProofV2Target::new(key_63)];
3477
3478        let trie_cursor =
3479            harness.trie_cursor_factory().storage_trie_cursor(harness.hashed_address()).unwrap();
3480        let hashed_cursor = harness
3481            .hashed_cursor_factory()
3482            .hashed_storage_cursor(harness.hashed_address())
3483            .unwrap();
3484        let mut calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor)
3485            .with_prefix_set(prefix_set.freeze());
3486
3487        let proofs = calculator
3488            .storage_proof(harness.hashed_address(), &mut targets)
3489            .expect("storage_proof should succeed");
3490        assert_eq!(1, proofs.len());
3491        let got_root = calculator
3492            .compute_root_hash(&proofs)
3493            .expect("compute_root_hash should succeed")
3494            .expect("should produce a root hash (proof contains root node)");
3495
3496        // With the bug, nibble 5 gets hashes[0] (nibble 3's hash) and nibble 8 gets
3497        // hashes[1] (nibble 5's hash), producing a wrong root.
3498        pretty_assertions::assert_eq!(
3499            expected_root,
3500            got_root,
3501            "Root hash should match trie without key_63; cached hash index is off when \
3502             an earlier hashed child has no leaves (absence proof target)"
3503        );
3504    }
3505}