reth_trie/proof_v2/
mod.rs

1//! Proof calculation version 2: Leaf-only implementation.
2//!
3//! This module provides a rewritten proof calculator that:
4//! - Uses only leaf data (HashedAccounts/Storages) to generate proofs
5//! - Returns proof nodes sorted lexicographically by path
6//! - Automatically resets after each calculation
7//! - Re-uses cursors across calculations
8//! - Supports generic value types with lazy evaluation
9
10use crate::{
11    hashed_cursor::{HashedCursor, HashedStorageCursor},
12    trie_cursor::{depth_first, TrieCursor, TrieStorageCursor},
13};
14use alloy_primitives::{B256, U256};
15use alloy_rlp::Encodable;
16use alloy_trie::TrieMask;
17use reth_execution_errors::trie::StateProofError;
18use reth_trie_common::{BranchNode, Nibbles, ProofTrieNode, RlpNode, TrieMasks, TrieNode};
19use std::{cmp::Ordering, iter::Peekable};
20use tracing::{instrument, trace};
21
22mod value;
23pub use value::*;
24
25mod node;
26use node::*;
27
28/// Target to use with the `tracing` crate.
29static TRACE_TARGET: &str = "trie::proof_v2";
30
31/// Number of bytes to pre-allocate for [`ProofCalculator`]'s `rlp_encode_buf` field.
32const RLP_ENCODE_BUF_SIZE: usize = 1024;
33
34/// A proof calculator that generates merkle proofs using only leaf data.
35///
36/// The calculator:
37/// - Accepts one or more B256 proof targets sorted lexicographically
38/// - Returns proof nodes sorted lexicographically by path
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    /// Children on the `child_stack` are converted to [`ProofTrieBranchChild::RlpNode`]s via the
63    /// [`Self::commit_child`] method. Committing a child indicates that no further changes are
64    /// expected to happen to it (e.g. splitting its short key when inserting a new branch). Given
65    /// that keys are consumed in lexicographical order, only the last child on the stack can
66    /// ever be modified, and therefore all children besides the last are expected to be
67    /// [`ProofTrieBranchChild::RlpNode`]s.
68    child_stack: Vec<ProofTrieBranchChild<VE::DeferredEncoder>>,
69    /// The proofs which will be returned from the calculation. This gets taken at the end of every
70    /// proof call.
71    retained_proofs: Vec<ProofTrieNode>,
72    /// Free-list of re-usable buffers of [`RlpNode`]s, used for encoding branch nodes to RLP.
73    ///
74    /// We are generally able to re-use these buffers across different branch nodes for the
75    /// duration of a proof calculation, but occasionally we will lose one when when a branch
76    /// node is returned as a `ProofTrieNode`.
77    rlp_nodes_bufs: Vec<Vec<RlpNode>>,
78    /// Re-usable byte buffer, used for RLP encoding.
79    rlp_encode_buf: Vec<u8>,
80}
81
82impl<TC, HC, VE: LeafValueEncoder> ProofCalculator<TC, HC, VE> {
83    /// Create a new [`ProofCalculator`] instance for calculating account proofs.
84    pub fn new(trie_cursor: TC, hashed_cursor: HC) -> Self {
85        Self {
86            trie_cursor,
87            hashed_cursor,
88            branch_stack: Vec::<_>::new(),
89            branch_path: Nibbles::new(),
90            child_stack: Vec::<_>::new(),
91            retained_proofs: Vec::<_>::new(),
92            rlp_nodes_bufs: Vec::<_>::new(),
93            rlp_encode_buf: Vec::<_>::with_capacity(RLP_ENCODE_BUF_SIZE),
94        }
95    }
96}
97
98/// Helper type for the [`Iterator`] used to pass targets in from the caller.
99type TargetsIter<I> = Peekable<WindowIter<I>>;
100
101impl<TC, HC, VE> ProofCalculator<TC, HC, VE>
102where
103    TC: TrieCursor,
104    HC: HashedCursor,
105    VE: LeafValueEncoder<Value = HC::Value>,
106{
107    /// Takes a re-usable `RlpNode` buffer from the internal free-list, or allocates a new one if
108    /// the free-list is empty.
109    ///
110    /// The returned Vec will have a length of zero.
111    fn take_rlp_nodes_buf(&mut self) -> Vec<RlpNode> {
112        self.rlp_nodes_bufs
113            .pop()
114            .map(|mut buf| {
115                buf.clear();
116                buf
117            })
118            .unwrap_or_else(|| Vec::with_capacity(16))
119    }
120
121    /// Returns true if the proof of a node at the given path should be retained.
122    /// A node is retained if its path is a prefix of any target.
123    /// This may move the
124    /// `targets` iterator forward if the given path comes after the current target.
125    ///
126    /// This method takes advantage of the [`WindowIter`] component of [`TargetsIter`] to only check
127    /// a single target at a time. The [`WindowIter`] allows us to look at a current target and the
128    /// next target simultaneously, forming an end-exclusive range.
129    ///
130    /// ```text
131    /// * Given targets: [ 0x012, 0x045, 0x678 ]
132    /// * targets.next() returns:
133    ///     - (0x012, Some(0x045)): covers (0x012..0x045)
134    ///     - (0x045, Some(0x678)): covers (0x045..0x678)
135    ///     - (0x678, None): covers (0x678..)
136    /// ```
137    ///
138    /// As long as the path which is passed in lies within that range we can continue to use the
139    /// current target. Once the path goes beyond that range (ie path >= next target) then we can be
140    /// sure that no further paths will be in the range, and we can iterate forward.
141    ///
142    /// ```text
143    /// * Given:
144    ///     - path: 0x04
145    ///     - targets.peek() returns (0x012, Some(0x045))
146    ///
147    /// * 0x04 comes _after_ 0x045 in depth-first order, so (0x012..0x045) does not contain 0x04.
148    ///
149    /// * targets.next() is called.
150    ///
151    /// * targets.peek() now returns (0x045, Some(0x678)). This does contain 0x04.
152    ///
153    /// * 0x04 is a prefix of 0x045, and so is retained.
154    /// ```
155    ///
156    /// Because paths in the trie are visited in depth-first order, it's imperative that targets are
157    /// given in depth-first order as well. If the targets were generated off of B256s, which is
158    /// the common-case, then this is equivalent to lexicographical order.
159    fn should_retain(
160        &self,
161        targets: &mut TargetsIter<impl Iterator<Item = Nibbles>>,
162        path: &Nibbles,
163    ) -> bool {
164        trace!(target: TRACE_TARGET, ?path, target = ?targets.peek(), "should_retain: called");
165        debug_assert!(self.retained_proofs.last().is_none_or(
166                |ProofTrieNode { path: last_retained_path, .. }| {
167                    depth_first::cmp(path, last_retained_path) == Ordering::Greater
168                }
169            ),
170            "should_retain called with path {path:?} which is not after previously retained node {:?} in depth-first order",
171            self.retained_proofs.last().map(|n| n.path),
172        );
173
174        let &(mut lower, mut upper) = targets.peek().expect("targets is never exhausted");
175
176        // If the path isn't in the current range then iterate forward until it is (or until there
177        // is no upper bound, indicating unbounded).
178        while upper.is_some_and(|upper| depth_first::cmp(path, &upper) != Ordering::Less) {
179            targets.next();
180            trace!(target: TRACE_TARGET, target = ?targets.peek(), "upper target <= path, next target");
181            let &(l, u) = targets.peek().expect("targets is never exhausted");
182            (lower, upper) = (l, u);
183        }
184
185        // If the node in question is a prefix of the target then we retain
186        lower.starts_with(path)
187    }
188
189    /// Takes a child which has been removed from the `child_stack` and converts it to an
190    /// [`RlpNode`].
191    ///
192    /// Calling this method indicates that the child will not undergo any further modifications, and
193    /// therefore can be retained as a proof node if applicable.
194    fn commit_child(
195        &mut self,
196        targets: &mut TargetsIter<impl Iterator<Item = Nibbles>>,
197        child_path: Nibbles,
198        child: ProofTrieBranchChild<VE::DeferredEncoder>,
199    ) -> Result<RlpNode, StateProofError> {
200        // If the child is already an `RlpNode` then there is nothing to do.
201        if let ProofTrieBranchChild::RlpNode(rlp_node) = child {
202            return Ok(rlp_node)
203        }
204
205        // If we should retain the child then do so.
206        if self.should_retain(targets, &child_path) {
207            trace!(target: TRACE_TARGET, ?child_path, "Retaining child");
208
209            // Convert to `ProofTrieNode`, which will be what is retained.
210            //
211            // If this node is a branch then its `rlp_nodes_buf` will be taken and not returned to
212            // the `rlp_nodes_bufs` free-list.
213            self.rlp_encode_buf.clear();
214            let proof_node = child.into_proof_trie_node(child_path, &mut self.rlp_encode_buf)?;
215
216            // Use the `ProofTrieNode` to encode the `RlpNode`, and then push it onto retained
217            // nodes before returning.
218            self.rlp_encode_buf.clear();
219            proof_node.node.encode(&mut self.rlp_encode_buf);
220
221            self.retained_proofs.push(proof_node);
222            return Ok(RlpNode::from_rlp(&self.rlp_encode_buf));
223        }
224
225        // If the child path is not being retained then we convert directly to an `RlpNode`
226        // using `into_rlp`. Since we are not retaining the node we can recover any `RlpNode`
227        // buffers for the free-list here, hence why we do this as a separate logical branch.
228        self.rlp_encode_buf.clear();
229        let (child_rlp_node, freed_rlp_nodes_buf) = child.into_rlp(&mut self.rlp_encode_buf)?;
230
231        // If there is an `RlpNode` buffer which can be re-used then push it onto the free-list.
232        if let Some(buf) = freed_rlp_nodes_buf {
233            self.rlp_nodes_bufs.push(buf);
234        }
235
236        Ok(child_rlp_node)
237    }
238
239    /// Returns the path of the child on top of the `child_stack`, or the root path if the stack is
240    /// empty.
241    fn last_child_path(&self) -> Nibbles {
242        // If there is no branch under construction then the top child must be the root child.
243        let Some(branch) = self.branch_stack.last() else {
244            return Nibbles::new();
245        };
246
247        debug_assert_ne!(branch.state_mask.get(), 0, "branch.state_mask can never be zero");
248        let last_nibble = u16::BITS - branch.state_mask.leading_zeros() - 1;
249
250        let mut child_path = self.branch_path;
251        debug_assert!(child_path.len() < 64);
252        child_path.push_unchecked(last_nibble as u8);
253        child_path
254    }
255
256    /// Calls [`Self::commit_child`] on the last child of `child_stack`, replacing it with a
257    /// [`ProofTrieBranchChild::RlpNode`].
258    ///
259    /// NOTE that this method call relies on the `state_mask` of the top branch of the
260    /// `branch_stack` to determine the last child's path. When committing the last child prior to
261    /// pushing a new child, it's important to set the new child's `state_mask` bit _after_ the call
262    /// to this method.
263    ///
264    /// # Panics
265    ///
266    /// This method panics if the `child_stack` is empty.
267    fn commit_last_child(
268        &mut self,
269        targets: &mut TargetsIter<impl Iterator<Item = Nibbles>>,
270    ) -> Result<(), StateProofError> {
271        let child = self
272            .child_stack
273            .pop()
274            .expect("`commit_last_child` cannot be called with empty `child_stack`");
275
276        // If the child is already an `RlpNode` then there is nothing to do, push it back on with no
277        // changes.
278        if let ProofTrieBranchChild::RlpNode(_) = child {
279            self.child_stack.push(child);
280            return Ok(())
281        }
282
283        let child_path = self.last_child_path();
284        let child_rlp_node = self.commit_child(targets, child_path, child)?;
285
286        // Replace the child on the stack
287        self.child_stack.push(ProofTrieBranchChild::RlpNode(child_rlp_node));
288        Ok(())
289    }
290
291    /// Creates a new leaf node on a branch, setting its `state_mask` bit and pushing the leaf onto
292    /// the `child_stack`.
293    ///
294    /// # Panics
295    ///
296    /// - If `branch_stack` is empty
297    /// - If the leaf's nibble is already set in the branch's `state_mask`.
298    fn push_new_leaf(
299        &mut self,
300        targets: &mut TargetsIter<impl Iterator<Item = Nibbles>>,
301        leaf_nibble: u8,
302        leaf_short_key: Nibbles,
303        leaf_val: VE::DeferredEncoder,
304    ) -> Result<(), StateProofError> {
305        // Before pushing the new leaf onto the `child_stack` we need to commit the previous last
306        // child (ie the first child of this new branch), so that only `child_stack`'s final child
307        // is a non-RlpNode.
308        self.commit_last_child(targets)?;
309
310        // Once the first child is committed we set the new child's bit on the top branch's
311        // `state_mask` and push that child.
312        let branch = self.branch_stack.last_mut().expect("branch_stack cannot be empty");
313
314        debug_assert!(!branch.state_mask.is_bit_set(leaf_nibble));
315        branch.state_mask.set_bit(leaf_nibble);
316
317        self.child_stack
318            .push(ProofTrieBranchChild::Leaf { short_key: leaf_short_key, value: leaf_val });
319
320        Ok(())
321    }
322
323    /// Pushes a new branch onto the `branch_stack`, while also pushing the given leaf onto the
324    /// `child_stack`.
325    ///
326    /// This method expects that there already exists a child on the `child_stack`, and that that
327    /// child has a non-zero short key. The new branch is constructed based on the top child from
328    /// the `child_stack` and the given leaf.
329    fn push_new_branch(
330        &mut self,
331        targets: &mut TargetsIter<impl Iterator<Item = Nibbles>>,
332        leaf_key: Nibbles,
333        leaf_val: VE::DeferredEncoder,
334    ) -> Result<(), StateProofError> {
335        // First determine the new leaf's shortkey relative to the current branch. If there is no
336        // current branch then the short key is the full key.
337        let leaf_short_key = if self.branch_stack.is_empty() {
338            leaf_key
339        } else {
340            // When there is a current branch then trim off its path as well as the nibble that it
341            // has set for this leaf.
342            trim_nibbles_prefix(&leaf_key, self.branch_path.len() + 1)
343        };
344
345        trace!(
346            target: TRACE_TARGET,
347            ?leaf_short_key,
348            branch_path = ?self.branch_path,
349            "push_new_branch: called",
350        );
351
352        // Get the new branch's first child, which is the child on the top of the stack with which
353        // the new leaf shares the same nibble on the current branch.
354        let first_child = self
355            .child_stack
356            .last_mut()
357            .expect("push_new_branch can't be called with empty child_stack");
358
359        let first_child_short_key = first_child.short_key();
360        debug_assert!(
361            !first_child_short_key.is_empty(),
362            "push_new_branch called when top child on stack is not a leaf or extension with a short key",
363        );
364
365        // Determine how many nibbles are shared between the new branch's first child and the new
366        // leaf. This common prefix will be the extension of the new branch
367        let common_prefix_len = first_child_short_key.common_prefix_length(&leaf_short_key);
368
369        // Trim off the common prefix from the first child's short key, plus one nibble which will
370        // stored by the new branch itself in its state mask.
371        let first_child_nibble = first_child_short_key.get_unchecked(common_prefix_len);
372        first_child.trim_short_key_prefix(common_prefix_len + 1);
373
374        // Similarly, trim off the common prefix, plus one nibble for the new branch, from the new
375        // leaf's short key.
376        let leaf_nibble = leaf_short_key.get_unchecked(common_prefix_len);
377        let leaf_short_key = trim_nibbles_prefix(&leaf_short_key, common_prefix_len + 1);
378
379        // Push the new branch onto the branch stack. We do not yet set the `state_mask` bit of the
380        // new leaf; `push_new_leaf` will do that.
381        self.branch_stack.push(ProofTrieBranch {
382            ext_len: common_prefix_len as u8,
383            state_mask: TrieMask::new(1 << first_child_nibble),
384            tree_mask: TrieMask::default(),
385            hash_mask: TrieMask::default(),
386        });
387
388        // Update the branch path to reflect the new branch which was just pushed. Its path will be
389        // the path of the previous branch, plus the nibble shared by each child, plus the parent
390        // extension (denoted by a non-zero `ext_len`). Since the new branch's path is a prefix of
391        // the original leaf_key we can just slice that.
392        //
393        // If the branch is the first branch then we do not add the extra 1, as there is no nibble
394        // in a parent branch to account for.
395        let branch_path_len = self.branch_path.len() +
396            common_prefix_len +
397            if self.branch_stack.len() == 1 { 0 } else { 1 };
398        self.branch_path = leaf_key.slice_unchecked(0, branch_path_len);
399
400        // Push the new leaf onto the new branch. This step depends on the top branch being in the
401        // correct state, so must be done last.
402        self.push_new_leaf(targets, leaf_nibble, leaf_short_key, leaf_val)?;
403
404        trace!(
405            target: TRACE_TARGET,
406            ?leaf_short_key,
407            ?common_prefix_len,
408            new_branch = ?self.branch_stack.last().expect("branch_stack was just pushed to"),
409            ?branch_path_len,
410            branch_path = ?self.branch_path,
411            "push_new_branch: returning",
412        );
413
414        Ok(())
415    }
416    /// Pops the top branch off of the `branch_stack`, hashes its children on the `child_stack`, and
417    /// replaces those children on the `child_stack`. The `branch_path` field will be updated
418    /// accordingly.
419    ///
420    /// # Panics
421    ///
422    /// This method panics if `branch_stack` is empty.
423    fn pop_branch(
424        &mut self,
425        targets: &mut TargetsIter<impl Iterator<Item = Nibbles>>,
426    ) -> Result<(), StateProofError> {
427        trace!(
428            target: TRACE_TARGET,
429            branch = ?self.branch_stack.last(),
430            branch_path = ?self.branch_path,
431            child_stack_len = ?self.child_stack.len(),
432            "pop_branch: called",
433        );
434
435        // Ensure the final child on the child stack has been committed, as this method expects all
436        // children of the branch to have been committed.
437        self.commit_last_child(targets)?;
438
439        let mut rlp_nodes_buf = self.take_rlp_nodes_buf();
440        let branch = self.branch_stack.pop().expect("branch_stack cannot be empty");
441
442        // Take the branch's children off the stack, using the state mask to determine how many
443        // there are.
444        let num_children = branch.state_mask.count_ones() as usize;
445        debug_assert!(num_children > 1, "A branch must have at least two children");
446        debug_assert!(
447            self.child_stack.len() >= num_children,
448            "Stack is missing necessary children ({num_children:?})"
449        );
450
451        // Collect children into an `RlpNode` Vec by committing and pushing each of them.
452        for child in self.child_stack.drain(self.child_stack.len() - num_children..) {
453            let ProofTrieBranchChild::RlpNode(child_rlp_node) = child else {
454                panic!(
455                    "all branch child must have been committed, found {}",
456                    std::any::type_name_of_val(&child)
457                );
458            };
459            rlp_nodes_buf.push(child_rlp_node);
460        }
461
462        debug_assert_eq!(
463            rlp_nodes_buf.len(),
464            branch.state_mask.count_ones() as usize,
465            "children length must match number of bits set in state_mask"
466        );
467
468        // Calculate the short key of the parent extension (if the branch has a parent extension).
469        // It's important to calculate this short key prior to modifying the `branch_path`.
470        let short_key = trim_nibbles_prefix(
471            &self.branch_path,
472            self.branch_path.len() - branch.ext_len as usize,
473        );
474
475        // Wrap the `BranchNode` so it can be pushed onto the child stack.
476        let mut branch_as_child =
477            ProofTrieBranchChild::Branch(BranchNode::new(rlp_nodes_buf, branch.state_mask));
478
479        // If there is an extension then encode the branch as an `RlpNode` and use it to construct
480        // the extension in its place
481        if !short_key.is_empty() {
482            let branch_rlp_node = self.commit_child(targets, self.branch_path, branch_as_child)?;
483            branch_as_child = ProofTrieBranchChild::Extension { short_key, child: branch_rlp_node };
484        };
485
486        self.child_stack.push(branch_as_child);
487
488        // Update the branch_path. If this branch is the only branch then only its extension needs
489        // to be trimmed, otherwise we also need to remove its nibble from its parent.
490        let new_path_len = self.branch_path.len() -
491            branch.ext_len as usize -
492            if self.branch_stack.is_empty() { 0 } else { 1 };
493
494        debug_assert!(self.branch_path.len() >= new_path_len);
495        self.branch_path = self.branch_path.slice_unchecked(0, new_path_len);
496
497        Ok(())
498    }
499
500    /// Adds a single leaf for a key to the stack, possibly collapsing an existing branch and/or
501    /// creating a new one depending on the path of the key.
502    fn add_leaf(
503        &mut self,
504        targets: &mut TargetsIter<impl Iterator<Item = Nibbles>>,
505        key: Nibbles,
506        val: VE::DeferredEncoder,
507    ) -> Result<(), StateProofError> {
508        loop {
509            trace!(
510                target: TRACE_TARGET,
511                ?key,
512                branch_stack_len = ?self.branch_stack.len(),
513                branch_path = ?self.branch_path,
514                child_stack_len = ?self.child_stack.len(),
515                "add_leaf: loop",
516            );
517
518            // Get the `state_mask` of the branch currently being built. If there are no branches on
519            // the stack then it means either the trie is empty or only a single leaf has been added
520            // previously.
521            let curr_branch_state_mask = match self.branch_stack.last() {
522                Some(curr_branch) => curr_branch.state_mask,
523                None if self.child_stack.is_empty() => {
524                    // If the child stack is empty then this is the first leaf, push it and be done
525                    self.child_stack
526                        .push(ProofTrieBranchChild::Leaf { short_key: key, value: val });
527                    return Ok(())
528                }
529                None => {
530                    // If the child stack is not empty then it must only have a single other child
531                    // which is either a leaf or extension with a non-zero short key.
532                    debug_assert_eq!(self.child_stack.len(), 1);
533                    debug_assert!(!self
534                        .child_stack
535                        .last()
536                        .expect("already checked for emptiness")
537                        .short_key()
538                        .is_empty());
539                    self.push_new_branch(targets, key, val)?;
540                    return Ok(())
541                }
542            };
543
544            // Find the common prefix length, which is the number of nibbles shared between the
545            // current branch and the key.
546            let common_prefix_len = self.branch_path.common_prefix_length(&key);
547
548            // If the current branch does not share all of its nibbles with the new key then it is
549            // not the parent of the new key. In this case the current branch will have no more
550            // children. We can pop it and loop back to the top to try again with its parent branch.
551            if common_prefix_len < self.branch_path.len() {
552                self.pop_branch(targets)?;
553                continue
554            }
555
556            // If the current branch is a prefix of the new key then the leaf is a child of the
557            // branch. If the branch doesn't have the leaf's nibble set then the leaf can be added
558            // directly, otherwise a new branch must be created in-between this branch and that
559            // existing child.
560            let nibble = key.get_unchecked(common_prefix_len);
561            if curr_branch_state_mask.is_bit_set(nibble) {
562                // This method will also push the new leaf onto the `child_stack`.
563                self.push_new_branch(targets, key, val)?;
564            } else {
565                let short_key = key.slice_unchecked(common_prefix_len + 1, key.len());
566                self.push_new_leaf(targets, nibble, short_key, val)?;
567            }
568
569            return Ok(())
570        }
571    }
572
573    /// Internal implementation of proof calculation. Assumes both cursors have already been reset.
574    /// See docs on [`Self::proof`] for expected behavior.
575    fn proof_inner(
576        &mut self,
577        value_encoder: &VE,
578        targets: impl IntoIterator<Item = Nibbles>,
579    ) -> Result<Vec<ProofTrieNode>, StateProofError> {
580        trace!(target: TRACE_TARGET, "proof_inner: called");
581
582        // In debug builds, verify that targets are sorted
583        #[cfg(debug_assertions)]
584        let targets = {
585            let mut prev: Option<Nibbles> = None;
586            targets.into_iter().inspect(move |target| {
587                if let Some(prev) = prev {
588                    debug_assert!(
589                        depth_first::cmp(&prev, target) != Ordering::Greater,
590                        "targets must be sorted depth-first, instead {:?} > {:?}",
591                        prev,
592                        target
593                    );
594                }
595                prev = Some(*target);
596            })
597        };
598
599        #[cfg(not(debug_assertions))]
600        let targets = targets.into_iter();
601
602        // Wrap targets into a `TargetsIter`.
603        let mut targets = WindowIter::new(targets).peekable();
604
605        // If there are no targets then nothing could be returned, return early.
606        if targets.peek().is_none() {
607            trace!(target: TRACE_TARGET, "Empty targets, returning");
608            return Ok(Vec::new())
609        }
610
611        // Ensure initial state is cleared. By the end of the method call these should be empty once
612        // again.
613        debug_assert!(self.branch_stack.is_empty());
614        debug_assert!(self.branch_path.is_empty());
615        debug_assert!(self.child_stack.is_empty());
616
617        let mut hashed_cursor_current = self.hashed_cursor.seek(B256::ZERO)?;
618        loop {
619            trace!(
620                target: TRACE_TARGET,
621                ?hashed_cursor_current,
622                branch_stack_len = ?self.branch_stack.len(),
623                branch_path = ?self.branch_path,
624                child_stack_len = ?self.child_stack.len(),
625                "proof_inner: loop",
626            );
627
628            // Sanity check before making any further changes:
629            // If there is a branch, there must be at least two children
630            debug_assert!(self.branch_stack.last().is_none_or(|_| self.child_stack.len() >= 2));
631
632            // Fetch the next leaf from the hashed cursor, converting the key to Nibbles and
633            // immediately creating the DeferredValueEncoder so that encoding of the leaf value can
634            // begin ASAP.
635            let Some((key, val)) = hashed_cursor_current.map(|(key_b256, val)| {
636                debug_assert_eq!(key_b256.len(), 32);
637                // SAFETY: key is a B256 and so is exactly 32-bytes.
638                let key = unsafe { Nibbles::unpack_unchecked(key_b256.as_slice()) };
639                let val = value_encoder.deferred_encoder(key_b256, val);
640                (key, val)
641            }) else {
642                break
643            };
644
645            self.add_leaf(&mut targets, key, val)?;
646            hashed_cursor_current = self.hashed_cursor.next()?;
647        }
648
649        // Once there's no more leaves we can pop the remaining branches, if any.
650        while !self.branch_stack.is_empty() {
651            self.pop_branch(&mut targets)?;
652        }
653
654        // At this point the branch stack should be empty. If the child stack is empty it means no
655        // keys were ever iterated from the hashed cursor in the first place. Otherwise there should
656        // only be a single node left: the root node.
657        debug_assert!(self.branch_stack.is_empty());
658        debug_assert!(self.branch_path.is_empty());
659        debug_assert!(self.child_stack.len() < 2);
660
661        // All targets match the root node, so always retain it. Determine the root node based on
662        // the child stack, and push the proof of the root node onto the result stack.
663        let root_node = if let Some(node) = self.child_stack.pop() {
664            self.rlp_encode_buf.clear();
665            node.into_proof_trie_node(Nibbles::new(), &mut self.rlp_encode_buf)?
666        } else {
667            ProofTrieNode {
668                path: Nibbles::new(), // root path
669                node: TrieNode::EmptyRoot,
670                masks: TrieMasks::none(),
671            }
672        };
673        self.retained_proofs.push(root_node);
674
675        trace!(
676            target: TRACE_TARGET,
677            retained_proofs_len = ?self.retained_proofs.len(),
678            "proof_inner: returning",
679        );
680        Ok(core::mem::take(&mut self.retained_proofs))
681    }
682}
683
684impl<TC, HC, VE> ProofCalculator<TC, HC, VE>
685where
686    TC: TrieCursor,
687    HC: HashedCursor,
688    VE: LeafValueEncoder<Value = HC::Value>,
689{
690    /// Generate a proof for the given targets.
691    ///
692    /// Given depth-first sorted targets, returns nodes whose paths are a prefix of any target. The
693    /// returned nodes will be sorted lexicographically by path.
694    ///
695    /// # Panics
696    ///
697    /// In debug builds, panics if the targets are not sorted lexicographically.
698    #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
699    pub fn proof(
700        &mut self,
701        value_encoder: &VE,
702        targets: impl IntoIterator<Item = Nibbles>,
703    ) -> Result<Vec<ProofTrieNode>, StateProofError> {
704        self.trie_cursor.reset();
705        self.hashed_cursor.reset();
706        self.proof_inner(value_encoder, targets)
707    }
708}
709
710/// A proof calculator for storage tries.
711pub type StorageProofCalculator<TC, HC> = ProofCalculator<TC, HC, StorageValueEncoder>;
712
713impl<TC, HC> StorageProofCalculator<TC, HC>
714where
715    TC: TrieStorageCursor,
716    HC: HashedStorageCursor<Value = U256>,
717{
718    /// Create a new [`StorageProofCalculator`] instance.
719    pub fn new_storage(trie_cursor: TC, hashed_cursor: HC) -> Self {
720        Self::new(trie_cursor, hashed_cursor)
721    }
722
723    /// Generate a proof for a storage trie at the given hashed address.
724    ///
725    /// Given depth-first sorted targets, returns nodes whose paths are a prefix of any target. The
726    /// returned nodes will be sorted lexicographically by path.
727    ///
728    /// # Panics
729    ///
730    /// In debug builds, panics if the targets are not sorted lexicographically.
731    #[instrument(target = TRACE_TARGET, level = "trace", skip(self, targets))]
732    pub fn storage_proof(
733        &mut self,
734        hashed_address: B256,
735        targets: impl IntoIterator<Item = Nibbles>,
736    ) -> Result<Vec<ProofTrieNode>, StateProofError> {
737        /// Static storage value encoder instance used by all storage proofs.
738        static STORAGE_VALUE_ENCODER: StorageValueEncoder = StorageValueEncoder;
739
740        self.hashed_cursor.set_hashed_address(hashed_address);
741
742        // Shortcut: check if storage is empty
743        if self.hashed_cursor.is_storage_empty()? {
744            // Return a single EmptyRoot node at the root path
745            return Ok(vec![ProofTrieNode {
746                path: Nibbles::default(),
747                node: TrieNode::EmptyRoot,
748                masks: TrieMasks::none(),
749            }])
750        }
751
752        // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has
753        // been checked.
754        self.trie_cursor.set_hashed_address(hashed_address);
755
756        // Use the static StorageValueEncoder and pass it to proof_inner
757        self.proof_inner(&STORAGE_VALUE_ENCODER, targets)
758    }
759}
760
761/// `WindowIter` is a wrapper around an [`Iterator`] which allows viewing both previous and current
762/// items on every iteration. It is similar to `itertools::tuple_windows`, except that the final
763/// item returned will contain the previous item and `None` as the current.
764struct WindowIter<I: Iterator> {
765    iter: I,
766    prev: Option<I::Item>,
767}
768
769impl<I: Iterator> WindowIter<I> {
770    /// Wraps an iterator with a [`WindowIter`].
771    const fn new(iter: I) -> Self {
772        Self { iter, prev: None }
773    }
774}
775
776impl<I: Iterator<Item: Copy>> Iterator for WindowIter<I> {
777    /// The iterator returns the previous and current items, respectively. If the underlying
778    /// iterator is exhausted then `Some(prev, None)` is returned on the subsequent call to
779    /// `WindowIter::next`, and `None` from the call after that.
780    type Item = (I::Item, Option<I::Item>);
781
782    fn next(&mut self) -> Option<Self::Item> {
783        loop {
784            match (self.prev, self.iter.next()) {
785                (None, None) => return None,
786                (None, Some(v)) => {
787                    self.prev = Some(v);
788                }
789                (Some(v), next) => {
790                    self.prev = next;
791                    return Some((v, next))
792                }
793            }
794        }
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::{
802        hashed_cursor::{mock::MockHashedCursorFactory, HashedCursorFactory},
803        proof::Proof,
804        trie_cursor::{depth_first, mock::MockTrieCursorFactory, TrieCursorFactory},
805    };
806    use alloy_primitives::map::{B256Map, B256Set};
807    use alloy_rlp::Decodable;
808    use assert_matches::assert_matches;
809    use itertools::Itertools;
810    use reth_trie_common::{HashedPostState, MultiProofTargets, TrieNode};
811    use std::collections::BTreeMap;
812
813    /// Target to use with the `tracing` crate.
814    static TRACE_TARGET: &str = "trie::proof_v2::tests";
815
816    /// A test harness for comparing `ProofCalculator` and legacy `Proof` implementations.
817    ///
818    /// This harness creates mock cursor factories from a `HashedPostState` and provides
819    /// a method to test that both proof implementations produce equivalent results.
820    struct ProofTestHarness {
821        /// Mock factory for trie cursors (empty by default for leaf-only tests)
822        trie_cursor_factory: MockTrieCursorFactory,
823        /// Mock factory for hashed cursors, populated from `HashedPostState`
824        hashed_cursor_factory: MockHashedCursorFactory,
825    }
826
827    impl ProofTestHarness {
828        /// Creates a new test harness from a `HashedPostState`.
829        ///
830        /// The `HashedPostState` is used to populate the mock hashed cursor factory directly.
831        /// The trie cursor factory is empty by default, suitable for testing the leaf-only
832        /// proof calculator.
833        fn new(post_state: HashedPostState) -> Self {
834            trace!(target: TRACE_TARGET, ?post_state, "Creating ProofTestHarness");
835
836            // Ensure that there's a storage trie dataset for every storage trie, even if empty.
837            let storage_trie_nodes: B256Map<BTreeMap<_, _>> = post_state
838                .storages
839                .keys()
840                .copied()
841                .map(|addr| (addr, Default::default()))
842                .collect();
843
844            // Create mock hashed cursor factory from the post state
845            let hashed_cursor_factory = MockHashedCursorFactory::from_hashed_post_state(post_state);
846
847            // Create empty trie cursor factory (leaf-only calculator doesn't need trie nodes)
848            let trie_cursor_factory =
849                MockTrieCursorFactory::new(BTreeMap::new(), storage_trie_nodes);
850
851            Self { trie_cursor_factory, hashed_cursor_factory }
852        }
853
854        /// Asserts that `ProofCalculator` and legacy `Proof` produce equivalent results for account
855        /// proofs.
856        ///
857        /// This method calls both implementations with the given account targets and compares
858        /// the results.
859        fn assert_proof(
860            &self,
861            targets: impl IntoIterator<Item = B256> + Clone,
862        ) -> Result<(), StateProofError> {
863            // Convert B256 targets to Nibbles for proof_v2
864            let targets_vec: Vec<B256> = targets.into_iter().collect();
865            let nibbles_targets: Vec<Nibbles> =
866                targets_vec.iter().map(|b256| Nibbles::unpack(b256.as_slice())).sorted().collect();
867            // Convert B256 targets to MultiProofTargets for legacy implementation
868            // For account-only proofs, each account maps to an empty storage set
869            let legacy_targets = targets_vec
870                .iter()
871                .map(|addr| (*addr, B256Set::default()))
872                .collect::<MultiProofTargets>();
873
874            // Create ProofCalculator (proof_v2) with account cursors
875            let trie_cursor = self.trie_cursor_factory.account_trie_cursor()?;
876            let hashed_cursor = self.hashed_cursor_factory.hashed_account_cursor()?;
877
878            // Call ProofCalculator::proof with account targets
879            let value_encoder = SyncAccountValueEncoder::new(
880                self.trie_cursor_factory.clone(),
881                self.hashed_cursor_factory.clone(),
882            );
883            let mut proof_calculator = ProofCalculator::new(trie_cursor, hashed_cursor);
884            let proof_v2_result = proof_calculator.proof(&value_encoder, nibbles_targets)?;
885
886            // Call Proof::multiproof (legacy implementation)
887            let proof_legacy_result =
888                Proof::new(self.trie_cursor_factory.clone(), self.hashed_cursor_factory.clone())
889                    .multiproof(legacy_targets)?;
890
891            // Decode and sort legacy proof nodes
892            let mut proof_legacy_nodes = proof_legacy_result
893                .account_subtree
894                .iter()
895                .map(|(path, node_enc)| {
896                    let mut buf = node_enc.as_ref();
897                    let node = TrieNode::decode(&mut buf)
898                        .expect("legacy implementation should not produce malformed proof nodes");
899
900                    ProofTrieNode {
901                        path: *path,
902                        node,
903                        masks: TrieMasks {
904                            hash_mask: proof_legacy_result
905                                .branch_node_hash_masks
906                                .get(path)
907                                .copied(),
908                            tree_mask: proof_legacy_result
909                                .branch_node_tree_masks
910                                .get(path)
911                                .copied(),
912                        },
913                    }
914                })
915                .sorted_by(|a, b| depth_first::cmp(&a.path, &b.path))
916                .collect::<Vec<_>>();
917
918            // When no targets are given the legacy implementation will still produce the root node
919            // in the proof. This differs from the V2 implementation, which produces nothing when
920            // given no targets.
921            if targets_vec.is_empty() {
922                assert_matches!(
923                    proof_legacy_nodes.pop(),
924                    Some(ProofTrieNode { path, .. }) if path.is_empty()
925                );
926                assert!(proof_legacy_nodes.is_empty());
927            }
928
929            // Basic comparison: both should succeed and produce identical results
930            assert_eq!(proof_legacy_nodes, proof_v2_result);
931
932            Ok(())
933        }
934    }
935
936    mod proptest_tests {
937        use super::*;
938        use alloy_primitives::{map::B256Map, U256};
939        use proptest::prelude::*;
940        use reth_primitives_traits::Account;
941        use reth_trie_common::HashedPostState;
942
943        /// Generate a strategy for Account values
944        fn account_strategy() -> impl Strategy<Value = Account> {
945            (any::<u64>(), any::<u64>(), any::<[u8; 32]>()).prop_map(
946                |(nonce, balance, code_hash)| Account {
947                    nonce,
948                    balance: U256::from(balance),
949                    bytecode_hash: Some(B256::from(code_hash)),
950                },
951            )
952        }
953
954        /// Generate a strategy for `HashedPostState` with random accounts
955        fn hashed_post_state_strategy() -> impl Strategy<Value = HashedPostState> {
956            prop::collection::vec((any::<[u8; 32]>(), account_strategy()), 0..40).prop_map(
957                |accounts| {
958                    let account_map = accounts
959                        .into_iter()
960                        .map(|(addr_bytes, account)| (B256::from(addr_bytes), Some(account)))
961                        .collect::<B256Map<_>>();
962
963                    // All accounts have empty storages.
964                    let storages = account_map
965                        .keys()
966                        .copied()
967                        .map(|addr| (addr, Default::default()))
968                        .collect::<B256Map<_>>();
969
970                    HashedPostState { accounts: account_map, storages }
971                },
972            )
973        }
974
975        /// Generate a strategy for proof targets that are 80% from the `HashedPostState` accounts
976        /// and 20% random keys.
977        fn proof_targets_strategy(account_keys: Vec<B256>) -> impl Strategy<Value = Vec<B256>> {
978            let num_accounts = account_keys.len();
979
980            // Generate between 0 and (num_accounts + 5) targets
981            let target_count = 0..=(num_accounts + 5);
982
983            target_count.prop_flat_map(move |count| {
984                let account_keys = account_keys.clone();
985                prop::collection::vec(
986                    prop::bool::weighted(0.8).prop_flat_map(move |from_accounts| {
987                        if from_accounts && !account_keys.is_empty() {
988                            // 80% chance: pick from existing account keys
989                            prop::sample::select(account_keys.clone()).boxed()
990                        } else {
991                            // 20% chance: generate random B256
992                            any::<[u8; 32]>().prop_map(B256::from).boxed()
993                        }
994                    }),
995                    count,
996                )
997            })
998        }
999
1000        proptest! {
1001            #![proptest_config(ProptestConfig::with_cases(8000))]
1002
1003            /// Tests that ProofCalculator produces valid proofs for randomly generated
1004            /// HashedPostState with proof targets.
1005            ///
1006            /// This test:
1007            /// - Generates random accounts in a HashedPostState
1008            /// - Generates proof targets: 80% from existing account keys, 20% random
1009            /// - Creates a test harness with the generated state
1010            /// - Calls assert_proof with the generated targets
1011            /// - Verifies both ProofCalculator and legacy Proof produce equivalent results
1012            #[test]
1013            fn proptest_proof_with_targets(
1014                (post_state, targets) in hashed_post_state_strategy()
1015                    .prop_flat_map(|post_state| {
1016                        let account_keys: Vec<B256> = post_state.accounts.keys().copied().collect();
1017                        let targets_strategy = proof_targets_strategy(account_keys);
1018                        (Just(post_state), targets_strategy)
1019                    })
1020            ) {
1021                reth_tracing::init_test_tracing();
1022                let harness = ProofTestHarness::new(post_state);
1023
1024                // Pass generated targets to both implementations
1025                harness.assert_proof(targets).expect("Proof generation failed");
1026            }
1027        }
1028    }
1029}