Skip to main content

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::{TrieCursor, TrieCursorFactory, TrieStorageCursor};
22use alloy_primitives::{map::B256Map, B256};
23use reth_storage_errors::db::DatabaseError;
24use reth_trie_common::{
25    updates::{StorageTrieUpdatesSorted, TrieUpdatesSorted},
26    BranchNodeCompact, Nibbles,
27};
28
29/// Result type for changeset operations.
30pub type ChangesetResult<T> = Result<T, DatabaseError>;
31
32/// Computes trie changesets by looking up current node values from the trie.
33///
34/// Takes the new trie updates and queries the trie for the old values of
35/// changed nodes. Returns changesets representing the state before the block
36/// was applied, suitable for reorg operations.
37///
38/// # Arguments
39///
40/// * `factory` - Trie cursor factory for reading current trie state
41/// * `trie_updates` - New trie node values produced by state root computation
42///
43/// # Returns
44///
45/// `TrieUpdatesSorted` containing old node values (before this block)
46pub fn compute_trie_changesets<Factory>(
47    factory: &Factory,
48    trie_updates: &TrieUpdatesSorted,
49) -> ChangesetResult<TrieUpdatesSorted>
50where
51    Factory: TrieCursorFactory,
52{
53    // Compute account trie changesets
54    let account_nodes = compute_account_changesets(factory, trie_updates)?;
55
56    // Compute storage trie changesets
57    let mut storage_tries = B256Map::default();
58
59    // Create storage cursor once and reuse it for all addresses
60    let mut storage_cursor = factory.storage_trie_cursor(B256::default())?;
61
62    for (hashed_address, storage_updates) in trie_updates.storage_tries_ref() {
63        storage_cursor.set_hashed_address(*hashed_address);
64
65        let storage_changesets = compute_storage_changesets(&mut storage_cursor, storage_updates)?;
66
67        if !storage_changesets.is_empty() {
68            storage_tries.insert(
69                *hashed_address,
70                StorageTrieUpdatesSorted { storage_nodes: storage_changesets },
71            );
72        }
73    }
74
75    // Build and return the result
76    Ok(TrieUpdatesSorted::new(account_nodes, storage_tries))
77}
78
79/// Computes account trie changesets.
80///
81/// Looks up the current value for each changed account node path and returns
82/// a vector of (path, `old_node`) pairs. The result is already sorted since
83/// `trie_updates.account_nodes_ref()` is sorted.
84fn compute_account_changesets<Factory>(
85    factory: &Factory,
86    trie_updates: &TrieUpdatesSorted,
87) -> ChangesetResult<Vec<(Nibbles, Option<BranchNodeCompact>)>>
88where
89    Factory: TrieCursorFactory,
90{
91    let mut cursor = factory.account_trie_cursor()?;
92    let mut account_changesets = Vec::with_capacity(trie_updates.account_nodes_ref().len());
93
94    // For each changed account node, look up its current value
95    // The input is already sorted, so the output will be sorted
96    for (path, _new_node) in trie_updates.account_nodes_ref() {
97        let old_node = cursor.seek_exact(*path)?.map(|(_path, node)| node);
98        account_changesets.push((*path, old_node));
99    }
100
101    Ok(account_changesets)
102}
103
104/// Computes storage trie changesets for a single account.
105///
106/// Looks up the current value for each changed storage node path and returns
107/// a vector of (path, `old_node`) pairs. The result is already sorted since
108/// `storage_updates.storage_nodes` is sorted.
109///
110/// # Arguments
111///
112/// * `cursor` - Reusable storage trie cursor. The hashed address will be set before use.
113/// * `hashed_address` - The hashed address of the account
114/// * `storage_updates` - Storage trie updates for this account
115fn compute_storage_changesets(
116    cursor: &mut impl TrieStorageCursor,
117    storage_updates: &StorageTrieUpdatesSorted,
118) -> ChangesetResult<Vec<(Nibbles, Option<BranchNodeCompact>)>> {
119    let mut storage_changesets = Vec::with_capacity(storage_updates.storage_nodes.len());
120
121    // For each changed storage node, look up its current value
122    // The input is already sorted, so the output will be sorted
123    for (path, _new_node) in &storage_updates.storage_nodes {
124        let old_node = cursor.seek_exact(*path)?.map(|(_path, node)| node);
125        storage_changesets.push((*path, old_node));
126    }
127
128    Ok(storage_changesets)
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::trie_cursor::mock::MockTrieCursorFactory;
135    use alloy_primitives::map::B256Map;
136    use reth_trie_common::updates::StorageTrieUpdatesSorted;
137    use std::collections::BTreeMap;
138
139    #[test]
140    fn test_empty_updates() {
141        // Create an empty mock factory
142        // Note: We need to include B256::default() in storage_tries because
143        // compute_trie_changesets creates cursors for it upfront
144        let mut storage_tries = B256Map::default();
145        storage_tries.insert(B256::default(), BTreeMap::new());
146        let factory = MockTrieCursorFactory::new(BTreeMap::new(), storage_tries);
147
148        // Create empty updates
149        let updates = TrieUpdatesSorted::new(vec![], B256Map::default());
150
151        // Compute changesets
152        let changesets = compute_trie_changesets(&factory, &updates).unwrap();
153
154        // Should produce empty changesets
155        assert!(changesets.account_nodes_ref().is_empty());
156        assert!(changesets.storage_tries_ref().is_empty());
157    }
158
159    #[test]
160    fn test_account_changesets() {
161        // Create some initial account trie state
162        let path1 = Nibbles::from_nibbles([0x1, 0x2, 0x3]);
163        let path2 = Nibbles::from_nibbles([0x4, 0x5, 0x6]);
164        // tree_mask and hash_mask must be subsets of state_mask
165        let node1 = BranchNodeCompact::new(0b1111, 0b1010, 0, vec![], None);
166        let node2 = BranchNodeCompact::new(0b1111, 0b1100, 0, vec![], None);
167
168        let mut account_nodes = BTreeMap::new();
169        account_nodes.insert(path1, node1.clone());
170        account_nodes.insert(path2, node2);
171
172        // Need to include B256::default() for cursor creation
173        let mut storage_tries = B256Map::default();
174        storage_tries.insert(B256::default(), BTreeMap::new());
175        let factory = MockTrieCursorFactory::new(account_nodes, storage_tries);
176
177        // Create updates that modify path1 and add a new path3
178        let path3 = Nibbles::from_nibbles([0x7, 0x8, 0x9]);
179        let new_node1 = BranchNodeCompact::new(0b1111, 0b0001, 0, vec![], None);
180        let new_node3 = BranchNodeCompact::new(0b1111, 0b0000, 0, vec![], None);
181
182        let updates = TrieUpdatesSorted::new(
183            vec![(path1, Some(new_node1)), (path3, Some(new_node3))],
184            B256Map::default(),
185        );
186
187        // Compute changesets
188        let changesets = compute_trie_changesets(&factory, &updates).unwrap();
189
190        // Check account changesets
191        assert_eq!(changesets.account_nodes_ref().len(), 2);
192
193        // path1 should have the old node1 value
194        assert_eq!(changesets.account_nodes_ref()[0].0, path1);
195        assert_eq!(changesets.account_nodes_ref()[0].1, Some(node1));
196
197        // path3 should have None (it didn't exist before)
198        assert_eq!(changesets.account_nodes_ref()[1].0, path3);
199        assert_eq!(changesets.account_nodes_ref()[1].1, None);
200    }
201
202    #[test]
203    fn test_storage_changesets() {
204        let hashed_address = B256::from([1u8; 32]);
205
206        // Create some initial storage trie state
207        let path1 = Nibbles::from_nibbles([0x1, 0x2]);
208        let path2 = Nibbles::from_nibbles([0x3, 0x4]);
209        let node1 = BranchNodeCompact::new(0b1111, 0b0011, 0, vec![], None);
210        let node2 = BranchNodeCompact::new(0b1111, 0b0101, 0, vec![], None);
211
212        let mut storage_nodes = BTreeMap::new();
213        storage_nodes.insert(path1, node1.clone());
214        storage_nodes.insert(path2, node2);
215
216        let mut storage_tries = B256Map::default();
217        storage_tries.insert(B256::default(), BTreeMap::new()); // For cursor creation
218        storage_tries.insert(hashed_address, storage_nodes);
219
220        let factory = MockTrieCursorFactory::new(BTreeMap::new(), storage_tries);
221
222        // Create updates that modify path1 and add a new path3
223        let path3 = Nibbles::from_nibbles([0x5, 0x6]);
224        let new_node1 = BranchNodeCompact::new(0b1111, 0b1000, 0, vec![], None);
225        let new_node3 = BranchNodeCompact::new(0b1111, 0b0000, 0, vec![], None);
226
227        let mut storage_updates = B256Map::default();
228        storage_updates.insert(
229            hashed_address,
230            StorageTrieUpdatesSorted {
231                storage_nodes: vec![(path1, Some(new_node1)), (path3, Some(new_node3))],
232            },
233        );
234
235        let updates = TrieUpdatesSorted::new(vec![], storage_updates);
236
237        // Compute changesets
238        let changesets = compute_trie_changesets(&factory, &updates).unwrap();
239
240        // Check storage changesets
241        assert_eq!(changesets.storage_tries_ref().len(), 1);
242        let storage_changesets = changesets.storage_tries_ref().get(&hashed_address).unwrap();
243        assert_eq!(storage_changesets.storage_nodes.len(), 2);
244
245        // path1 should have the old node1 value
246        assert_eq!(storage_changesets.storage_nodes[0].0, path1);
247        assert_eq!(storage_changesets.storage_nodes[0].1, Some(node1));
248
249        // path3 should have None (it didn't exist before)
250        assert_eq!(storage_changesets.storage_nodes[1].0, path3);
251        assert_eq!(storage_changesets.storage_nodes[1].1, None);
252    }
253}