Skip to main content

reth_trie/trie_cursor/
masked.rs

1use super::{TrieCursor, TrieCursorFactory, TrieStorageCursor};
2use alloy_primitives::{map::B256Map, B256};
3use reth_storage_errors::db::DatabaseError;
4use reth_trie_common::{
5    prefix_set::{PrefixSet, TriePrefixSets},
6    BranchNodeCompact, Nibbles,
7};
8use std::sync::Arc;
9
10/// A [`TrieCursorFactory`] wrapper that creates cursors which invalidate cached trie hash data
11/// for children whose paths match the prefix sets in a [`TriePrefixSets`].
12///
13/// The `destroyed_accounts` field of the prefix sets is not used by the cursor — it is only
14/// relevant during trie update finalization, not during cursor traversal.
15#[derive(Debug, Clone)]
16pub struct MaskedTrieCursorFactory<CF> {
17    /// Underlying trie cursor factory.
18    cursor_factory: CF,
19    /// Frozen prefix sets used for masking.
20    prefix_sets: TriePrefixSets,
21}
22
23impl<CF> MaskedTrieCursorFactory<CF> {
24    /// Create a new factory from an inner cursor factory and frozen prefix sets.
25    pub const fn new(cursor_factory: CF, prefix_sets: TriePrefixSets) -> Self {
26        Self { cursor_factory, prefix_sets }
27    }
28}
29
30impl<CF: TrieCursorFactory> TrieCursorFactory for MaskedTrieCursorFactory<CF> {
31    type AccountTrieCursor<'a>
32        = MaskedTrieCursor<CF::AccountTrieCursor<'a>>
33    where
34        Self: 'a;
35
36    type StorageTrieCursor<'a>
37        = MaskedTrieCursor<CF::StorageTrieCursor<'a>>
38    where
39        Self: 'a;
40
41    fn account_trie_cursor(&self) -> Result<Self::AccountTrieCursor<'_>, DatabaseError> {
42        let cursor = self.cursor_factory.account_trie_cursor()?;
43        Ok(MaskedTrieCursor::new(cursor, self.prefix_sets.account_prefix_set.clone()))
44    }
45
46    fn storage_trie_cursor(
47        &self,
48        hashed_address: B256,
49    ) -> Result<Self::StorageTrieCursor<'_>, DatabaseError> {
50        let cursor = self.cursor_factory.storage_trie_cursor(hashed_address)?;
51        let prefix_set =
52            self.prefix_sets.storage_prefix_sets.get(&hashed_address).cloned().unwrap_or_default();
53        Ok(MaskedTrieCursor::new_storage(
54            cursor,
55            prefix_set,
56            self.prefix_sets.storage_prefix_sets.clone(),
57        ))
58    }
59}
60
61/// A [`TrieCursor`] wrapper that invalidates cached trie hash data for children whose paths match
62/// a [`PrefixSet`].
63///
64/// For each node returned by the inner cursor, hash bits are unset for children whose paths match
65/// the prefix set, and the corresponding hashes are removed from the node. If a node's `hash_mask`
66/// and `tree_mask` are both empty after masking, the node is skipped entirely.
67#[derive(Debug)]
68pub struct MaskedTrieCursor<C> {
69    /// The inner cursor.
70    cursor: C,
71    /// Prefix set used to determine which children's hashes to invalidate.
72    prefix_set: PrefixSet,
73    /// Storage prefix sets for swapping on `set_hashed_address`.
74    storage_prefix_sets: Option<B256Map<PrefixSet>>,
75}
76
77impl<C> MaskedTrieCursor<C> {
78    /// Create a new cursor wrapping `cursor`, masking hash bits for children whose paths match
79    /// `prefix_set`.
80    pub const fn new(cursor: C, prefix_set: PrefixSet) -> Self {
81        Self { cursor, prefix_set, storage_prefix_sets: None }
82    }
83
84    /// Create a new storage cursor that can swap its prefix set on `set_hashed_address`.
85    pub const fn new_storage(
86        cursor: C,
87        prefix_set: PrefixSet,
88        storage_prefix_sets: B256Map<PrefixSet>,
89    ) -> Self {
90        Self { cursor, prefix_set, storage_prefix_sets: Some(storage_prefix_sets) }
91    }
92}
93
94impl<C: TrieCursor> MaskedTrieCursor<C> {
95    /// Mask hash bits on a node for children whose paths match the prefix set.
96    ///
97    /// Returns `true` if the node should be kept, `false` if it should be skipped (both
98    /// `hash_mask` and `tree_mask` are empty after masking).
99    fn mask_node(&mut self, key: &Nibbles, node: &mut BranchNodeCompact) -> bool {
100        if !self.prefix_set.contains(key) {
101            return true;
102        }
103
104        // The subtree is modified — root hash is always invalid.
105        node.root_hash = None;
106
107        let original_hash_mask = node.hash_mask;
108        if original_hash_mask.is_empty() {
109            return true;
110        }
111
112        let mut new_hash_mask = original_hash_mask;
113        let mut child_path = *key;
114        let key_len = key.len();
115
116        for nibble in original_hash_mask.iter() {
117            child_path.truncate(key_len);
118            child_path.push(nibble);
119
120            if self.prefix_set.contains(&child_path) {
121                new_hash_mask.unset_bit(nibble);
122            }
123        }
124
125        if new_hash_mask != original_hash_mask {
126            // Remove hashes for unset bits in-place.
127            let hashes = Arc::make_mut(&mut node.hashes);
128            let mut write = 0;
129            for (read, nibble) in original_hash_mask.iter().enumerate() {
130                if new_hash_mask.is_bit_set(nibble) {
131                    hashes[write] = hashes[read];
132                    write += 1;
133                }
134            }
135            hashes.truncate(write);
136
137            node.hash_mask = new_hash_mask;
138
139            if node.hash_mask.is_empty() && node.tree_mask.is_empty() {
140                return false;
141            }
142        }
143
144        true
145    }
146
147    /// Apply masking to entries, advancing past fully-masked nodes.
148    fn mask_entries(
149        &mut self,
150        mut entry: Option<(Nibbles, BranchNodeCompact)>,
151    ) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
152        while let Some((key, mut node)) = entry {
153            if self.mask_node(&key, &mut node) {
154                return Ok(Some((key, node)));
155            }
156            entry = self.cursor.next()?;
157        }
158        Ok(None)
159    }
160}
161
162impl<C: TrieCursor> TrieCursor for MaskedTrieCursor<C> {
163    fn seek_exact(
164        &mut self,
165        key: Nibbles,
166    ) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
167        if let Some((key, mut node)) = self.cursor.seek_exact(key)? {
168            if self.mask_node(&key, &mut node) {
169                Ok(Some((key, node)))
170            } else {
171                Ok(None)
172            }
173        } else {
174            Ok(None)
175        }
176    }
177
178    fn seek(
179        &mut self,
180        key: Nibbles,
181    ) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
182        let entry = self.cursor.seek(key)?;
183        self.mask_entries(entry)
184    }
185
186    fn next(&mut self) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
187        let entry = self.cursor.next()?;
188        self.mask_entries(entry)
189    }
190
191    fn current(&mut self) -> Result<Option<Nibbles>, DatabaseError> {
192        self.cursor.current()
193    }
194
195    fn reset(&mut self) {
196        self.cursor.reset();
197    }
198}
199
200impl<C: TrieStorageCursor> TrieStorageCursor for MaskedTrieCursor<C> {
201    fn set_hashed_address(&mut self, hashed_address: B256) {
202        self.cursor.set_hashed_address(hashed_address);
203        if let Some(storage_prefix_sets) = &self.storage_prefix_sets {
204            self.prefix_set = storage_prefix_sets.get(&hashed_address).cloned().unwrap_or_default();
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use crate::trie_cursor::mock::MockTrieCursor;
213    use parking_lot::Mutex;
214    use reth_trie_common::prefix_set::PrefixSetMut;
215    use std::{collections::BTreeMap, sync::Arc};
216
217    fn make_cursor(nodes: Vec<(Nibbles, BranchNodeCompact)>) -> MockTrieCursor {
218        let map: BTreeMap<Nibbles, BranchNodeCompact> = nodes.into_iter().collect();
219        MockTrieCursor::new(Arc::new(map), Arc::new(Mutex::new(Vec::new())))
220    }
221
222    fn node(state_mask: u16) -> BranchNodeCompact {
223        BranchNodeCompact::new(state_mask, 0, 0, vec![], None)
224    }
225
226    fn node_with_hashes(state_mask: u16, hash_mask: u16, hashes: Vec<B256>) -> BranchNodeCompact {
227        BranchNodeCompact::new(state_mask, 0, hash_mask, hashes, None)
228    }
229
230    fn node_with_tree_mask(
231        state_mask: u16,
232        tree_mask: u16,
233        hash_mask: u16,
234        hashes: Vec<B256>,
235    ) -> BranchNodeCompact {
236        BranchNodeCompact::new(state_mask, tree_mask, hash_mask, hashes, None)
237    }
238
239    fn hash(byte: u8) -> B256 {
240        B256::repeat_byte(byte)
241    }
242
243    #[test]
244    fn test_seek_masks_matching_child_hashes() {
245        // Node at [0x1] with children 2 and 5 hashed.
246        // Prefix set marks child 2 as changed.
247        let nodes = vec![(
248            Nibbles::from_nibbles([0x1]),
249            node_with_hashes(0b0000_0000_0010_0100, 0b0000_0000_0010_0100, vec![hash(2), hash(5)]),
250        )];
251
252        let mut ps = PrefixSetMut::default();
253        ps.insert(Nibbles::from_nibbles([0x1, 0x2]));
254
255        let inner = make_cursor(nodes);
256        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
257
258        let result = cursor.seek(Nibbles::default()).unwrap();
259        let (key, node) = result.unwrap();
260        assert_eq!(key, Nibbles::from_nibbles([0x1]));
261        // Hash bit 2 should be unset, only bit 5 remains.
262        assert!(!node.hash_mask.is_bit_set(2));
263        assert!(node.hash_mask.is_bit_set(5));
264        assert_eq!(&*node.hashes, &[hash(5)]);
265    }
266
267    #[test]
268    fn test_seek_skips_fully_masked_node() {
269        // Node at [0x1] with only child 3 hashed, tree_mask empty.
270        // Prefix set marks child 3 as changed → fully masked → skipped.
271        // Node at [0x2] is unaffected → returned.
272        let nodes = vec![
273            (
274                Nibbles::from_nibbles([0x1]),
275                node_with_hashes(0b0000_0000_0000_1000, 0b0000_0000_0000_1000, vec![hash(3)]),
276            ),
277            (Nibbles::from_nibbles([0x2]), node(0b0000_0000_0000_0001)),
278        ];
279
280        let mut ps = PrefixSetMut::default();
281        ps.insert(Nibbles::from_nibbles([0x1, 0x3]));
282
283        let inner = make_cursor(nodes);
284        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
285
286        let result = cursor.seek(Nibbles::default()).unwrap();
287        assert_eq!(result, Some((Nibbles::from_nibbles([0x2]), node(0b0000_0000_0000_0001))));
288    }
289
290    #[test]
291    fn test_node_with_tree_mask_not_skipped() {
292        // Node at [0x1] with child 3 hashed, tree_mask has bit 3 set.
293        // Prefix set marks child 3 → hash cleared, but tree_mask keeps the node alive.
294        let nodes = vec![(
295            Nibbles::from_nibbles([0x1]),
296            node_with_tree_mask(
297                0b0000_0000_0000_1000,
298                0b0000_0000_0000_1000,
299                0b0000_0000_0000_1000,
300                vec![hash(3)],
301            ),
302        )];
303
304        let mut ps = PrefixSetMut::default();
305        ps.insert(Nibbles::from_nibbles([0x1, 0x3]));
306
307        let inner = make_cursor(nodes);
308        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
309
310        let result = cursor.seek(Nibbles::default()).unwrap();
311        let (key, node) = result.unwrap();
312        assert_eq!(key, Nibbles::from_nibbles([0x1]));
313        assert!(node.hash_mask.is_empty());
314        assert!(node.tree_mask.is_bit_set(3));
315        assert!(node.hashes.is_empty());
316    }
317
318    #[test]
319    fn test_seek_exact_masks_hash_bits() {
320        let nodes = vec![(
321            Nibbles::from_nibbles([0x1]),
322            node_with_tree_mask(
323                0b0000_0000_0010_0100,
324                0b0000_0000_0010_0100,
325                0b0000_0000_0010_0100,
326                vec![hash(2), hash(5)],
327            ),
328        )];
329
330        let mut ps = PrefixSetMut::default();
331        ps.insert(Nibbles::from_nibbles([0x1, 0x5]));
332
333        let inner = make_cursor(nodes);
334        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
335
336        let result = cursor.seek_exact(Nibbles::from_nibbles([0x1])).unwrap();
337        let (_, node) = result.unwrap();
338        assert!(node.hash_mask.is_bit_set(2));
339        assert!(!node.hash_mask.is_bit_set(5));
340        assert_eq!(&*node.hashes, &[hash(2)]);
341    }
342
343    #[test]
344    fn test_seek_exact_returns_none_for_fully_masked() {
345        let nodes = vec![(
346            Nibbles::from_nibbles([0x1]),
347            node_with_hashes(0b0000_0000_0000_0100, 0b0000_0000_0000_0100, vec![hash(2)]),
348        )];
349
350        let mut ps = PrefixSetMut::default();
351        ps.insert(Nibbles::from_nibbles([0x1, 0x2]));
352
353        let inner = make_cursor(nodes);
354        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
355
356        let result = cursor.seek_exact(Nibbles::from_nibbles([0x1])).unwrap();
357        assert_eq!(result, None);
358    }
359
360    #[test]
361    fn test_next_masks_and_skips() {
362        // Three nodes: [0x1] unaffected, [0x2] fully masked, [0x3] unaffected.
363        let nodes = vec![
364            (
365                Nibbles::from_nibbles([0x1]),
366                node_with_hashes(0b0000_0000_0000_0010, 0b0000_0000_0000_0010, vec![hash(1)]),
367            ),
368            (
369                Nibbles::from_nibbles([0x2]),
370                node_with_hashes(0b0000_0000_0001_0000, 0b0000_0000_0001_0000, vec![hash(4)]),
371            ),
372            (
373                Nibbles::from_nibbles([0x3]),
374                node_with_hashes(0b0000_0000_0100_0000, 0b0000_0000_0100_0000, vec![hash(6)]),
375            ),
376        ];
377
378        let mut ps = PrefixSetMut::default();
379        ps.insert(Nibbles::from_nibbles([0x2, 0x4]));
380
381        let inner = make_cursor(nodes);
382        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
383
384        // seek to [0x1], no match → returned unchanged.
385        let result = cursor.seek(Nibbles::from_nibbles([0x1])).unwrap();
386        let (key, node) = result.unwrap();
387        assert_eq!(key, Nibbles::from_nibbles([0x1]));
388        assert_eq!(&*node.hashes, &[hash(1)]);
389
390        // next() should skip [0x2] (fully masked), returning [0x3].
391        let result = cursor.next().unwrap();
392        let (key, node) = result.unwrap();
393        assert_eq!(key, Nibbles::from_nibbles([0x3]));
394        assert_eq!(&*node.hashes, &[hash(6)]);
395    }
396
397    #[test]
398    fn test_no_match_returns_unchanged() {
399        let nodes = vec![(
400            Nibbles::from_nibbles([0x2]),
401            node_with_hashes(0b0000_0000_0000_0010, 0b0000_0000_0000_0010, vec![hash(1)]),
402        )];
403
404        let mut ps = PrefixSetMut::default();
405        ps.insert(Nibbles::from_nibbles([0x1, 0x3]));
406
407        let inner = make_cursor(nodes);
408        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
409
410        let result = cursor.seek(Nibbles::default()).unwrap();
411        let (key, node) = result.unwrap();
412        assert_eq!(key, Nibbles::from_nibbles([0x2]));
413        // Unchanged — prefix set doesn't match [0x2].
414        assert!(node.hash_mask.is_bit_set(1));
415        assert_eq!(&*node.hashes, &[hash(1)]);
416    }
417
418    #[test]
419    fn test_empty_prefix_set_returns_all_unchanged() {
420        let h1 = hash(1);
421        let h2 = hash(2);
422        let nodes = vec![
423            (
424                Nibbles::from_nibbles([0x1]),
425                node_with_hashes(0b0000_0000_0000_0010, 0b0000_0000_0000_0010, vec![h1]),
426            ),
427            (
428                Nibbles::from_nibbles([0x2]),
429                node_with_hashes(0b0000_0000_0000_0100, 0b0000_0000_0000_0100, vec![h2]),
430            ),
431        ];
432
433        let ps = PrefixSetMut::default();
434        let inner = make_cursor(nodes);
435        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
436
437        let r1 = cursor.seek(Nibbles::default()).unwrap().unwrap();
438        assert_eq!(r1.0, Nibbles::from_nibbles([0x1]));
439        assert_eq!(&*r1.1.hashes, &[h1]);
440
441        let r2 = cursor.next().unwrap().unwrap();
442        assert_eq!(r2.0, Nibbles::from_nibbles([0x2]));
443        assert_eq!(&*r2.1.hashes, &[h2]);
444
445        assert_eq!(cursor.next().unwrap(), None);
446    }
447
448    #[test]
449    fn test_root_hash_cleared_on_mask() {
450        let mut n =
451            node_with_hashes(0b0000_0000_0010_0100, 0b0000_0000_0010_0100, vec![hash(2), hash(5)]);
452        n.root_hash = Some(hash(0xFF));
453
454        let nodes = vec![(Nibbles::from_nibbles([0x1]), n)];
455
456        let mut ps = PrefixSetMut::default();
457        ps.insert(Nibbles::from_nibbles([0x1, 0x2]));
458
459        let inner = make_cursor(nodes);
460        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
461
462        let (_, node) = cursor.seek(Nibbles::default()).unwrap().unwrap();
463        assert_eq!(node.root_hash, None);
464    }
465
466    #[test]
467    fn test_node_without_hashes_returned_unchanged() {
468        // Node with state_mask only (no hashes, no tree_mask) should pass through.
469        let nodes = vec![(Nibbles::from_nibbles([0x1]), node(0b0000_0000_0000_0011))];
470
471        let mut ps = PrefixSetMut::default();
472        ps.insert(Nibbles::from_nibbles([0x1, 0x0]));
473
474        let inner = make_cursor(nodes);
475        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
476
477        let result = cursor.seek(Nibbles::default()).unwrap();
478        assert_eq!(result, Some((Nibbles::from_nibbles([0x1]), node(0b0000_0000_0000_0011))));
479    }
480
481    #[test]
482    fn test_empty_cursor_returns_none() {
483        let nodes = vec![];
484        let ps = PrefixSetMut::default();
485        let inner = make_cursor(nodes);
486        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
487
488        assert_eq!(cursor.seek(Nibbles::default()).unwrap(), None);
489    }
490
491    #[test]
492    fn test_reset_delegates() {
493        let nodes =
494            vec![(Nibbles::from_nibbles([0x1]), node(1)), (Nibbles::from_nibbles([0x2]), node(2))];
495
496        let ps = PrefixSetMut::default();
497        let inner = make_cursor(nodes);
498        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
499
500        let _ = cursor.seek(Nibbles::from_nibbles([0x1])).unwrap();
501        assert_eq!(cursor.current().unwrap(), Some(Nibbles::from_nibbles([0x1])));
502
503        cursor.reset();
504        assert_eq!(cursor.current().unwrap(), None);
505    }
506
507    #[test]
508    fn test_partial_mask_preserves_remaining_hashes() {
509        // Node at [0x1] with children 0, 3, 7 hashed.
510        // Prefix set marks children 0 and 7 as changed.
511        // Only hash for child 3 should remain.
512        let nodes = vec![(
513            Nibbles::from_nibbles([0x1]),
514            node_with_tree_mask(
515                0b0000_0000_1000_1001,
516                0b0000_0000_1000_1001,
517                0b0000_0000_1000_1001,
518                vec![hash(0), hash(3), hash(7)],
519            ),
520        )];
521
522        let mut ps = PrefixSetMut::default();
523        ps.insert(Nibbles::from_nibbles([0x1, 0x0]));
524        ps.insert(Nibbles::from_nibbles([0x1, 0x7]));
525
526        let inner = make_cursor(nodes);
527        let mut cursor = MaskedTrieCursor::new(inner, ps.freeze());
528
529        let (key, node) = cursor.seek(Nibbles::default()).unwrap().unwrap();
530        assert_eq!(key, Nibbles::from_nibbles([0x1]));
531        assert!(!node.hash_mask.is_bit_set(0));
532        assert!(node.hash_mask.is_bit_set(3));
533        assert!(!node.hash_mask.is_bit_set(7));
534        assert_eq!(&*node.hashes, &[hash(3)]);
535        assert_eq!(node.root_hash, None);
536    }
537
538    mod proptest_tests {
539        use crate::{
540            hashed_cursor::{mock::MockHashedCursorFactory, HashedPostStateCursorFactory},
541            proof::Proof,
542            trie_cursor::{
543                masked::MaskedTrieCursorFactory, mock::MockTrieCursorFactory,
544                noop::NoopTrieCursorFactory,
545            },
546            StateRoot,
547        };
548        use alloy_primitives::{map::B256Set, B256, U256};
549        use proptest::prelude::*;
550        use reth_primitives_traits::Account;
551        use reth_trie_common::{HashedPostState, HashedStorage, MultiProofTargets};
552
553        fn account_strategy() -> impl Strategy<Value = Account> {
554            (any::<u64>(), any::<u64>(), any::<[u8; 32]>()).prop_map(
555                |(nonce, balance, code_hash)| Account {
556                    nonce,
557                    balance: U256::from(balance),
558                    bytecode_hash: Some(B256::from(code_hash)),
559                },
560            )
561        }
562
563        fn storage_value_strategy() -> impl Strategy<Value = U256> {
564            any::<u64>().prop_filter("non-zero", |v| *v != 0).prop_map(U256::from)
565        }
566
567        /// Generates a base dataset of 1000 storage slots for account `B256::ZERO`,
568        /// a 200-entry changeset partially overlapping with the base, and random
569        /// proof targets partially overlapping with both.
570        #[allow(clippy::type_complexity)]
571        fn test_input_strategy(
572        ) -> impl Strategy<Value = (Vec<(B256, U256)>, Account, Vec<(B256, Option<U256>)>, Vec<B256>)>
573        {
574            (
575                // 1000 base storage slots: unique keys with non-zero values
576                prop::collection::vec(
577                    (any::<[u8; 32]>().prop_map(B256::from), storage_value_strategy()),
578                    1000,
579                ),
580                account_strategy(),
581                // 200 changeset entries: (key, Option<value>) where None = removal
582                prop::collection::vec(
583                    (
584                        any::<[u8; 32]>().prop_map(B256::from),
585                        prop::option::of(storage_value_strategy()),
586                    ),
587                    200,
588                ),
589                // Extra random keys for proof targets
590                prop::collection::vec(any::<[u8; 32]>().prop_map(B256::from), 50),
591            )
592                .prop_flat_map(
593                    |(base_slots, account, changeset_raw, extra_targets)| {
594                        // Dedup base slots by key
595                        let mut base_map = alloy_primitives::map::B256Map::default();
596                        for (k, v) in &base_slots {
597                            base_map.insert(*k, *v);
598                        }
599                        let base_deduped: Vec<(B256, U256)> =
600                            base_map.iter().map(|(&k, &v)| (k, v)).collect();
601                        let base_keys: Vec<B256> = base_deduped.iter().map(|(k, _)| *k).collect();
602
603                        // Build changeset: 50% overlap with base keys, 50% new keys
604                        let changeset_len = changeset_raw.len();
605                        let half = changeset_len / 2;
606                        let base_keys_for_overlap = base_keys.clone();
607
608                        // Use indices to select from base keys for overlap portion
609                        let overlap_indices =
610                            prop::collection::vec(0..base_keys_for_overlap.len().max(1), half);
611
612                        overlap_indices.prop_map(move |indices| {
613                            let mut changeset: Vec<(B256, Option<U256>)> = Vec::new();
614
615                            // First half: overlapping with base keys
616                            for (i, (_, value)) in
617                                indices.iter().zip(changeset_raw.iter()).take(half)
618                            {
619                                let key = if base_keys_for_overlap.is_empty() {
620                                    changeset_raw[*i].0
621                                } else {
622                                    base_keys_for_overlap[*i % base_keys_for_overlap.len()]
623                                };
624                                changeset.push((key, *value));
625                            }
626
627                            // Second half: new keys from changeset_raw
628                            for (key, value) in changeset_raw.iter().skip(half) {
629                                changeset.push((*key, *value));
630                            }
631
632                            // Build proof targets: mix of base keys, changeset keys, and randoms
633                            let changeset_keys: Vec<B256> =
634                                changeset.iter().map(|(k, _)| *k).collect();
635                            let mut proof_slot_targets: Vec<B256> = Vec::new();
636
637                            // ~40% from base
638                            for k in base_keys.iter().take(40) {
639                                proof_slot_targets.push(*k);
640                            }
641                            // ~30% from changeset
642                            for k in changeset_keys.iter().take(30) {
643                                proof_slot_targets.push(*k);
644                            }
645                            // ~30% random
646                            for k in extra_targets.iter().take(30) {
647                                proof_slot_targets.push(*k);
648                            }
649
650                            (base_deduped.clone(), account, changeset, proof_slot_targets)
651                        })
652                    },
653                )
654        }
655
656        proptest! {
657            #![proptest_config(ProptestConfig::with_cases(50))]
658            #[test]
659            fn proptest_masked_cursor_multiproof_equivalence(
660                (base_slots, account, changeset, proof_slot_targets) in test_input_strategy()
661            ) {
662                reth_tracing::init_test_tracing();
663
664                let hashed_address = B256::ZERO;
665
666                // Step 1: Create the base hashed post state with a single account
667                // and 1000 storage slots.
668                let base_state = HashedPostState {
669                    accounts: std::iter::once((hashed_address, Some(account))).collect(),
670                    storages: std::iter::once((
671                        hashed_address,
672                        HashedStorage::from_iter(false, base_slots),
673                    ))
674                    .collect(),
675                };
676
677                // Step 2: Compute trie updates from state root over the full base state.
678                let base_hashed_cursor_factory =
679                    MockHashedCursorFactory::from_hashed_post_state(base_state);
680                let (_, trie_updates) = StateRoot::new(
681                    NoopTrieCursorFactory,
682                    base_hashed_cursor_factory.clone(),
683                )
684                .root_with_updates()
685                .expect("state root computation should succeed");
686
687                // Step 3: Create a MockTrieCursorFactory from those trie updates.
688                let mock_trie_cursor_factory =
689                    MockTrieCursorFactory::from_trie_updates(trie_updates);
690
691                // Step 4: Build the changeset post state. Removals use U256::ZERO.
692                let changeset_storage: Vec<(B256, U256)> = changeset
693                    .iter()
694                    .map(|(k, v)| (*k, v.unwrap_or(U256::ZERO)))
695                    .collect();
696                let changeset_state = HashedPostState {
697                    accounts: std::iter::once((hashed_address, Some(account))).collect(),
698                    storages: std::iter::once((
699                        hashed_address,
700                        HashedStorage::from_iter(false, changeset_storage),
701                    ))
702                    .collect(),
703                };
704
705                // Step 5: Generate prefix sets from the changeset.
706                let prefix_sets_mut = changeset_state.construct_prefix_sets();
707
708                // Step 6: Build proof targets.
709                let slot_targets: B256Set = proof_slot_targets.into_iter().collect();
710                let targets =
711                    MultiProofTargets::from_iter([(hashed_address, slot_targets)]);
712
713                // Step 7: Create the HashedPostStateCursorFactory overlaying changeset
714                // on the base.
715                let changeset_sorted = changeset_state.into_sorted();
716                let overlay_cursor_factory = HashedPostStateCursorFactory::new(
717                    base_hashed_cursor_factory,
718                    &changeset_sorted,
719                );
720
721                // Step 8a: Approach A — prefix sets passed to Proof directly.
722                let proof_a = Proof::new(
723                    mock_trie_cursor_factory.clone(),
724                    overlay_cursor_factory.clone(),
725                )
726                .with_prefix_sets_mut(prefix_sets_mut.clone());
727                let multiproof_a = proof_a
728                    .multiproof(targets.clone())
729                    .expect("multiproof A should succeed");
730
731                // Step 8b: Approach B — MaskedTrieCursorFactory, no prefix sets on Proof.
732                let masked_trie_cursor_factory = MaskedTrieCursorFactory::new(
733                    mock_trie_cursor_factory,
734                    prefix_sets_mut.freeze(),
735                );
736                let proof_b = Proof::new(
737                    masked_trie_cursor_factory,
738                    overlay_cursor_factory,
739                );
740                let multiproof_b = proof_b
741                    .multiproof(targets)
742                    .expect("multiproof B should succeed");
743
744                // Step 9: Compare results.
745                assert_eq!(
746                    multiproof_a, multiproof_b,
747                    "multiproof with prefix sets should equal multiproof with masked cursor"
748                );
749            }
750        }
751    }
752}