Skip to main content

reth_trie/
witness.rs

1use crate::{
2    hashed_cursor::HashedCursorFactory,
3    prefix_set::TriePrefixSetsMut,
4    proof_v2::{self, MultiProofCalculator},
5    trie_cursor::TrieCursorFactory,
6    TRIE_ACCOUNT_RLP_MAX_SIZE,
7};
8use alloy_primitives::{
9    keccak256,
10    map::{B256Map, HashMap},
11    Bytes, B256,
12};
13use alloy_rlp::{Encodable, EMPTY_STRING_CODE};
14use alloy_trie::{nodes::BranchNodeRef, EMPTY_ROOT_HASH};
15use reth_execution_errors::{SparseStateTrieErrorKind, StateProofError, TrieWitnessError};
16use reth_trie_common::{
17    DecodedMultiProofV2, ExecutionWitnessMode, HashedPostState, MultiProofTargetsV2, ProofV2Target,
18    TrieNodeV2,
19};
20use reth_trie_sparse::{LeafUpdate, SparseStateTrie, SparseTrie as _, TrieNodeEpoch};
21
22/// State transition witness for the trie.
23#[derive(Debug)]
24pub struct TrieWitness<T, H> {
25    /// The cursor factory for traversing trie nodes.
26    trie_cursor_factory: T,
27    /// The factory for hashed cursors.
28    hashed_cursor_factory: H,
29    /// A set of prefix sets that have changes.
30    prefix_sets: TriePrefixSetsMut,
31    /// Flag indicating whether the root node should always be included (even if the target state
32    /// is empty). This setting is useful if the caller wants to verify the witness against the
33    /// parent state root.
34    /// Set to `false` by default.
35    always_include_root_node: bool,
36    /// Controls how the witness is generated.
37    mode: ExecutionWitnessMode,
38    /// Recorded witness.
39    witness: B256Map<Bytes>,
40}
41
42impl<T, H> TrieWitness<T, H> {
43    /// Creates a new witness generator.
44    pub fn new(trie_cursor_factory: T, hashed_cursor_factory: H) -> Self {
45        Self {
46            trie_cursor_factory,
47            hashed_cursor_factory,
48            prefix_sets: TriePrefixSetsMut::default(),
49            always_include_root_node: false,
50            mode: ExecutionWitnessMode::Legacy,
51            witness: HashMap::default(),
52        }
53    }
54
55    /// Set the prefix sets. They have to be mutable in order to allow extension with proof target.
56    pub fn with_prefix_sets_mut(mut self, prefix_sets: TriePrefixSetsMut) -> Self {
57        self.prefix_sets = prefix_sets;
58        self
59    }
60
61    /// Set `always_include_root_node` to true. Root node will be included even in empty state.
62    /// This setting is useful if the caller wants to verify the witness against the
63    /// parent state root.
64    pub const fn always_include_root_node(mut self) -> Self {
65        self.always_include_root_node = true;
66        self
67    }
68
69    /// Set the execution witness generation mode.
70    pub const fn with_execution_witness_mode(mut self, mode: ExecutionWitnessMode) -> Self {
71        self.mode = mode;
72        self
73    }
74}
75
76impl<T, H> TrieWitness<T, H>
77where
78    T: TrieCursorFactory + Clone,
79    H: HashedCursorFactory + Clone,
80{
81    /// Compute the state transition witness for the trie. Gather all required nodes
82    /// to apply `state` on top of the current trie state.
83    ///
84    /// # Arguments
85    ///
86    /// `state` - state transition containing both modified and touched accounts and storage slots.
87    #[allow(clippy::clone_on_copy)]
88    pub fn compute(mut self, state: HashedPostState) -> Result<B256Map<Bytes>, TrieWitnessError> {
89        let is_state_empty = state.is_empty();
90        if is_state_empty && !self.always_include_root_node {
91            return Ok(Default::default())
92        }
93
94        let proof_targets = if is_state_empty {
95            MultiProofTargetsV2 {
96                account_targets: vec![ProofV2Target::new(B256::ZERO)],
97                ..Default::default()
98            }
99        } else {
100            Self::get_proof_targets(&state)
101        };
102        let mut calculator = MultiProofCalculator::new(
103            self.trie_cursor_factory.clone(),
104            self.hashed_cursor_factory.clone(),
105            self.prefix_sets.clone().freeze(),
106            Default::default(),
107        );
108        let multiproof = calculator.multiproof(proof_targets)?;
109
110        // No need to reconstruct the rest of the trie, we just need to include
111        // the root node and return.
112        if is_state_empty {
113            let (root_hash, root_node) = if let Some(root_node) =
114                multiproof.account_proofs.into_iter().find(|n| n.path.is_empty())
115            {
116                let bytes = Bytes::from(alloy_rlp::encode(&root_node.node));
117                (keccak256(&bytes), bytes)
118            } else {
119                (EMPTY_ROOT_HASH, Bytes::from([EMPTY_STRING_CODE]))
120            };
121            return Ok(B256Map::from_iter([(root_hash, root_node)]))
122        }
123
124        // Record all nodes from multiproof in the witness.
125        self.record_multiproof_nodes(&multiproof);
126
127        let mut sparse_trie = SparseStateTrie::new();
128        sparse_trie.reveal_decoded_multiproof_v2(multiproof)?;
129
130        // Build storage leaf updates for all accounts with storage changes, split into
131        // removals and upserts. Legacy mode applies removals first to preserve the
132        // historical witness shape expected by existing consumers: a removal can collapse
133        // a branch and force proof fetches that some consumers still rely on. Canonical
134        // mode applies upserts first to avoid those compatibility-only nodes and emit
135        // the minimized draft-spec witness.
136        let mut storage_removals: B256Map<B256Map<LeafUpdate>> = B256Map::default();
137        let mut storage_upserts: B256Map<B256Map<LeafUpdate>> = B256Map::default();
138        for (hashed_address, storage) in &state.storages {
139            for (&hashed_slot, value) in &storage.storage {
140                if value.is_zero() {
141                    storage_removals
142                        .entry(*hashed_address)
143                        .or_default()
144                        .insert(hashed_slot, LeafUpdate::Changed(vec![]));
145                } else {
146                    storage_upserts.entry(*hashed_address).or_default().insert(
147                        hashed_slot,
148                        LeafUpdate::Changed(alloy_rlp::encode_fixed_size(value).to_vec()),
149                    );
150                }
151            }
152        }
153
154        let storage_update_sets = if self.mode.is_canonical() {
155            [&mut storage_upserts, &mut storage_removals]
156        } else {
157            [&mut storage_removals, &mut storage_upserts]
158        };
159
160        // Apply storage updates in mode-specific order, fetching additional proofs as needed.
161        for storage_updates in storage_update_sets {
162            loop {
163                let mut targets = MultiProofTargetsV2::default();
164
165                for (&hashed_address, slot_updates) in storage_updates.iter_mut() {
166                    if slot_updates.is_empty() {
167                        continue;
168                    }
169                    let storage_trie = sparse_trie
170                        .storage_trie_mut(&hashed_address)
171                        .expect("storage trie was revealed from multiproof");
172                    storage_trie
173                        .update_leaves(slot_updates, |key, parent| {
174                            targets
175                                .storage_targets
176                                .entry(hashed_address)
177                                .or_default()
178                                .push(ProofV2Target::new(key).with_parent(parent));
179                        })
180                        .map_err(|err| {
181                            SparseStateTrieErrorKind::SparseStorageTrie(
182                                hashed_address,
183                                err.into_kind(),
184                            )
185                        })?;
186                }
187
188                if targets.is_empty() {
189                    break;
190                }
191
192                let multiproof = calculator.multiproof(targets)?;
193                self.record_multiproof_nodes(&multiproof);
194                sparse_trie.reveal_decoded_multiproof_v2(multiproof)?;
195            }
196        }
197
198        // Build account leaf updates, split into removals and upserts. Legacy mode keeps
199        // removals-first for the same compatibility reason as storage updates, while
200        // canonical mode uses upserts-first so account updates follow the minimized
201        // draft-spec witness order.
202        let mut account_removals: B256Map<LeafUpdate> = B256Map::default();
203        let mut account_upserts: B256Map<LeafUpdate> = B256Map::default();
204        for &hashed_address in state.accounts.keys().chain(state.storages.keys()) {
205            if account_removals.contains_key(&hashed_address) ||
206                account_upserts.contains_key(&hashed_address)
207            {
208                continue;
209            }
210
211            let account = state
212                .accounts
213                .get(&hashed_address)
214                .ok_or(TrieWitnessError::MissingAccount(hashed_address))?
215                .clone()
216                .unwrap_or_default();
217
218            let storage_root =
219                if let Some(storage_trie) = sparse_trie.storage_trie_mut(&hashed_address) {
220                    storage_trie.root(TrieNodeEpoch::UNMODIFIED)
221                } else {
222                    let record_root_node = !self.mode.is_canonical() ||
223                        state
224                            .storages
225                            .get(&hashed_address)
226                            .is_some_and(|storage| !storage.storage.is_empty());
227                    self.account_storage_root(hashed_address, record_root_node)?
228                };
229
230            if account.is_empty() && storage_root == EMPTY_ROOT_HASH {
231                account_removals.insert(hashed_address, LeafUpdate::Changed(vec![]));
232            } else {
233                let mut rlp = Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE);
234                account.into_trie_account(storage_root).encode(&mut rlp);
235                account_upserts.insert(hashed_address, LeafUpdate::Changed(rlp));
236            }
237        }
238
239        let account_update_sets = if self.mode.is_canonical() {
240            [&mut account_upserts, &mut account_removals]
241        } else {
242            [&mut account_removals, &mut account_upserts]
243        };
244
245        // Apply account updates in mode-specific order, fetching additional proofs as needed.
246        for account_updates in account_update_sets {
247            loop {
248                let mut targets = MultiProofTargetsV2::default();
249
250                sparse_trie
251                    .trie_mut()
252                    .update_leaves(account_updates, |key, parent| {
253                        targets.account_targets.push(ProofV2Target::new(key).with_parent(parent));
254                    })
255                    .map_err(SparseStateTrieErrorKind::from)?;
256
257                if targets.is_empty() {
258                    break;
259                }
260
261                let multiproof = calculator.multiproof(targets)?;
262                self.record_multiproof_nodes(&multiproof);
263                sparse_trie.reveal_decoded_multiproof_v2(multiproof)?;
264            }
265        }
266
267        if self.mode.is_canonical() {
268            // Empty trie nodes carry no useful witness information and are trivially
269            // reconstructible from the empty root hash.
270            self.witness.retain(|_, value| value.as_ref() != [EMPTY_STRING_CODE]);
271        }
272
273        Ok(self.witness)
274    }
275
276    /// Record all nodes from a V2 decoded multiproof in the witness.
277    fn record_multiproof_nodes(&mut self, multiproof: &DecodedMultiProofV2) {
278        let mut encoded = Vec::new();
279        for proof_node in &multiproof.account_proofs {
280            self.record_witness_node(&proof_node.node, &mut encoded);
281        }
282        for proof_nodes in multiproof.storage_proofs.values() {
283            for proof_node in proof_nodes {
284                self.record_witness_node(&proof_node.node, &mut encoded);
285            }
286        }
287    }
288
289    /// Record a single [`TrieNodeV2`] in the witness.
290    fn record_witness_node(&mut self, node: &TrieNodeV2, encoded: &mut Vec<u8>) {
291        encoded.clear();
292        node.encode(encoded);
293        let hash = keccak256(encoded.as_slice());
294        self.witness.entry(hash).or_insert_with(|| Bytes::copy_from_slice(encoded));
295
296        if let TrieNodeV2::Branch(branch) = node &&
297            !branch.key.is_empty()
298        {
299            encoded.clear();
300            BranchNodeRef::new(&branch.stack, branch.state_mask).encode(encoded);
301            let hash = keccak256(encoded.as_slice());
302            self.witness.entry(hash).or_insert_with(|| Bytes::copy_from_slice(encoded));
303        }
304    }
305
306    /// Compute the storage root for an account by walking the storage trie using the cursor
307    /// factories and trie input prefix sets. Records the root node in the witness when requested.
308    fn account_storage_root(
309        &mut self,
310        hashed_address: B256,
311        record_root_node: bool,
312    ) -> Result<B256, TrieWitnessError> {
313        let storage_trie_cursor = self
314            .trie_cursor_factory
315            .storage_trie_cursor(hashed_address)
316            .map_err(StateProofError::from)?;
317        let hashed_storage_cursor = self
318            .hashed_cursor_factory
319            .hashed_storage_cursor(hashed_address)
320            .map_err(StateProofError::from)?;
321        let mut calculator = proof_v2::StorageProofCalculator::new_storage(
322            storage_trie_cursor,
323            hashed_storage_cursor,
324        );
325        if let Some(prefix_set) = self.prefix_sets.storage_prefix_sets.get(&hashed_address) {
326            calculator = calculator.with_prefix_set(prefix_set.clone().freeze());
327        }
328        let root_node = calculator.storage_root_node(hashed_address)?;
329        let root_hash = calculator
330            .compute_root_hash(core::slice::from_ref(&root_node))?
331            .unwrap_or(EMPTY_ROOT_HASH);
332        drop(calculator);
333        if record_root_node {
334            let mut encoded = Vec::new();
335            self.record_witness_node(&root_node.node, &mut encoded);
336        }
337        Ok(root_hash)
338    }
339
340    /// Retrieve proof targets for incoming hashed state.
341    /// Aggregates all accounts and slots present in the state.
342    fn get_proof_targets(state: &HashedPostState) -> MultiProofTargetsV2 {
343        let mut targets = MultiProofTargetsV2::default();
344        for &hashed_address in state.accounts.keys() {
345            targets.account_targets.push(ProofV2Target::new(hashed_address));
346        }
347        for (&hashed_address, storage) in &state.storages {
348            if !state.accounts.contains_key(&hashed_address) {
349                targets.account_targets.push(ProofV2Target::new(hashed_address));
350            }
351            // Accounts without storage slot changes need only an account proof.
352            if storage.storage.is_empty() {
353                continue;
354            }
355            let storage_keys = storage.storage.keys().map(|k| ProofV2Target::new(*k)).collect();
356            targets.storage_targets.insert(hashed_address, storage_keys);
357        }
358        targets
359    }
360}