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
90pub(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 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 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 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 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 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 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 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 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 !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
229fn 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
259fn 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}