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
102pub(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 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 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 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 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 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 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 let database_not_exists = trie_cursor()?.seek(Nibbles::default())?.is_none();
197 let mut diff = StorageTrieUpdatesDiff {
198 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 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 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 !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
254fn 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
284fn 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}