Skip to main content

reth_trie_common/
target_v2.rs

1//! V2 proof targets and chunking.
2
3use crate::Nibbles;
4use alloc::vec::Vec;
5use alloy_primitives::{keccak256, map::B256Map, B256};
6use evm2::evm::AccountChanges;
7
8/// Target describes a proof target. For every proof target given, a proof calculator will calculate
9/// and return all nodes whose path is a prefix of the target's `key_nibbles`.
10#[derive(Debug, Copy, Clone)]
11pub struct ProofV2Target {
12    /// The key of the proof target, as nibbles.
13    pub key_nibbles: Nibbles,
14    /// The known-parent context for this target.
15    pub parent: ProofV2TargetParent,
16}
17
18impl ProofV2Target {
19    /// Returns a new [`ProofV2Target`] which matches all trie nodes whose path is a prefix of this
20    /// key.
21    pub fn new(key: B256) -> Self {
22        // SAFETY: key is a B256 and so is exactly 32-bytes.
23        let key_nibbles = unsafe { Nibbles::unpack_unchecked(key.as_slice()) };
24        Self { key_nibbles, parent: ProofV2TargetParent::NONE }
25    }
26
27    /// Returns the key the target was initialized with.
28    pub fn key(&self) -> B256 {
29        B256::from_slice(&self.key_nibbles.pack())
30    }
31
32    /// Sets the already-revealed parent branch of this target.
33    pub const fn with_parent(mut self, parent: ProofV2TargetParent) -> Self {
34        self.parent = parent;
35        self
36    }
37}
38
39impl From<B256> for ProofV2Target {
40    fn from(key: B256) -> Self {
41        Self::new(key)
42    }
43}
44
45/// The already-revealed parent branch of a [`ProofV2Target`].
46///
47/// [`Self::NONE`] indicates that no parent is known and the proof must include the actual trie
48/// root. A known parent at path length `n` makes the proof start at its direct child at length
49/// `n + 1`. In particular, a known parent at path length zero means the root branch is already
50/// revealed, so the proof starts at one of its direct children. Known parent path lengths are
51/// always less than 64.
52///
53/// Parent contexts are ordered from broadest to narrowest: [`Self::NONE`] precedes every known
54/// parent, and known parents are ordered by path length.
55#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
56pub struct ProofV2TargetParent(Option<u8>);
57
58impl ProofV2TargetParent {
59    /// No parent branch is known, so the proof must include the actual trie root.
60    pub const NONE: Self = Self(None);
61
62    /// Returns a known parent branch with the given logical path length.
63    ///
64    /// # Panics
65    ///
66    /// Panics if `path_len` is greater than or equal to 64.
67    pub const fn new(path_len: usize) -> Self {
68        assert!(path_len < 64, "parent path length must be less than 64");
69        Self(Some(path_len as u8))
70    }
71
72    /// Returns `true` if the parent branch is already known.
73    pub const fn is_known(self) -> bool {
74        self.0.is_some()
75    }
76
77    /// Returns the logical path length of the known parent branch, if any.
78    pub const fn path_len(self) -> Option<usize> {
79        match self.0 {
80            Some(path_len) => Some(path_len as usize),
81            None => None,
82        }
83    }
84
85    /// Returns the path of the known parent branch for the target path, if any.
86    pub fn path(self, mut target_path: Nibbles) -> Option<Nibbles> {
87        target_path.truncate(self.path_len()?);
88        Some(target_path)
89    }
90}
91
92/// A set of account and storage V2 proof targets. The account and storage targets do not need to
93/// necessarily overlap.
94#[derive(Debug, Default)]
95pub struct MultiProofTargetsV2 {
96    /// The set of account proof targets to generate proofs for.
97    pub account_targets: Vec<ProofV2Target>,
98    /// The sets of storage proof targets to generate proofs for.
99    pub storage_targets: B256Map<Vec<ProofV2Target>>,
100}
101
102impl MultiProofTargetsV2 {
103    /// Returns true is there are no account or storage targets.
104    pub fn is_empty(&self) -> bool {
105        self.account_targets.is_empty() && self.storage_targets.is_empty()
106    }
107
108    /// Returns the number of items that will be considered during chunking.
109    pub fn chunking_length(&self) -> usize {
110        self.account_targets.len() +
111            self.storage_targets.values().map(|slots| slots.len()).sum::<usize>()
112    }
113
114    /// Returns an iterator that yields chunks of the specified size.
115    ///
116    /// # Panics
117    ///
118    /// Panics if the chunk size is zero.
119    pub fn chunks(self, chunk_size: usize) -> impl Iterator<Item = Self> {
120        ChunkedMultiProofTargetsV2::new(self, chunk_size)
121    }
122
123    /// Returns a set of [`MultiProofTargetsV2`] and the total amount of storage targets, based on
124    /// the given account changes.
125    ///
126    /// Accounts that were not changed, or that were selfdestructed, get no targets. Since
127    /// selfdestruct can only happen in the creating transaction, proofs for those accounts are not
128    /// worth prefetching.
129    ///
130    /// See: <https://eips.ethereum.org/EIPS/eip-6780>
131    pub fn from_account_changes<'a>(
132        changes: impl IntoIterator<Item = AccountChanges<'a>>,
133    ) -> (Self, usize) {
134        let mut targets = Self::default();
135        let mut storage_target_count = 0;
136        for changes in changes {
137            if changes.selfdestructed {
138                continue
139            }
140
141            let storage_slots = changes
142                .storage
143                .changed_slots()
144                .map(|(key, _)| ProofV2Target::new(keccak256(B256::from(*key))))
145                .collect::<Vec<_>>();
146            let info_changed = changes.original != changes.current;
147            if !info_changed && storage_slots.is_empty() {
148                continue
149            }
150
151            let hashed_address = keccak256(changes.address);
152            if info_changed {
153                targets.account_targets.push(ProofV2Target::new(hashed_address));
154            }
155            if !storage_slots.is_empty() {
156                storage_target_count += storage_slots.len();
157                targets.storage_targets.insert(hashed_address, storage_slots);
158            }
159        }
160
161        (targets, storage_target_count)
162    }
163}
164
165/// An iterator that yields chunks of V2 proof targets of at most `size` account and storage
166/// targets.
167///
168/// Unlike legacy chunking, V2 preserves account targets exactly as they were (including their
169/// parent metadata). Account targets must appear in a chunk. Storage targets for those accounts
170/// are chunked together, but if they exceed the chunk size, subsequent chunks contain only the
171/// remaining storage targets without repeating the account target.
172#[derive(Debug)]
173pub struct ChunkedMultiProofTargetsV2 {
174    /// Remaining account targets to process
175    account_targets: alloc::vec::IntoIter<ProofV2Target>,
176    /// Storage targets by account address
177    storage_targets: B256Map<Vec<ProofV2Target>>,
178    /// Current account being processed (if any storage slots remain)
179    current_account_storage: Option<(B256, alloc::vec::IntoIter<ProofV2Target>)>,
180    /// Chunk size
181    size: usize,
182}
183
184impl ChunkedMultiProofTargetsV2 {
185    /// Creates a new chunked iterator for the given targets.
186    ///
187    /// # Panics
188    ///
189    /// Panics if `size` is zero.
190    pub fn new(targets: MultiProofTargetsV2, size: usize) -> Self {
191        assert!(size > 0, "chunk size must be non-zero");
192        Self {
193            account_targets: targets.account_targets.into_iter(),
194            storage_targets: targets.storage_targets,
195            current_account_storage: None,
196            size,
197        }
198    }
199}
200
201impl Iterator for ChunkedMultiProofTargetsV2 {
202    type Item = MultiProofTargetsV2;
203
204    fn next(&mut self) -> Option<Self::Item> {
205        let mut chunk = MultiProofTargetsV2::default();
206        let mut count = 0;
207
208        // First, finish any remaining storage slots from previous account
209        if let Some((account_addr, ref mut storage_iter)) = self.current_account_storage {
210            let remaining_capacity = self.size - count;
211            let slots: Vec<_> = storage_iter.by_ref().take(remaining_capacity).collect();
212
213            count += slots.len();
214            chunk.storage_targets.insert(account_addr, slots);
215
216            // If iterator is exhausted, clear current_account_storage
217            if storage_iter.len() == 0 {
218                self.current_account_storage = None;
219            }
220        }
221
222        // Process account targets and their storage
223        while count < self.size {
224            let Some(account_target) = self.account_targets.next() else {
225                break;
226            };
227
228            // Add the account target
229            chunk.account_targets.push(account_target);
230            count += 1;
231
232            // Check if this account has storage targets
233            let account_addr = account_target.key();
234            if let Some(storage_slots) = self.storage_targets.remove(&account_addr) {
235                let remaining_capacity = self.size - count;
236
237                if storage_slots.len() <= remaining_capacity {
238                    // Optimization: We can take all slots, just move the vec
239                    count += storage_slots.len();
240                    chunk.storage_targets.insert(account_addr, storage_slots);
241                } else {
242                    // We need to split the storage slots
243                    let mut storage_iter = storage_slots.into_iter();
244                    let slots_in_chunk: Vec<_> =
245                        storage_iter.by_ref().take(remaining_capacity).collect();
246                    count += slots_in_chunk.len();
247
248                    chunk.storage_targets.insert(account_addr, slots_in_chunk);
249
250                    // Save remaining storage slots for next chunk
251                    self.current_account_storage = Some((account_addr, storage_iter));
252                    break;
253                }
254            }
255        }
256
257        // Process any remaining storage-only entries (accounts not in account_targets)
258        while let Some((account_addr, storage_slots)) = self.storage_targets.iter_mut().next() &&
259            count < self.size
260        {
261            let account_addr = *account_addr;
262            let storage_slots = core::mem::take(storage_slots);
263            let remaining_capacity = self.size - count;
264
265            // Always remove from the map - if there are remaining slots they go to
266            // current_account_storage
267            self.storage_targets.remove(&account_addr);
268
269            if storage_slots.len() <= remaining_capacity {
270                // Optimization: We can take all slots, just move the vec
271                count += storage_slots.len();
272                chunk.storage_targets.insert(account_addr, storage_slots);
273            } else {
274                // We need to split the storage slots
275                let mut storage_iter = storage_slots.into_iter();
276                let slots_in_chunk: Vec<_> =
277                    storage_iter.by_ref().take(remaining_capacity).collect();
278
279                chunk.storage_targets.insert(account_addr, slots_in_chunk);
280
281                // Save remaining storage slots for next chunk
282                if storage_iter.len() > 0 {
283                    self.current_account_storage = Some((account_addr, storage_iter));
284                }
285                break;
286            }
287        }
288
289        if chunk.account_targets.is_empty() && chunk.storage_targets.is_empty() {
290            None
291        } else {
292            Some(chunk)
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use alloy_primitives::{Address, U256};
301    use evm2::evm::{AccountInfo, StorageOverlay, StorageSlot, Tracked};
302
303    fn overlay(slots: &[(u64, u64, u64)]) -> StorageOverlay {
304        let mut storage = StorageOverlay::default();
305        for &(key, original, current) in slots {
306            storage.slots.insert(
307                U256::from(key),
308                StorageSlot {
309                    value: Tracked::from_parts(U256::from(original), U256::from(current)),
310                    ..Default::default()
311                },
312            );
313        }
314        storage
315    }
316
317    fn changes<'a>(
318        address: Address,
319        original: &'a AccountInfo,
320        current: Option<&'a AccountInfo>,
321        selfdestructed: bool,
322        storage: &'a StorageOverlay,
323    ) -> AccountChanges<'a> {
324        AccountChanges {
325            address,
326            original: Some(original),
327            current,
328            created: true,
329            selfdestructed,
330            touched: false,
331            code: None,
332            storage,
333        }
334    }
335
336    #[test]
337    fn from_account_changes_ignores_unchanged_reads() {
338        let info = AccountInfo::default();
339        let storage = overlay(&[(7, 8, 8)]);
340        let (targets, count) = MultiProofTargetsV2::from_account_changes([AccountChanges {
341            created: false,
342            ..changes(Address::repeat_byte(1), &info, Some(&info), false, &storage)
343        }]);
344        assert!(targets.is_empty());
345        assert_eq!(count, 0);
346    }
347
348    #[test]
349    fn from_account_changes_includes_changes() {
350        let address = Address::repeat_byte(1);
351        let original = AccountInfo::default();
352        let current = AccountInfo { nonce: 1, ..original.clone() };
353        let storage = overlay(&[(7, 0, 8)]);
354        let (targets, count) = MultiProofTargetsV2::from_account_changes([changes(
355            address,
356            &original,
357            Some(&current),
358            false,
359            &storage,
360        )]);
361        assert_eq!(
362            targets.account_targets.iter().map(ProofV2Target::key).collect::<Vec<_>>(),
363            vec![keccak256(address)]
364        );
365        assert_eq!(
366            targets.storage_targets[&keccak256(address)]
367                .iter()
368                .map(ProofV2Target::key)
369                .collect::<Vec<_>>(),
370            vec![keccak256(B256::from(U256::from(7)))]
371        );
372        assert_eq!(count, 1);
373    }
374
375    #[test]
376    fn from_account_changes_skips_selfdestructed() {
377        let address = Address::repeat_byte(1);
378        let retained = Address::repeat_byte(2);
379        let info = AccountInfo::default();
380        let storage = overlay(&[(7, 0, 8)]);
381        let (targets, count) = MultiProofTargetsV2::from_account_changes([
382            changes(address, &info, None, true, &storage),
383            // Lifecycle flags alone do not imply an account trie update.
384            changes(retained, &info, Some(&info), false, &storage),
385        ]);
386        assert!(targets.account_targets.is_empty());
387        assert_eq!(targets.storage_targets.len(), 1);
388        assert!(targets.storage_targets.contains_key(&keccak256(retained)));
389        assert_eq!(count, 1);
390    }
391}