reth_trie/
changesets.rs

1//! Trie changeset computation.
2//!
3//! This module provides functionality to compute trie changesets from trie updates.
4//! Changesets represent the old values of trie nodes before a block was applied,
5//! enabling reorgs by reverting blocks to their previous state.
6//!
7//! ## Overview
8//!
9//! When a block is executed, the trie is updated with new node values. To support
10//! chain reorganizations, we need to preserve the old values that existed before
11//! the block was applied. These old values are called "changesets".
12//!
13//! ## Usage
14//!
15//! The primary function is `compute_trie_changesets`, which takes:
16//! - A `TrieCursorFactory` for reading current trie state
17//! - `TrieUpdatesSorted` containing the new node values
18//!
19//! And returns `TrieUpdatesSorted` containing the old node values.
20
21use crate::trie_cursor::TrieCursorIter;
22use alloy_primitives::{map::B256Map, B256};
23use itertools::{merge_join_by, EitherOrBoth};
24use reth_storage_errors::db::DatabaseError;
25use reth_trie_common::{
26    updates::{StorageTrieUpdatesSorted, TrieUpdatesSorted},
27    BranchNodeCompact, Nibbles,
28};
29use std::cmp::Ordering;
30
31use crate::trie_cursor::{TrieCursor, TrieCursorFactory, TrieStorageCursor};
32
33/// Result type for changeset operations.
34pub type ChangesetResult<T> = Result<T, DatabaseError>;
35
36/// Computes trie changesets by looking up current node values from the trie.
37///
38/// Takes the new trie updates and queries the trie for the old values of
39/// changed nodes. Returns changesets representing the state before the block
40/// was applied, suitable for reorg operations.
41///
42/// # Arguments
43///
44/// * `factory` - Trie cursor factory for reading current trie state
45/// * `trie_updates` - New trie node values produced by state root computation
46///
47/// # Returns
48///
49/// `TrieUpdatesSorted` containing old node values (before this block)
50pub fn compute_trie_changesets<Factory>(
51    factory: &Factory,
52    trie_updates: &TrieUpdatesSorted,
53) -> ChangesetResult<TrieUpdatesSorted>
54where
55    Factory: TrieCursorFactory,
56{
57    // Compute account trie changesets
58    let account_nodes = compute_account_changesets(factory, trie_updates)?;
59
60    // Compute storage trie changesets
61    let mut storage_tries = B256Map::default();
62
63    // Create storage cursor once and reuse it for all addresses
64    let mut storage_cursor = factory.storage_trie_cursor(B256::default())?;
65
66    for (hashed_address, storage_updates) in trie_updates.storage_tries_ref() {
67        storage_cursor.set_hashed_address(*hashed_address);
68
69        let storage_changesets = if storage_updates.is_deleted() {
70            // Handle wiped storage
71            compute_wiped_storage_changesets(&mut storage_cursor, storage_updates)?
72        } else {
73            // Handle normal storage updates
74            compute_storage_changesets(&mut storage_cursor, storage_updates)?
75        };
76
77        if !storage_changesets.is_empty() {
78            storage_tries.insert(
79                *hashed_address,
80                StorageTrieUpdatesSorted {
81                    is_deleted: storage_updates.is_deleted(),
82                    storage_nodes: storage_changesets,
83                },
84            );
85        }
86    }
87
88    // Build and return the result
89    Ok(TrieUpdatesSorted::new(account_nodes, storage_tries))
90}
91
92/// Computes account trie changesets.
93///
94/// Looks up the current value for each changed account node path and returns
95/// a vector of (path, `old_node`) pairs. The result is already sorted since
96/// `trie_updates.account_nodes_ref()` is sorted.
97fn compute_account_changesets<Factory>(
98    factory: &Factory,
99    trie_updates: &TrieUpdatesSorted,
100) -> ChangesetResult<Vec<(Nibbles, Option<BranchNodeCompact>)>>
101where
102    Factory: TrieCursorFactory,
103{
104    let mut cursor = factory.account_trie_cursor()?;
105    let mut account_changesets = Vec::with_capacity(trie_updates.account_nodes_ref().len());
106
107    // For each changed account node, look up its current value
108    // The input is already sorted, so the output will be sorted
109    for (path, _new_node) in trie_updates.account_nodes_ref() {
110        let old_node = cursor.seek_exact(*path)?.map(|(_path, node)| node);
111        account_changesets.push((*path, old_node));
112    }
113
114    Ok(account_changesets)
115}
116
117/// Computes storage trie changesets for a single account.
118///
119/// Looks up the current value for each changed storage node path and returns
120/// a vector of (path, `old_node`) pairs. The result is already sorted since
121/// `storage_updates.storage_nodes` is sorted.
122///
123/// # Arguments
124///
125/// * `cursor` - Reusable storage trie cursor. The hashed address will be set before use.
126/// * `hashed_address` - The hashed address of the account
127/// * `storage_updates` - Storage trie updates for this account
128fn compute_storage_changesets(
129    cursor: &mut impl TrieStorageCursor,
130    storage_updates: &StorageTrieUpdatesSorted,
131) -> ChangesetResult<Vec<(Nibbles, Option<BranchNodeCompact>)>> {
132    let mut storage_changesets = Vec::with_capacity(storage_updates.storage_nodes.len());
133
134    // For each changed storage node, look up its current value
135    // The input is already sorted, so the output will be sorted
136    for (path, _new_node) in &storage_updates.storage_nodes {
137        let old_node = cursor.seek_exact(*path)?.map(|(_path, node)| node);
138        storage_changesets.push((*path, old_node));
139    }
140
141    Ok(storage_changesets)
142}
143
144/// Handles wiped storage trie changeset computation.
145///
146/// When an account's storage is completely wiped (e.g., account is destroyed),
147/// we need to capture not just the changed nodes, but ALL existing nodes in
148/// the storage trie, since they all will be deleted.
149///
150/// This uses an iterator-based approach to avoid allocating an intermediate Vec.
151/// It merges two sorted iterators:
152/// - Current values of changed paths
153/// - All existing nodes in the storage trie
154///
155/// # Arguments
156///
157/// * `changed_cursor` - Cursor for looking up changed node values
158/// * `wiped_cursor` - Cursor for iterating all nodes in the storage trie
159/// * `hashed_address` - The hashed address of the account
160/// * `storage_updates` - Storage trie updates for this account
161fn compute_wiped_storage_changesets(
162    cursor: &mut impl TrieStorageCursor,
163    storage_updates: &StorageTrieUpdatesSorted,
164) -> ChangesetResult<Vec<(Nibbles, Option<BranchNodeCompact>)>> {
165    // Set the hashed address for this account's storage trie
166    // Create an iterator that yields all nodes in the storage trie
167    let all_nodes = TrieCursorIter::new(cursor);
168
169    // Merge the two sorted iterators
170    let merged = storage_trie_wiped_changeset_iter(
171        storage_updates.storage_nodes.iter().map(|e| e.0),
172        all_nodes,
173    )?;
174
175    // Collect into a Vec
176    let mut storage_changesets = Vec::new();
177    for result in merged {
178        storage_changesets.push(result?);
179    }
180
181    Ok(storage_changesets)
182}
183
184/// Returns an iterator which produces the changeset values for an account whose storage was wiped
185/// during a block.
186///
187/// ## Arguments
188///
189/// - `curr_values_of_changed` is an iterator over the current values of all trie nodes modified by
190///   the block, ordered by path.
191/// - `all_nodes` is an iterator over all existing trie nodes for the account, ordered by path.
192///
193/// ## Returns
194///
195/// An iterator of trie node paths and a `Some(node)` (indicating the node was wiped) or a `None`
196/// (indicating the node was modified in the block but didn't previously exist). The iterator's
197/// results will be ordered by path.
198pub fn storage_trie_wiped_changeset_iter(
199    changed_paths: impl Iterator<Item = Nibbles>,
200    all_nodes: impl Iterator<Item = Result<(Nibbles, BranchNodeCompact), DatabaseError>>,
201) -> Result<
202    impl Iterator<Item = Result<(Nibbles, Option<BranchNodeCompact>), DatabaseError>>,
203    DatabaseError,
204> {
205    let all_nodes = all_nodes.map(|e| e.map(|(nibbles, node)| (nibbles, Some(node))));
206
207    let merged = merge_join_by(changed_paths, all_nodes, |a, b| match (a, b) {
208        (_, Err(_)) => Ordering::Greater,
209        (a, Ok(b)) => a.cmp(&b.0),
210    });
211
212    Ok(merged.map(|either_or| match either_or {
213        EitherOrBoth::Left(changed) => {
214            // A path of a changed node which was not found in the database. The current value of
215            // this path must be None, otherwise it would have also been returned by the `all_nodes`
216            // iter.
217            Ok((changed, None))
218        }
219        EitherOrBoth::Right(wiped) => {
220            // A node was found in the db (indicating it was wiped) but was not a changed node.
221            // Return it as-is.
222            wiped
223        }
224        EitherOrBoth::Both(_changed, wiped) => {
225            // A path of a changed node was found with a previous value in the database. If the
226            // changed node had no previous value (None) it wouldn't be returned by `all_nodes` and
227            // so would be in the Left branch.
228            //
229            // Due to the ordering closure passed to `merge_join_by` it's not possible for wrapped
230            // to be an error here.
231            debug_assert!(wiped.is_ok(), "unreachable error condition: {wiped:?}");
232            wiped
233        }
234    }))
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::trie_cursor::mock::MockTrieCursorFactory;
241    use alloy_primitives::map::B256Map;
242    use reth_trie_common::updates::StorageTrieUpdatesSorted;
243    use std::collections::BTreeMap;
244
245    #[test]
246    fn test_empty_updates() {
247        // Create an empty mock factory
248        // Note: We need to include B256::default() in storage_tries because
249        // compute_trie_changesets creates cursors for it upfront
250        let mut storage_tries = B256Map::default();
251        storage_tries.insert(B256::default(), BTreeMap::new());
252        let factory = MockTrieCursorFactory::new(BTreeMap::new(), storage_tries);
253
254        // Create empty updates
255        let updates = TrieUpdatesSorted::new(vec![], B256Map::default());
256
257        // Compute changesets
258        let changesets = compute_trie_changesets(&factory, &updates).unwrap();
259
260        // Should produce empty changesets
261        assert!(changesets.account_nodes_ref().is_empty());
262        assert!(changesets.storage_tries_ref().is_empty());
263    }
264
265    #[test]
266    fn test_account_changesets() {
267        // Create some initial account trie state
268        let path1 = Nibbles::from_nibbles([0x1, 0x2, 0x3]);
269        let path2 = Nibbles::from_nibbles([0x4, 0x5, 0x6]);
270        // tree_mask and hash_mask must be subsets of state_mask
271        let node1 = BranchNodeCompact::new(0b1111, 0b1010, 0, vec![], None);
272        let node2 = BranchNodeCompact::new(0b1111, 0b1100, 0, vec![], None);
273
274        let mut account_nodes = BTreeMap::new();
275        account_nodes.insert(path1, node1.clone());
276        account_nodes.insert(path2, node2);
277
278        // Need to include B256::default() for cursor creation
279        let mut storage_tries = B256Map::default();
280        storage_tries.insert(B256::default(), BTreeMap::new());
281        let factory = MockTrieCursorFactory::new(account_nodes, storage_tries);
282
283        // Create updates that modify path1 and add a new path3
284        let path3 = Nibbles::from_nibbles([0x7, 0x8, 0x9]);
285        let new_node1 = BranchNodeCompact::new(0b1111, 0b0001, 0, vec![], None);
286        let new_node3 = BranchNodeCompact::new(0b1111, 0b0000, 0, vec![], None);
287
288        let updates = TrieUpdatesSorted::new(
289            vec![(path1, Some(new_node1)), (path3, Some(new_node3))],
290            B256Map::default(),
291        );
292
293        // Compute changesets
294        let changesets = compute_trie_changesets(&factory, &updates).unwrap();
295
296        // Check account changesets
297        assert_eq!(changesets.account_nodes_ref().len(), 2);
298
299        // path1 should have the old node1 value
300        assert_eq!(changesets.account_nodes_ref()[0].0, path1);
301        assert_eq!(changesets.account_nodes_ref()[0].1, Some(node1));
302
303        // path3 should have None (it didn't exist before)
304        assert_eq!(changesets.account_nodes_ref()[1].0, path3);
305        assert_eq!(changesets.account_nodes_ref()[1].1, None);
306    }
307
308    #[test]
309    fn test_storage_changesets() {
310        let hashed_address = B256::from([1u8; 32]);
311
312        // Create some initial storage trie state
313        let path1 = Nibbles::from_nibbles([0x1, 0x2]);
314        let path2 = Nibbles::from_nibbles([0x3, 0x4]);
315        let node1 = BranchNodeCompact::new(0b1111, 0b0011, 0, vec![], None);
316        let node2 = BranchNodeCompact::new(0b1111, 0b0101, 0, vec![], None);
317
318        let mut storage_nodes = BTreeMap::new();
319        storage_nodes.insert(path1, node1.clone());
320        storage_nodes.insert(path2, node2);
321
322        let mut storage_tries = B256Map::default();
323        storage_tries.insert(B256::default(), BTreeMap::new()); // For cursor creation
324        storage_tries.insert(hashed_address, storage_nodes);
325
326        let factory = MockTrieCursorFactory::new(BTreeMap::new(), storage_tries);
327
328        // Create updates that modify path1 and add a new path3
329        let path3 = Nibbles::from_nibbles([0x5, 0x6]);
330        let new_node1 = BranchNodeCompact::new(0b1111, 0b1000, 0, vec![], None);
331        let new_node3 = BranchNodeCompact::new(0b1111, 0b0000, 0, vec![], None);
332
333        let mut storage_updates = B256Map::default();
334        storage_updates.insert(
335            hashed_address,
336            StorageTrieUpdatesSorted {
337                is_deleted: false,
338                storage_nodes: vec![(path1, Some(new_node1)), (path3, Some(new_node3))],
339            },
340        );
341
342        let updates = TrieUpdatesSorted::new(vec![], storage_updates);
343
344        // Compute changesets
345        let changesets = compute_trie_changesets(&factory, &updates).unwrap();
346
347        // Check storage changesets
348        assert_eq!(changesets.storage_tries_ref().len(), 1);
349        let storage_changesets = changesets.storage_tries_ref().get(&hashed_address).unwrap();
350        assert!(!storage_changesets.is_deleted);
351        assert_eq!(storage_changesets.storage_nodes.len(), 2);
352
353        // path1 should have the old node1 value
354        assert_eq!(storage_changesets.storage_nodes[0].0, path1);
355        assert_eq!(storage_changesets.storage_nodes[0].1, Some(node1));
356
357        // path3 should have None (it didn't exist before)
358        assert_eq!(storage_changesets.storage_nodes[1].0, path3);
359        assert_eq!(storage_changesets.storage_nodes[1].1, None);
360    }
361
362    #[test]
363    fn test_wiped_storage() {
364        let hashed_address = B256::from([2u8; 32]);
365
366        // Create initial storage trie with multiple nodes
367        let path1 = Nibbles::from_nibbles([0x1, 0x2]);
368        let path2 = Nibbles::from_nibbles([0x3, 0x4]);
369        let path3 = Nibbles::from_nibbles([0x5, 0x6]);
370        let node1 = BranchNodeCompact::new(0b1111, 0b0011, 0, vec![], None);
371        let node2 = BranchNodeCompact::new(0b1111, 0b0101, 0, vec![], None);
372        let node3 = BranchNodeCompact::new(0b1111, 0b1001, 0, vec![], None);
373
374        let mut storage_nodes = BTreeMap::new();
375        storage_nodes.insert(path1, node1.clone());
376        storage_nodes.insert(path2, node2.clone());
377        storage_nodes.insert(path3, node3.clone());
378
379        let mut storage_tries = B256Map::default();
380        storage_tries.insert(B256::default(), BTreeMap::new()); // For cursor creation
381        storage_tries.insert(hashed_address, storage_nodes);
382
383        let factory = MockTrieCursorFactory::new(BTreeMap::new(), storage_tries);
384
385        // Create updates that modify path1 and mark storage as wiped
386        let new_node1 = BranchNodeCompact::new(0b1111, 0b1000, 0, vec![], None);
387
388        let mut storage_updates = B256Map::default();
389        storage_updates.insert(
390            hashed_address,
391            StorageTrieUpdatesSorted {
392                is_deleted: true,
393                storage_nodes: vec![(path1, Some(new_node1))],
394            },
395        );
396
397        let updates = TrieUpdatesSorted::new(vec![], storage_updates);
398
399        // Compute changesets
400        let changesets = compute_trie_changesets(&factory, &updates).unwrap();
401
402        // Check storage changesets
403        assert_eq!(changesets.storage_tries_ref().len(), 1);
404        let storage_changesets = changesets.storage_tries_ref().get(&hashed_address).unwrap();
405        assert!(storage_changesets.is_deleted);
406
407        // Should include all three nodes (changed path1 + wiped path2 and path3)
408        assert_eq!(storage_changesets.storage_nodes.len(), 3);
409
410        // All paths should be present in sorted order
411        assert_eq!(storage_changesets.storage_nodes[0].0, path1);
412        assert_eq!(storage_changesets.storage_nodes[1].0, path2);
413        assert_eq!(storage_changesets.storage_nodes[2].0, path3);
414
415        // All should have their old values
416        assert_eq!(storage_changesets.storage_nodes[0].1, Some(node1));
417        assert_eq!(storage_changesets.storage_nodes[1].1, Some(node2));
418        assert_eq!(storage_changesets.storage_nodes[2].1, Some(node3));
419    }
420
421    #[test]
422    fn test_wiped_storage_with_new_path() {
423        let hashed_address = B256::from([3u8; 32]);
424
425        // Create initial storage trie with two nodes
426        let path1 = Nibbles::from_nibbles([0x1, 0x2]);
427        let path3 = Nibbles::from_nibbles([0x5, 0x6]);
428        let node1 = BranchNodeCompact::new(0b1111, 0b0011, 0, vec![], None);
429        let node3 = BranchNodeCompact::new(0b1111, 0b1001, 0, vec![], None);
430
431        let mut storage_nodes = BTreeMap::new();
432        storage_nodes.insert(path1, node1.clone());
433        storage_nodes.insert(path3, node3.clone());
434
435        let mut storage_tries = B256Map::default();
436        storage_tries.insert(B256::default(), BTreeMap::new()); // For cursor creation
437        storage_tries.insert(hashed_address, storage_nodes);
438
439        let factory = MockTrieCursorFactory::new(BTreeMap::new(), storage_tries);
440
441        // Create updates that add a new path2 that didn't exist before
442        let path2 = Nibbles::from_nibbles([0x3, 0x4]);
443        let new_node2 = BranchNodeCompact::new(0b1111, 0b0101, 0, vec![], None);
444
445        let mut storage_updates = B256Map::default();
446        storage_updates.insert(
447            hashed_address,
448            StorageTrieUpdatesSorted {
449                is_deleted: true,
450                storage_nodes: vec![(path2, Some(new_node2))],
451            },
452        );
453
454        let updates = TrieUpdatesSorted::new(vec![], storage_updates);
455
456        // Compute changesets
457        let changesets = compute_trie_changesets(&factory, &updates).unwrap();
458
459        // Check storage changesets
460        let storage_changesets = changesets.storage_tries_ref().get(&hashed_address).unwrap();
461        assert!(storage_changesets.is_deleted);
462
463        // Should include all three paths: existing path1, new path2, existing path3
464        assert_eq!(storage_changesets.storage_nodes.len(), 3);
465
466        // Check sorted order
467        assert_eq!(storage_changesets.storage_nodes[0].0, path1);
468        assert_eq!(storage_changesets.storage_nodes[1].0, path2);
469        assert_eq!(storage_changesets.storage_nodes[2].0, path3);
470
471        // path1 and path3 have old values, path2 has None (didn't exist)
472        assert_eq!(storage_changesets.storage_nodes[0].1, Some(node1));
473        assert_eq!(storage_changesets.storage_nodes[1].1, None);
474        assert_eq!(storage_changesets.storage_nodes[2].1, Some(node3));
475    }
476}