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}