Skip to main content

reth_engine_tree/tree/
trie_updates.rs

1use alloy_primitives::{
2    map::{B256Map, HashMap},
3    B256,
4};
5use reth_db::DatabaseError;
6use reth_trie::{
7    trie_cursor::{TrieCursor, TrieCursorFactory},
8    updates::{StorageTrieUpdates, TrieUpdates},
9    BranchNodeCompact, Nibbles,
10};
11use std::collections::BTreeSet;
12use tracing::warn;
13
14#[derive(Debug)]
15struct EntryDiff<T> {
16    task: T,
17    regular: T,
18    database: T,
19}
20
21#[derive(Debug, Default)]
22struct TrieUpdatesDiff {
23    account_nodes: HashMap<Nibbles, EntryDiff<Option<BranchNodeCompact>>>,
24    removed_nodes: HashMap<Nibbles, EntryDiff<bool>>,
25    storage_tries: B256Map<StorageTrieUpdatesDiff>,
26}
27
28impl TrieUpdatesDiff {
29    fn has_differences(&self) -> bool {
30        !self.account_nodes.is_empty() ||
31            !self.removed_nodes.is_empty() ||
32            !self.storage_tries.is_empty()
33    }
34
35    pub(super) fn log_differences(mut self) {
36        if self.has_differences() {
37            for (path, EntryDiff { task, regular, database }) in &mut self.account_nodes {
38                warn!(target: "engine::tree", ?path, ?task, ?regular, ?database, "Difference in account trie updates");
39            }
40
41            for (
42                path,
43                EntryDiff {
44                    task: task_removed,
45                    regular: regular_removed,
46                    database: database_not_exists,
47                },
48            ) in &self.removed_nodes
49            {
50                warn!(target: "engine::tree", ?path, ?task_removed, ?regular_removed, ?database_not_exists, "Difference in removed account trie nodes");
51            }
52
53            for (address, storage_diff) in self.storage_tries {
54                storage_diff.log_differences(address);
55            }
56        }
57    }
58}
59
60#[derive(Debug, Default)]
61struct StorageTrieUpdatesDiff {
62    is_deleted: Option<EntryDiff<bool>>,
63    storage_nodes: HashMap<Nibbles, EntryDiff<Option<BranchNodeCompact>>>,
64    removed_nodes: HashMap<Nibbles, EntryDiff<bool>>,
65}
66
67impl StorageTrieUpdatesDiff {
68    fn has_differences(&self) -> bool {
69        self.is_deleted.is_some() ||
70            !self.storage_nodes.is_empty() ||
71            !self.removed_nodes.is_empty()
72    }
73
74    fn log_differences(&self, address: B256) {
75        if let Some(EntryDiff {
76            task: task_deleted,
77            regular: regular_deleted,
78            database: database_not_exists,
79        }) = self.is_deleted
80        {
81            warn!(target: "engine::tree", ?address, ?task_deleted, ?regular_deleted, ?database_not_exists, "Difference in storage trie deletion");
82        }
83
84        for (path, EntryDiff { task, regular, database }) in &self.storage_nodes {
85            warn!(target: "engine::tree", ?address, ?path, ?task, ?regular, ?database, "Difference in storage trie updates");
86        }
87
88        for (
89            path,
90            EntryDiff {
91                task: task_removed,
92                regular: regular_removed,
93                database: database_not_exists,
94            },
95        ) in &self.removed_nodes
96        {
97            warn!(target: "engine::tree", ?address, ?path, ?task_removed, ?regular_removed, ?database_not_exists, "Difference in removed storage trie nodes");
98        }
99    }
100}
101
102/// Compares the trie updates from state root task, regular state root calculation and database,
103/// and logs the differences if there's any.
104///
105/// Returns `true` if there are differences.
106pub(crate) fn compare_trie_updates(
107    trie_cursor_factory: impl TrieCursorFactory,
108    task: TrieUpdates,
109    regular: TrieUpdates,
110) -> Result<bool, DatabaseError> {
111    let mut task = adjust_trie_updates(task);
112    let mut regular = adjust_trie_updates(regular);
113
114    let mut diff = TrieUpdatesDiff::default();
115
116    // compare account nodes
117    let mut account_trie_cursor = trie_cursor_factory.account_trie_cursor()?;
118    for key in task
119        .account_nodes
120        .keys()
121        .chain(regular.account_nodes.keys())
122        .copied()
123        .collect::<BTreeSet<_>>()
124    {
125        let (task, regular) = (task.account_nodes.remove(&key), regular.account_nodes.remove(&key));
126        let database = account_trie_cursor.seek_exact(key)?.map(|x| x.1);
127
128        if !branch_nodes_equal(task.as_ref(), regular.as_ref(), database.as_ref())? {
129            diff.account_nodes.insert(key, EntryDiff { task, regular, database });
130        }
131    }
132
133    // compare removed nodes
134    let mut account_trie_cursor = trie_cursor_factory.account_trie_cursor()?;
135    for key in task
136        .removed_nodes
137        .iter()
138        .chain(regular.removed_nodes.iter())
139        .copied()
140        .collect::<BTreeSet<_>>()
141    {
142        let (task_removed, regular_removed) =
143            (task.removed_nodes.contains(&key), regular.removed_nodes.contains(&key));
144        let database_not_exists = account_trie_cursor.seek_exact(key)?.is_none();
145        // If the deletion is a no-op, meaning that the entry is not in the
146        // database, do not add it to the diff.
147        if task_removed != regular_removed && !database_not_exists {
148            diff.removed_nodes.insert(
149                key,
150                EntryDiff {
151                    task: task_removed,
152                    regular: regular_removed,
153                    database: database_not_exists,
154                },
155            );
156        }
157    }
158
159    // compare storage tries
160    for key in task
161        .storage_tries
162        .keys()
163        .chain(regular.storage_tries.keys())
164        .copied()
165        .collect::<BTreeSet<_>>()
166    {
167        let (mut task, mut regular) =
168            (task.storage_tries.remove(&key), regular.storage_tries.remove(&key));
169        if task != regular {
170            #[expect(clippy::or_fun_call)]
171            let storage_diff = compare_storage_trie_updates(
172                || trie_cursor_factory.storage_trie_cursor(key),
173                // Compare non-existent storage tries as empty.
174                task.as_mut().unwrap_or(&mut Default::default()),
175                regular.as_mut().unwrap_or(&mut Default::default()),
176            )?;
177            if storage_diff.has_differences() {
178                diff.storage_tries.insert(key, storage_diff);
179            }
180        }
181    }
182
183    // log differences
184    let has_differences = diff.has_differences();
185    diff.log_differences();
186
187    Ok(has_differences)
188}
189
190fn compare_storage_trie_updates<C: TrieCursor>(
191    trie_cursor: impl Fn() -> Result<C, DatabaseError>,
192    task: &mut StorageTrieUpdates,
193    regular: &mut StorageTrieUpdates,
194) -> Result<StorageTrieUpdatesDiff, DatabaseError> {
195    // Check if the storage trie exists by seeking to the first entry
196    let database_not_exists = trie_cursor()?.seek(Nibbles::default())?.is_none();
197    let mut diff = StorageTrieUpdatesDiff {
198        // If the deletion is a no-op, meaning that the entry is not in the
199        // database, do not add it to the diff.
200        is_deleted: (task.is_deleted != regular.is_deleted && !database_not_exists).then_some(
201            EntryDiff {
202                task: task.is_deleted,
203                regular: regular.is_deleted,
204                database: database_not_exists,
205            },
206        ),
207        ..Default::default()
208    };
209
210    // compare storage nodes
211    let mut storage_trie_cursor = trie_cursor()?;
212    for key in task
213        .storage_nodes
214        .keys()
215        .chain(regular.storage_nodes.keys())
216        .copied()
217        .collect::<BTreeSet<_>>()
218    {
219        let (task, regular) = (task.storage_nodes.remove(&key), regular.storage_nodes.remove(&key));
220        let database = storage_trie_cursor.seek_exact(key)?.map(|x| x.1);
221        if !branch_nodes_equal(task.as_ref(), regular.as_ref(), database.as_ref())? {
222            diff.storage_nodes.insert(key, EntryDiff { task, regular, database });
223        }
224    }
225
226    // compare removed nodes
227    let mut storage_trie_cursor = trie_cursor()?;
228    for key in
229        task.removed_nodes.iter().chain(regular.removed_nodes.iter()).collect::<BTreeSet<_>>()
230    {
231        let (task_removed, regular_removed) =
232            (task.removed_nodes.contains(key), regular.removed_nodes.contains(key));
233        if task_removed == regular_removed {
234            continue;
235        }
236        let database_not_exists = storage_trie_cursor.seek_exact(*key)?.map(|x| x.1).is_none();
237        // If the deletion is a no-op, meaning that the entry is not in the
238        // database, do not add it to the diff.
239        if !database_not_exists {
240            diff.removed_nodes.insert(
241                *key,
242                EntryDiff {
243                    task: task_removed,
244                    regular: regular_removed,
245                    database: database_not_exists,
246                },
247            );
248        }
249    }
250
251    Ok(diff)
252}
253
254/// Filters the removed nodes of both account trie updates and storage trie updates, so that they
255/// don't include those nodes that were also updated.
256fn adjust_trie_updates(trie_updates: TrieUpdates) -> TrieUpdates {
257    TrieUpdates {
258        removed_nodes: trie_updates
259            .removed_nodes
260            .into_iter()
261            .filter(|key| !trie_updates.account_nodes.contains_key(key))
262            .collect(),
263        storage_tries: trie_updates
264            .storage_tries
265            .into_iter()
266            .map(|(address, updates)| {
267                (
268                    address,
269                    StorageTrieUpdates {
270                        removed_nodes: updates
271                            .removed_nodes
272                            .into_iter()
273                            .filter(|key| !updates.storage_nodes.contains_key(key))
274                            .collect(),
275                        ..updates
276                    },
277                )
278            })
279            .collect(),
280        ..trie_updates
281    }
282}
283
284/// Compares the branch nodes from state root task and regular state root calculation.
285///
286/// If one of the branch nodes is [`None`], it means it's not updated and the other is compared to
287/// the branch node from the database.
288///
289/// Returns `true` if they are equal.
290fn branch_nodes_equal(
291    task: Option<&BranchNodeCompact>,
292    regular: Option<&BranchNodeCompact>,
293    database: Option<&BranchNodeCompact>,
294) -> Result<bool, DatabaseError> {
295    Ok(match (task, regular) {
296        (Some(task), Some(regular)) => {
297            task.state_mask == regular.state_mask &&
298                task.tree_mask == regular.tree_mask &&
299                task.hash_mask == regular.hash_mask &&
300                task.hashes == regular.hashes &&
301                task.root_hash == regular.root_hash
302        }
303        (None, None) => true,
304        _ => {
305            if task.is_some() {
306                task == database
307            } else {
308                regular == database
309            }
310        }
311    })
312}