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    storage_nodes: HashMap<Nibbles, EntryDiff<Option<BranchNodeCompact>>>,
63    removed_nodes: HashMap<Nibbles, EntryDiff<bool>>,
64}
65
66impl StorageTrieUpdatesDiff {
67    fn has_differences(&self) -> bool {
68        !self.storage_nodes.is_empty() || !self.removed_nodes.is_empty()
69    }
70
71    fn log_differences(&self, address: B256) {
72        for (path, EntryDiff { task, regular, database }) in &self.storage_nodes {
73            warn!(target: "engine::tree", ?address, ?path, ?task, ?regular, ?database, "Difference in storage trie updates");
74        }
75
76        for (
77            path,
78            EntryDiff {
79                task: task_removed,
80                regular: regular_removed,
81                database: database_not_exists,
82            },
83        ) in &self.removed_nodes
84        {
85            warn!(target: "engine::tree", ?address, ?path, ?task_removed, ?regular_removed, ?database_not_exists, "Difference in removed storage trie nodes");
86        }
87    }
88}
89
90/// Compares the trie updates from state root task, regular state root calculation and database,
91/// and logs the differences if there's any.
92///
93/// Returns `true` if there are differences.
94pub(crate) fn compare_trie_updates(
95    trie_cursor_factory: impl TrieCursorFactory,
96    task: TrieUpdates,
97    regular: TrieUpdates,
98) -> Result<bool, DatabaseError> {
99    let mut task = adjust_trie_updates(task);
100    let mut regular = adjust_trie_updates(regular);
101
102    let mut diff = TrieUpdatesDiff::default();
103
104    // compare account nodes
105    let mut account_trie_cursor = trie_cursor_factory.account_trie_cursor()?;
106    for key in task
107        .account_nodes
108        .keys()
109        .chain(regular.account_nodes.keys())
110        .copied()
111        .collect::<BTreeSet<_>>()
112    {
113        let (task, regular) = (task.account_nodes.remove(&key), regular.account_nodes.remove(&key));
114        let database = account_trie_cursor.seek_exact(key)?.map(|x| x.1);
115
116        if !branch_nodes_equal(task.as_ref(), regular.as_ref(), database.as_ref())? {
117            diff.account_nodes.insert(key, EntryDiff { task, regular, database });
118        }
119    }
120
121    // compare removed nodes
122    let mut account_trie_cursor = trie_cursor_factory.account_trie_cursor()?;
123    for key in task
124        .removed_nodes
125        .iter()
126        .chain(regular.removed_nodes.iter())
127        .copied()
128        .collect::<BTreeSet<_>>()
129    {
130        let (task_removed, regular_removed) =
131            (task.removed_nodes.contains(&key), regular.removed_nodes.contains(&key));
132        let database_not_exists = account_trie_cursor.seek_exact(key)?.is_none();
133        // If the deletion is a no-op, meaning that the entry is not in the
134        // database, do not add it to the diff.
135        if task_removed != regular_removed && !database_not_exists {
136            diff.removed_nodes.insert(
137                key,
138                EntryDiff {
139                    task: task_removed,
140                    regular: regular_removed,
141                    database: database_not_exists,
142                },
143            );
144        }
145    }
146
147    // compare storage tries
148    for key in task
149        .storage_tries
150        .keys()
151        .chain(regular.storage_tries.keys())
152        .copied()
153        .collect::<BTreeSet<_>>()
154    {
155        let (mut task, mut regular) =
156            (task.storage_tries.remove(&key), regular.storage_tries.remove(&key));
157        if task != regular {
158            #[expect(clippy::or_fun_call)]
159            let storage_diff = compare_storage_trie_updates(
160                || trie_cursor_factory.storage_trie_cursor(key),
161                // Compare non-existent storage tries as empty.
162                task.as_mut().unwrap_or(&mut Default::default()),
163                regular.as_mut().unwrap_or(&mut Default::default()),
164            )?;
165            if storage_diff.has_differences() {
166                diff.storage_tries.insert(key, storage_diff);
167            }
168        }
169    }
170
171    // log differences
172    let has_differences = diff.has_differences();
173    diff.log_differences();
174
175    Ok(has_differences)
176}
177
178fn compare_storage_trie_updates<C: TrieCursor>(
179    trie_cursor: impl Fn() -> Result<C, DatabaseError>,
180    task: &mut StorageTrieUpdates,
181    regular: &mut StorageTrieUpdates,
182) -> Result<StorageTrieUpdatesDiff, DatabaseError> {
183    let mut diff = StorageTrieUpdatesDiff::default();
184
185    // compare storage nodes
186    let mut storage_trie_cursor = trie_cursor()?;
187    for key in task
188        .storage_nodes
189        .keys()
190        .chain(regular.storage_nodes.keys())
191        .copied()
192        .collect::<BTreeSet<_>>()
193    {
194        let (task, regular) = (task.storage_nodes.remove(&key), regular.storage_nodes.remove(&key));
195        let database = storage_trie_cursor.seek_exact(key)?.map(|x| x.1);
196        if !branch_nodes_equal(task.as_ref(), regular.as_ref(), database.as_ref())? {
197            diff.storage_nodes.insert(key, EntryDiff { task, regular, database });
198        }
199    }
200
201    // compare removed nodes
202    let mut storage_trie_cursor = trie_cursor()?;
203    for key in
204        task.removed_nodes.iter().chain(regular.removed_nodes.iter()).collect::<BTreeSet<_>>()
205    {
206        let (task_removed, regular_removed) =
207            (task.removed_nodes.contains(key), regular.removed_nodes.contains(key));
208        if task_removed == regular_removed {
209            continue;
210        }
211        let database_not_exists = storage_trie_cursor.seek_exact(*key)?.map(|x| x.1).is_none();
212        // If the deletion is a no-op, meaning that the entry is not in the
213        // database, do not add it to the diff.
214        if !database_not_exists {
215            diff.removed_nodes.insert(
216                *key,
217                EntryDiff {
218                    task: task_removed,
219                    regular: regular_removed,
220                    database: database_not_exists,
221                },
222            );
223        }
224    }
225
226    Ok(diff)
227}
228
229/// Filters the removed nodes of both account trie updates and storage trie updates, so that they
230/// don't include those nodes that were also updated.
231fn adjust_trie_updates(trie_updates: TrieUpdates) -> TrieUpdates {
232    TrieUpdates {
233        removed_nodes: trie_updates
234            .removed_nodes
235            .into_iter()
236            .filter(|key| !trie_updates.account_nodes.contains_key(key))
237            .collect(),
238        storage_tries: trie_updates
239            .storage_tries
240            .into_iter()
241            .map(|(address, updates)| {
242                (
243                    address,
244                    StorageTrieUpdates {
245                        removed_nodes: updates
246                            .removed_nodes
247                            .into_iter()
248                            .filter(|key| !updates.storage_nodes.contains_key(key))
249                            .collect(),
250                        ..updates
251                    },
252                )
253            })
254            .collect(),
255        ..trie_updates
256    }
257}
258
259/// Compares the branch nodes from state root task and regular state root calculation.
260///
261/// If one of the branch nodes is [`None`], it means it's not updated and the other is compared to
262/// the branch node from the database.
263///
264/// Returns `true` if they are equal.
265fn branch_nodes_equal(
266    task: Option<&BranchNodeCompact>,
267    regular: Option<&BranchNodeCompact>,
268    database: Option<&BranchNodeCompact>,
269) -> Result<bool, DatabaseError> {
270    Ok(match (task, regular) {
271        (Some(task), Some(regular)) => {
272            task.state_mask == regular.state_mask &&
273                task.tree_mask == regular.tree_mask &&
274                task.hash_mask == regular.hash_mask &&
275                task.hashes == regular.hashes &&
276                task.root_hash == regular.root_hash
277        }
278        (None, None) => true,
279        _ => {
280            if task.is_some() {
281                task == database
282            } else {
283                regular == database
284            }
285        }
286    })
287}