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 revm_state::EvmState;
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 minimum length of a node's path for it to be retained.
15    pub min_len: u8,
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, min_len: 0 }
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    /// Only match trie nodes whose path is at least this long.
33    ///
34    /// # Panics
35    ///
36    /// This method panics if `min_len` is greater than 64.
37    pub fn with_min_len(mut self, min_len: u8) -> Self {
38        debug_assert!(min_len <= 64);
39        self.min_len = min_len;
40        self
41    }
42}
43
44impl From<B256> for ProofV2Target {
45    fn from(key: B256) -> Self {
46        Self::new(key)
47    }
48}
49
50/// A set of account and storage V2 proof targets. The account and storage targets do not need to
51/// necessarily overlap.
52#[derive(Debug, Default)]
53pub struct MultiProofTargetsV2 {
54    /// The set of account proof targets to generate proofs for.
55    pub account_targets: Vec<ProofV2Target>,
56    /// The sets of storage proof targets to generate proofs for.
57    pub storage_targets: B256Map<Vec<ProofV2Target>>,
58}
59
60impl MultiProofTargetsV2 {
61    /// Returns true is there are no account or storage targets.
62    pub fn is_empty(&self) -> bool {
63        self.account_targets.is_empty() && self.storage_targets.is_empty()
64    }
65
66    /// Returns the number of items that will be considered during chunking.
67    pub fn chunking_length(&self) -> usize {
68        self.account_targets.len() +
69            self.storage_targets.values().map(|slots| slots.len()).sum::<usize>()
70    }
71
72    /// Returns an iterator that yields chunks of the specified size.
73    pub fn chunks(self, chunk_size: usize) -> impl Iterator<Item = Self> {
74        ChunkedMultiProofTargetsV2::new(self, chunk_size)
75    }
76
77    /// Returns a set of [`MultiProofTargetsV2`] and the total amount of storage targets, based on
78    /// the given state.
79    pub fn from_state(state: EvmState) -> (Self, usize) {
80        let mut targets = Self::default();
81        targets.account_targets.reserve(state.len());
82        targets.storage_targets.reserve(state.len());
83        let mut storage_target_count = 0;
84        for (addr, account) in state {
85            // if the account was not touched, or if the account was selfdestructed, do not
86            // fetch proofs for it
87            //
88            // Since selfdestruct can only happen in the same transaction, we can skip
89            // prefetching proofs for selfdestructed accounts
90            //
91            // See: https://eips.ethereum.org/EIPS/eip-6780
92            if !account.is_touched() || account.is_selfdestructed() {
93                continue
94            }
95
96            let hashed_address = keccak256(addr);
97
98            if account.info != account.original_info() {
99                targets.account_targets.push(hashed_address.into());
100            }
101
102            let mut storage_slots = Vec::with_capacity(account.storage.len());
103            for (key, slot) in account.storage {
104                // do nothing if unchanged
105                if !slot.is_changed() {
106                    continue
107                }
108
109                let hashed_slot = keccak256(B256::new(key.to_be_bytes()));
110                storage_slots.push(ProofV2Target::from(hashed_slot));
111            }
112
113            storage_target_count += storage_slots.len();
114            if !storage_slots.is_empty() {
115                targets.storage_targets.insert(hashed_address, storage_slots);
116            }
117        }
118
119        (targets, storage_target_count)
120    }
121}
122
123/// An iterator that yields chunks of V2 proof targets of at most `size` account and storage
124/// targets.
125///
126/// Unlike legacy chunking, V2 preserves account targets exactly as they were (with their `min_len`
127/// metadata). Account targets must appear in a chunk. Storage targets for those accounts are
128/// chunked together, but if they exceed the chunk size, subsequent chunks contain only the
129/// remaining storage targets without repeating the account target.
130#[derive(Debug)]
131pub struct ChunkedMultiProofTargetsV2 {
132    /// Remaining account targets to process
133    account_targets: alloc::vec::IntoIter<ProofV2Target>,
134    /// Storage targets by account address
135    storage_targets: B256Map<Vec<ProofV2Target>>,
136    /// Current account being processed (if any storage slots remain)
137    current_account_storage: Option<(B256, alloc::vec::IntoIter<ProofV2Target>)>,
138    /// Chunk size
139    size: usize,
140}
141
142impl ChunkedMultiProofTargetsV2 {
143    /// Creates a new chunked iterator for the given targets.
144    pub fn new(targets: MultiProofTargetsV2, size: usize) -> Self {
145        Self {
146            account_targets: targets.account_targets.into_iter(),
147            storage_targets: targets.storage_targets,
148            current_account_storage: None,
149            size,
150        }
151    }
152}
153
154impl Iterator for ChunkedMultiProofTargetsV2 {
155    type Item = MultiProofTargetsV2;
156
157    fn next(&mut self) -> Option<Self::Item> {
158        let mut chunk = MultiProofTargetsV2::default();
159        let mut count = 0;
160
161        // First, finish any remaining storage slots from previous account
162        if let Some((account_addr, ref mut storage_iter)) = self.current_account_storage {
163            let remaining_capacity = self.size - count;
164            let slots: Vec<_> = storage_iter.by_ref().take(remaining_capacity).collect();
165
166            count += slots.len();
167            chunk.storage_targets.insert(account_addr, slots);
168
169            // If iterator is exhausted, clear current_account_storage
170            if storage_iter.len() == 0 {
171                self.current_account_storage = None;
172            }
173        }
174
175        // Process account targets and their storage
176        while count < self.size {
177            let Some(account_target) = self.account_targets.next() else {
178                break;
179            };
180
181            // Add the account target
182            chunk.account_targets.push(account_target);
183            count += 1;
184
185            // Check if this account has storage targets
186            let account_addr = account_target.key();
187            if let Some(storage_slots) = self.storage_targets.remove(&account_addr) {
188                let remaining_capacity = self.size - count;
189
190                if storage_slots.len() <= remaining_capacity {
191                    // Optimization: We can take all slots, just move the vec
192                    count += storage_slots.len();
193                    chunk.storage_targets.insert(account_addr, storage_slots);
194                } else {
195                    // We need to split the storage slots
196                    let mut storage_iter = storage_slots.into_iter();
197                    let slots_in_chunk: Vec<_> =
198                        storage_iter.by_ref().take(remaining_capacity).collect();
199                    count += slots_in_chunk.len();
200
201                    chunk.storage_targets.insert(account_addr, slots_in_chunk);
202
203                    // Save remaining storage slots for next chunk
204                    self.current_account_storage = Some((account_addr, storage_iter));
205                    break;
206                }
207            }
208        }
209
210        // Process any remaining storage-only entries (accounts not in account_targets)
211        while let Some((account_addr, storage_slots)) = self.storage_targets.iter_mut().next() &&
212            count < self.size
213        {
214            let account_addr = *account_addr;
215            let storage_slots = core::mem::take(storage_slots);
216            let remaining_capacity = self.size - count;
217
218            // Always remove from the map - if there are remaining slots they go to
219            // current_account_storage
220            self.storage_targets.remove(&account_addr);
221
222            if storage_slots.len() <= remaining_capacity {
223                // Optimization: We can take all slots, just move the vec
224                count += storage_slots.len();
225                chunk.storage_targets.insert(account_addr, storage_slots);
226            } else {
227                // We need to split the storage slots
228                let mut storage_iter = storage_slots.into_iter();
229                let slots_in_chunk: Vec<_> =
230                    storage_iter.by_ref().take(remaining_capacity).collect();
231
232                chunk.storage_targets.insert(account_addr, slots_in_chunk);
233
234                // Save remaining storage slots for next chunk
235                if storage_iter.len() > 0 {
236                    self.current_account_storage = Some((account_addr, storage_iter));
237                }
238                break;
239            }
240        }
241
242        if chunk.account_targets.is_empty() && chunk.storage_targets.is_empty() {
243            None
244        } else {
245            Some(chunk)
246        }
247    }
248}