Skip to main content

reth_trie/
witness.rs

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