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::{TrieCursor, TrieStorageCursor},
13};
14use alloy_primitives::{B256, U256};
15use alloy_trie::TrieMask;
16use reth_execution_errors::trie::StateProofError;
17use reth_trie_common::{BranchNode, Nibbles, ProofTrieNode, RlpNode, TrieMasks, TrieNode};
18use tracing::{instrument, trace};
19
20mod value;
21pub use value::*;
22
23mod node;
24use node::*;
25
26/// Target to use with the `tracing` crate.
27static TRACE_TARGET: &str = "trie::proof_v2";
28
29/// A proof calculator that generates merkle proofs using only leaf data.
30///
31/// The calculator:
32/// - Accepts one or more B256 proof targets sorted lexicographically
33/// - Returns proof nodes sorted lexicographically by path
34/// - Automatically resets after each calculation
35/// - Re-uses cursors from one calculation to the next
36#[derive(Debug)]
37pub struct ProofCalculator<TC, HC, VE: LeafValueEncoder> {
38 /// Trie cursor for traversing stored branch nodes.
39 trie_cursor: TC,
40 /// Hashed cursor for iterating over leaf data.
41 hashed_cursor: HC,
42 /// Branches which are currently in the process of being constructed, each being a child of
43 /// the previous one.
44 branch_stack: Vec<ProofTrieBranch>,
45 /// The path of the last branch in `branch_stack`.
46 branch_path: Nibbles,
47 /// Children of branches in the `branch_stack`.
48 ///
49 /// Each branch in `branch_stack` tracks which children are in this stack using its
50 /// `state_mask`; the number of children the branch has in this stack is equal to the number of
51 /// bits set in its `state_mask`.
52 ///
53 /// The children for the bottom branch in `branch_stack` are found at the bottom of this stack,
54 /// and so on. When a branch is removed from `branch_stack` its children are removed from this
55 /// one, and the branch is pushed onto this stack in their place (see [`Self::pop_branch`].
56 child_stack: Vec<ProofTrieBranchChild<VE::DeferredEncoder>>,
57 /// Free-list of re-usable buffers of [`RlpNode`]s, used for encoding branch nodes to RLP.
58 ///
59 /// We are generally able to re-use these buffers across different branch nodes for the
60 /// duration of a proof calculation, but occasionally we will lose one when when a branch
61 /// node is returned as a `ProofTrieNode`.
62 rlp_nodes_bufs: Vec<Vec<RlpNode>>,
63 /// Re-usable byte buffer, used for RLP encoding.
64 rlp_encode_buf: Vec<u8>,
65}
66
67impl<TC, HC, VE: LeafValueEncoder> ProofCalculator<TC, HC, VE> {
68 /// Create a new [`ProofCalculator`] instance for calculating account proofs.
69 pub const fn new(trie_cursor: TC, hashed_cursor: HC) -> Self {
70 Self {
71 trie_cursor,
72 hashed_cursor,
73 branch_stack: Vec::<_>::new(),
74 branch_path: Nibbles::new(),
75 child_stack: Vec::<_>::new(),
76 rlp_nodes_bufs: Vec::<_>::new(),
77 rlp_encode_buf: Vec::<_>::new(),
78 }
79 }
80}
81
82impl<TC, HC, VE> ProofCalculator<TC, HC, VE>
83where
84 TC: TrieCursor,
85 HC: HashedCursor,
86 VE: LeafValueEncoder<Value = HC::Value>,
87{
88 /// Takes a re-usable `RlpNode` buffer from the internal free-list, or allocates a new one if
89 /// the free-list is empty.
90 ///
91 /// The returned Vec will have a length of zero.
92 fn take_rlp_nodes_buf(&mut self) -> Vec<RlpNode> {
93 self.rlp_nodes_bufs
94 .pop()
95 .map(|mut buf| {
96 buf.clear();
97 buf
98 })
99 .unwrap_or_else(|| Vec::with_capacity(16))
100 }
101
102 /// Pushes a new branch onto the `branch_stack`, while also pushing the given leaf onto the
103 /// `child_stack`.
104 ///
105 /// This method expects that there already exists a child on the `child_stack`, and that that
106 /// child has a non-zero short key. The new branch is constructed based on the top child from
107 /// the `child_stack` and the given leaf.
108 fn push_new_branch(&mut self, leaf_key: Nibbles, leaf_val: VE::DeferredEncoder) {
109 // First determine the new leaf's shortkey relative to the current branch. If there is no
110 // current branch then the short key is the full key.
111 let leaf_short_key = if self.branch_stack.is_empty() {
112 leaf_key
113 } else {
114 // When there is a current branch then trim off its path as well as the nibble that it
115 // has set for this leaf.
116 trim_nibbles_prefix(&leaf_key, self.branch_path.len() + 1)
117 };
118
119 trace!(
120 target: TRACE_TARGET,
121 ?leaf_short_key,
122 branch_path = ?self.branch_path,
123 "push_new_branch: called",
124 );
125
126 // Get the new branch's first child, which is the child on the top of the stack with which
127 // the new leaf shares the same nibble on the current branch.
128 let first_child = self
129 .child_stack
130 .last_mut()
131 .expect("push_branch can't be called with empty child_stack");
132
133 let first_child_short_key = first_child.short_key();
134 debug_assert!(
135 !first_child_short_key.is_empty(),
136 "push_branch called when top child on stack is not a leaf or extension with a short key",
137 );
138
139 // Determine how many nibbles are shared between the new branch's first child and the new
140 // leaf. This common prefix will be the extension of the new branch
141 let common_prefix_len = first_child_short_key.common_prefix_length(&leaf_short_key);
142
143 // Trim off the common prefix from the first child's short key, plus one nibble which will
144 // stored by the new branch itself in its state mask.
145 let first_child_nibble = first_child_short_key.get_unchecked(common_prefix_len);
146 first_child.trim_short_key_prefix(common_prefix_len + 1);
147
148 // Similarly, trim off the common prefix, plus one nibble for the new branch, from the new
149 // leaf's short key.
150 let leaf_nibble = leaf_short_key.get_unchecked(common_prefix_len);
151 let leaf_short_key = trim_nibbles_prefix(&leaf_short_key, common_prefix_len + 1);
152
153 // Push the new leaf onto the child stack; it will be the second child of the new branch.
154 // The new branch's first child is the child already on the top of the stack, for which
155 // we've already adjusted its short key.
156 self.child_stack
157 .push(ProofTrieBranchChild::Leaf { short_key: leaf_short_key, value: leaf_val });
158
159 // Construct the state mask of the new branch, and push the new branch onto the branch
160 // stack.
161 self.branch_stack.push(ProofTrieBranch {
162 ext_len: common_prefix_len as u8,
163 state_mask: {
164 let mut m = TrieMask::default();
165 m.set_bit(first_child_nibble);
166 m.set_bit(leaf_nibble);
167 m
168 },
169 tree_mask: TrieMask::default(),
170 hash_mask: TrieMask::default(),
171 });
172
173 // Update the branch path to reflect the new branch which was just pushed. Its path will be
174 // the path of the previous branch, plus the nibble shared by each child, plus the parent
175 // extension (denoted by a non-zero `ext_len`). Since the new branch's path is a prefix of
176 // the original leaf_key we can just slice that.
177 //
178 // If the branch is the first branch then we do not add the extra 1, as there is no nibble
179 // in a parent branch to account for.
180 let branch_path_len = self.branch_path.len() +
181 common_prefix_len +
182 if self.branch_stack.len() == 1 { 0 } else { 1 };
183 self.branch_path = leaf_key.slice_unchecked(0, branch_path_len);
184
185 trace!(
186 target: TRACE_TARGET,
187 ?leaf_short_key,
188 ?common_prefix_len,
189 new_branch = ?self.branch_stack.last().expect("branch_stack was just pushed to"),
190 ?branch_path_len,
191 branch_path = ?self.branch_path,
192 "push_new_branch: returning",
193 );
194 }
195
196 /// Pops the top branch off of the `branch_stack`, hashes its children on the `child_stack`, and
197 /// replaces those children on the `child_stack`. The `branch_path` field will be updated
198 /// accordingly.
199 ///
200 /// # Panics
201 ///
202 /// This method panics if `branch_stack` is empty.
203 fn pop_branch(&mut self) -> Result<(), StateProofError> {
204 let mut rlp_nodes_buf = self.take_rlp_nodes_buf();
205 let branch = self.branch_stack.pop().expect("branch_stack cannot be empty");
206
207 trace!(
208 target: TRACE_TARGET,
209 ?branch,
210 branch_path = ?self.branch_path,
211 "pop_branch: called",
212 );
213
214 // Take the branch's children off the stack, using the state mask to determine how many
215 // there are.
216 let num_children = branch.state_mask.count_ones() as usize;
217 debug_assert!(num_children > 1, "A branch must have at least two children");
218 debug_assert!(
219 self.child_stack.len() >= num_children,
220 "Stack is missing necessary children"
221 );
222 let children = self.child_stack.drain(self.child_stack.len() - num_children..);
223
224 // We will be pushing the branch onto the child stack, which will require its parent
225 // extension's short key (if it has a parent extension). Calculate this short key from the
226 // `branch_path` prior to modifying the `branch_path`.
227 let short_key = trim_nibbles_prefix(
228 &self.branch_path,
229 self.branch_path.len() - branch.ext_len as usize,
230 );
231
232 // Update the branch_path. If this branch is the only branch then only its extension needs
233 // to be trimmed, otherwise we also need to remove its nibble from its parent.
234 let new_path_len = self.branch_path.len() -
235 branch.ext_len as usize -
236 if self.branch_stack.is_empty() { 0 } else { 1 };
237
238 debug_assert!(self.branch_path.len() >= new_path_len);
239 self.branch_path = self.branch_path.slice_unchecked(0, new_path_len);
240
241 // From here we will be encoding the branch node and pushing it onto the child stack,
242 // replacing its children.
243
244 // Collect children into an `RlpNode` Vec by calling into_rlp on each.
245 for child in children {
246 self.rlp_encode_buf.clear();
247 let (child_rlp_node, freed_rlp_nodes_buf) = child.into_rlp(&mut self.rlp_encode_buf)?;
248 rlp_nodes_buf.push(child_rlp_node);
249
250 // If there is an `RlpNode` buffer which can be re-used then push it onto the free-list.
251 if let Some(buf) = freed_rlp_nodes_buf {
252 self.rlp_nodes_bufs.push(buf);
253 }
254 }
255
256 debug_assert_eq!(
257 rlp_nodes_buf.len(),
258 branch.state_mask.count_ones() as usize,
259 "children length must match number of bits set in state_mask"
260 );
261
262 // Construct the `BranchNode`.
263 let branch_node = BranchNode::new(rlp_nodes_buf, branch.state_mask);
264
265 // Wrap the `BranchNode` so it can be pushed onto the child stack.
266 let branch_as_child = if short_key.is_empty() {
267 // If there is no extension then push a branch node
268 ProofTrieBranchChild::Branch(branch_node)
269 } else {
270 // Otherwise push an extension node
271 ProofTrieBranchChild::Extension { short_key, child: branch_node }
272 };
273
274 self.child_stack.push(branch_as_child);
275 Ok(())
276 }
277
278 /// Adds a single leaf for a key to the stack, possibly collapsing an existing branch and/or
279 /// creating a new one depending on the path of the key.
280 fn add_leaf(&mut self, key: Nibbles, val: VE::DeferredEncoder) -> Result<(), StateProofError> {
281 loop {
282 // Get the branch currently being built. If there are no branches on the stack then it
283 // means either the trie is empty or only a single leaf has been added previously.
284 let curr_branch = match self.branch_stack.last_mut() {
285 Some(curr_branch) => curr_branch,
286 None if self.child_stack.is_empty() => {
287 // If the child stack is empty then this is the first leaf, push it and be done
288 self.child_stack
289 .push(ProofTrieBranchChild::Leaf { short_key: key, value: val });
290 return Ok(())
291 }
292 None => {
293 // If the child stack is not empty then it must only have a single other child
294 // which is either a leaf or extension with a non-zero short key.
295 debug_assert_eq!(self.child_stack.len(), 1);
296 debug_assert!(!self
297 .child_stack
298 .last()
299 .expect("already checked for emptiness")
300 .short_key()
301 .is_empty());
302 self.push_new_branch(key, val);
303 return Ok(())
304 }
305 };
306
307 // Find the common prefix length, which is the number of nibbles shared between the
308 // current branch and the key.
309 let common_prefix_len = self.branch_path.common_prefix_length(&key);
310
311 // If the current branch does not share all of its nibbles with the new key then it is
312 // not the parent of the new key. In this case the current branch will have no more
313 // children. We can pop it and loop back to the top to try again with its parent branch.
314 if common_prefix_len < self.branch_path.len() {
315 self.pop_branch()?;
316 continue
317 }
318
319 // If the current branch is a prefix of the new key then the leaf is a child of the
320 // branch. If the branch doesn't have the leaf's nibble set then the leaf can be added
321 // directly, otherwise a new branch must be created in-between this branch and that
322 // existing child.
323 let nibble = key.get_unchecked(common_prefix_len);
324 if curr_branch.state_mask.is_bit_set(nibble) {
325 // This method will also push the new leaf onto the `child_stack`.
326 self.push_new_branch(key, val);
327 } else {
328 curr_branch.state_mask.set_bit(nibble);
329
330 // Add this leaf as a new child of the current branch (no intermediate branch
331 // needed).
332 self.child_stack.push(ProofTrieBranchChild::Leaf {
333 short_key: key.slice_unchecked(common_prefix_len + 1, key.len()),
334 value: val,
335 });
336 }
337
338 return Ok(())
339 }
340 }
341
342 /// Internal implementation of proof calculation. Assumes both cursors have already been reset.
343 /// See docs on [`Self::proof`] for expected behavior.
344 fn proof_inner(
345 &mut self,
346 value_encoder: &VE,
347 targets: impl IntoIterator<Item = Nibbles>,
348 ) -> Result<Vec<ProofTrieNode>, StateProofError> {
349 trace!(target: TRACE_TARGET, "proof_inner called");
350
351 // In debug builds, verify that targets are sorted
352 #[cfg(debug_assertions)]
353 let targets = {
354 let mut prev: Option<Nibbles> = None;
355 targets.into_iter().inspect(move |target| {
356 if let Some(prev) = prev {
357 debug_assert!(
358 prev <= *target,
359 "targets must be sorted lexicographically: {:?} > {:?}",
360 prev,
361 target
362 );
363 }
364 prev = Some(*target);
365 })
366 };
367
368 #[cfg(not(debug_assertions))]
369 let targets = targets.into_iter();
370
371 // Ensure initial state is cleared. By the end of the method call these should be empty once
372 // again.
373 debug_assert!(self.branch_stack.is_empty());
374 debug_assert!(self.branch_path.is_empty());
375 debug_assert!(self.child_stack.is_empty());
376
377 // Silence unused variable warning for now
378 let _ = targets;
379
380 let mut proof_nodes = Vec::new();
381 let mut hashed_cursor_current = self.hashed_cursor.seek(B256::ZERO)?;
382 loop {
383 trace!(target: TRACE_TARGET, ?hashed_cursor_current, "proof_inner loop");
384
385 // Fetch the next leaf from the hashed cursor, converting the key to Nibbles and
386 // immediately creating the DeferredValueEncoder so that encoding of the leaf value can
387 // begin ASAP.
388 let Some((key, val)) = hashed_cursor_current.map(|(key_b256, val)| {
389 debug_assert_eq!(key_b256.len(), 32);
390 // SAFETY: key is a B256 and so is exactly 32-bytes.
391 let key = unsafe { Nibbles::unpack_unchecked(key_b256.as_slice()) };
392 let val = value_encoder.deferred_encoder(key_b256, val);
393 (key, val)
394 }) else {
395 break
396 };
397
398 self.add_leaf(key, val)?;
399 hashed_cursor_current = self.hashed_cursor.next()?;
400 }
401
402 // Once there's no more leaves we can pop the remaining branches, if any.
403 while !self.branch_stack.is_empty() {
404 self.pop_branch()?;
405 }
406
407 // At this point the branch stack should be empty. If the child stack is empty it means no
408 // keys were ever iterated from the hashed cursor in the first place. Otherwise there should
409 // only be a single node left: the root node.
410 debug_assert!(self.branch_stack.is_empty());
411 debug_assert!(self.branch_path.is_empty());
412 debug_assert!(self.child_stack.len() < 2);
413
414 // Determine the root node based on the child stack, and push the proof of the root node
415 // onto the result stack.
416 let root_node = if let Some(node) = self.child_stack.pop() {
417 self.rlp_encode_buf.clear();
418 node.into_trie_node(&mut self.rlp_encode_buf)?
419 } else {
420 TrieNode::EmptyRoot
421 };
422
423 proof_nodes.push(ProofTrieNode {
424 path: Nibbles::new(), // root path
425 node: root_node,
426 masks: TrieMasks::none(),
427 });
428
429 Ok(proof_nodes)
430 }
431}
432
433impl<TC, HC, VE> ProofCalculator<TC, HC, VE>
434where
435 TC: TrieCursor,
436 HC: HashedCursor,
437 VE: LeafValueEncoder<Value = HC::Value>,
438{
439 /// Generate a proof for the given targets.
440 ///
441 /// Given lexicographically sorted targets, returns nodes whose paths are a prefix of any
442 /// target. The returned nodes will be sorted lexicographically by path.
443 ///
444 /// # Panics
445 ///
446 /// In debug builds, panics if the targets are not sorted lexicographically.
447 #[instrument(target = TRACE_TARGET, level = "trace", skip_all)]
448 pub fn proof(
449 &mut self,
450 value_encoder: &VE,
451 targets: impl IntoIterator<Item = Nibbles>,
452 ) -> Result<Vec<ProofTrieNode>, StateProofError> {
453 self.trie_cursor.reset();
454 self.hashed_cursor.reset();
455 self.proof_inner(value_encoder, targets)
456 }
457}
458
459/// A proof calculator for storage tries.
460pub type StorageProofCalculator<TC, HC> = ProofCalculator<TC, HC, StorageValueEncoder>;
461
462impl<TC, HC> StorageProofCalculator<TC, HC>
463where
464 TC: TrieStorageCursor,
465 HC: HashedStorageCursor<Value = U256>,
466{
467 /// Create a new [`StorageProofCalculator`] instance.
468 pub const fn new_storage(trie_cursor: TC, hashed_cursor: HC) -> Self {
469 Self::new(trie_cursor, hashed_cursor)
470 }
471
472 /// Generate a proof for a storage trie at the given hashed address.
473 ///
474 /// Given lexicographically sorted targets, returns nodes whose paths are a prefix of any
475 /// target. The returned nodes will be sorted lexicographically by path.
476 ///
477 /// # Panics
478 ///
479 /// In debug builds, panics if the targets are not sorted lexicographically.
480 #[instrument(target = TRACE_TARGET, level = "trace", skip(self, targets))]
481 pub fn storage_proof(
482 &mut self,
483 hashed_address: B256,
484 targets: impl IntoIterator<Item = Nibbles>,
485 ) -> Result<Vec<ProofTrieNode>, StateProofError> {
486 /// Static storage value encoder instance used by all storage proofs.
487 static STORAGE_VALUE_ENCODER: StorageValueEncoder = StorageValueEncoder;
488
489 self.hashed_cursor.set_hashed_address(hashed_address);
490
491 // Shortcut: check if storage is empty
492 if self.hashed_cursor.is_storage_empty()? {
493 // Return a single EmptyRoot node at the root path
494 return Ok(vec![ProofTrieNode {
495 path: Nibbles::default(),
496 node: TrieNode::EmptyRoot,
497 masks: TrieMasks::none(),
498 }])
499 }
500
501 // Don't call `set_hashed_address` on the trie cursor until after the previous shortcut has
502 // been checked.
503 self.trie_cursor.set_hashed_address(hashed_address);
504
505 // Use the static StorageValueEncoder and pass it to proof_inner
506 self.proof_inner(&STORAGE_VALUE_ENCODER, targets)
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::{
514 hashed_cursor::{mock::MockHashedCursorFactory, HashedCursorFactory},
515 proof::Proof,
516 trie_cursor::{mock::MockTrieCursorFactory, TrieCursorFactory},
517 };
518 use alloy_primitives::map::B256Map;
519 use alloy_rlp::Decodable;
520 use itertools::Itertools;
521 use reth_trie_common::{HashedPostState, MultiProofTargets};
522 use std::collections::BTreeMap;
523
524 /// Target to use with the `tracing` crate.
525 static TRACE_TARGET: &str = "trie::proof_v2::tests";
526
527 /// A test harness for comparing `ProofCalculator` and legacy `Proof` implementations.
528 ///
529 /// This harness creates mock cursor factories from a `HashedPostState` and provides
530 /// a method to test that both proof implementations produce equivalent results.
531 struct ProofTestHarness {
532 /// Mock factory for trie cursors (empty by default for leaf-only tests)
533 trie_cursor_factory: MockTrieCursorFactory,
534 /// Mock factory for hashed cursors, populated from `HashedPostState`
535 hashed_cursor_factory: MockHashedCursorFactory,
536 }
537
538 impl ProofTestHarness {
539 /// Creates a new test harness from a `HashedPostState`.
540 ///
541 /// The `HashedPostState` is used to populate the mock hashed cursor factory directly.
542 /// The trie cursor factory is empty by default, suitable for testing the leaf-only
543 /// proof calculator.
544 fn new(post_state: HashedPostState) -> Self {
545 trace!(target: TRACE_TARGET, ?post_state, "Creating ProofTestHarness");
546
547 // Extract accounts from post state, filtering out None (deleted accounts)
548 let hashed_accounts: BTreeMap<B256, _> = post_state
549 .accounts
550 .into_iter()
551 .filter_map(|(addr, account)| account.map(|acc| (addr, acc)))
552 .collect();
553
554 // Extract storage tries from post state
555 let hashed_storage_tries: B256Map<BTreeMap<B256, U256>> = post_state
556 .storages
557 .into_iter()
558 .map(|(addr, hashed_storage)| {
559 // Convert HashedStorage to BTreeMap, filtering out zero values (deletions)
560 let storage_map: BTreeMap<B256, U256> = hashed_storage
561 .storage
562 .into_iter()
563 .filter_map(|(slot, value)| (value != U256::ZERO).then_some((slot, value)))
564 .collect();
565 (addr, storage_map)
566 })
567 .collect();
568
569 // Ensure that there's a storage trie dataset for every storage trie, even if empty.
570 let storage_trie_nodes: B256Map<BTreeMap<_, _>> = hashed_storage_tries
571 .keys()
572 .copied()
573 .map(|addr| (addr, Default::default()))
574 .collect();
575
576 // Create mock hashed cursor factory populated with the post state data
577 let hashed_cursor_factory =
578 MockHashedCursorFactory::new(hashed_accounts, hashed_storage_tries);
579
580 // Create empty trie cursor factory (leaf-only calculator doesn't need trie nodes)
581 let trie_cursor_factory =
582 MockTrieCursorFactory::new(BTreeMap::new(), storage_trie_nodes);
583
584 Self { trie_cursor_factory, hashed_cursor_factory }
585 }
586
587 /// Asserts that `ProofCalculator` and legacy `Proof` produce equivalent results for account
588 /// proofs.
589 ///
590 /// This method calls both implementations with the given account targets and compares
591 /// the results. For now, it performs a basic comparison by checking that both succeed
592 /// and produce non-empty results. More detailed comparison logic can be added as needed.
593 fn assert_proof(
594 &self,
595 // For now ProofCalculator doesn't support real targets, we just compare calculated
596 // roots.
597 _targets: impl IntoIterator<Item = B256> + Clone,
598 ) -> Result<(), StateProofError> {
599 // Create ProofCalculator (proof_v2) with account cursors
600 let trie_cursor = self.trie_cursor_factory.account_trie_cursor()?;
601 let hashed_cursor = self.hashed_cursor_factory.hashed_account_cursor()?;
602
603 // Call ProofCalculator::proof with account targets
604 let value_encoder = SyncAccountValueEncoder::new(
605 self.trie_cursor_factory.clone(),
606 self.hashed_cursor_factory.clone(),
607 );
608 let mut proof_calculator = ProofCalculator::new(trie_cursor, hashed_cursor);
609 let proof_v2_result = proof_calculator.proof(&value_encoder, [Nibbles::new()])?;
610
611 // Call Proof::multiproof (legacy implementation)
612 let proof_legacy_result =
613 Proof::new(self.trie_cursor_factory.clone(), self.hashed_cursor_factory.clone())
614 .multiproof(MultiProofTargets::default())?;
615
616 // Decode and sort legacy proof nodes
617 let proof_legacy_nodes = proof_legacy_result
618 .account_subtree
619 .iter()
620 .map(|(path, node_enc)| {
621 let mut buf = node_enc.as_ref();
622 let node = TrieNode::decode(&mut buf)
623 .expect("legacy implementation should not produce malformed proof nodes");
624
625 ProofTrieNode {
626 path: *path,
627 node,
628 masks: TrieMasks {
629 hash_mask: proof_legacy_result
630 .branch_node_hash_masks
631 .get(path)
632 .copied(),
633 tree_mask: proof_legacy_result
634 .branch_node_tree_masks
635 .get(path)
636 .copied(),
637 },
638 }
639 })
640 .sorted_by_key(|n| n.path)
641 .collect::<Vec<_>>();
642
643 // Basic comparison: both should succeed and produce identical results
644 assert_eq!(proof_legacy_nodes, proof_v2_result);
645
646 Ok(())
647 }
648 }
649
650 mod proptest_tests {
651 use super::*;
652 use alloy_primitives::{map::B256Map, U256};
653 use proptest::prelude::*;
654 use reth_primitives_traits::Account;
655 use reth_trie_common::HashedPostState;
656
657 /// Generate a strategy for Account values
658 fn account_strategy() -> impl Strategy<Value = Account> {
659 (any::<u64>(), any::<u64>(), any::<[u8; 32]>()).prop_map(
660 |(nonce, balance, code_hash)| Account {
661 nonce,
662 balance: U256::from(balance),
663 bytecode_hash: Some(B256::from(code_hash)),
664 },
665 )
666 }
667
668 /// Generate a strategy for `HashedPostState` with random accounts
669 fn hashed_post_state_strategy() -> impl Strategy<Value = HashedPostState> {
670 prop::collection::vec((any::<[u8; 32]>(), account_strategy()), 0..20).prop_map(
671 |accounts| {
672 let account_map = accounts
673 .into_iter()
674 .map(|(addr_bytes, account)| (B256::from(addr_bytes), Some(account)))
675 .collect::<B256Map<_>>();
676
677 // All accounts have empty storages.
678 let storages = account_map
679 .keys()
680 .copied()
681 .map(|addr| (addr, Default::default()))
682 .collect::<B256Map<_>>();
683
684 HashedPostState { accounts: account_map, storages }
685 },
686 )
687 }
688
689 proptest! {
690 #![proptest_config(ProptestConfig::with_cases(5000))]
691
692 /// Tests that ProofCalculator produces valid proofs for randomly generated
693 /// HashedPostState with empty target sets.
694 ///
695 /// This test:
696 /// - Generates random accounts in a HashedPostState
697 /// - Creates a test harness with the generated state
698 /// - Calls assert_proof with an empty target set
699 /// - Verifies both ProofCalculator and legacy Proof succeed
700 #[test]
701 fn proptest_proof_with_empty_targets(
702 post_state in hashed_post_state_strategy(),
703 ) {
704 reth_tracing::init_test_tracing();
705 let harness = ProofTestHarness::new(post_state);
706
707 // Pass empty target set
708 harness.assert_proof(std::iter::empty()).expect("Proof generation failed");
709 }
710 }
711 }
712}