1use clap::Parser;
2use reth_db::{open_db_read_only, tables_to_generic, DatabaseEnv};
3use reth_db_api::{
4 cursor::DbCursorRO,
5 database::Database,
6 table::{Encode, IntoVec, Table},
7 transaction::DbTx,
8 Tables,
9};
10use reth_db_common::DbTool;
11use reth_node_builder::{NodeTypes, NodeTypesWithDBAdapter};
12use reth_node_core::{
13 args::DatabaseArgs,
14 dirs::{DataDirPath, PlatformPath},
15};
16use std::{
17 collections::BTreeMap,
18 fmt::Debug,
19 fs::{self, File},
20 io::Write,
21 path::{Path, PathBuf},
22};
23use tracing::{info, warn};
24
25#[derive(Parser, Debug)]
26pub struct Command {
28 #[arg(long, verbatim_doc_comment)]
30 secondary_datadir: PlatformPath<DataDirPath>,
31
32 #[command(flatten)]
34 second_db: DatabaseArgs,
35
36 #[arg(long, verbatim_doc_comment)]
38 table: Option<Tables>,
39
40 #[arg(long, verbatim_doc_comment)]
42 output: PlatformPath<PathBuf>,
43}
44
45impl Command {
46 pub fn execute<T: NodeTypes>(
60 self,
61 tool: &DbTool<NodeTypesWithDBAdapter<T, DatabaseEnv>>,
62 ) -> eyre::Result<()> {
63 warn!("Make sure the node is not running when running `reth db diff`!");
64 let second_db_path: PathBuf = self.secondary_datadir.join("db").into();
66 let second_db = open_db_read_only(&second_db_path, self.second_db.database_args())?;
67
68 let tables = match &self.table {
69 Some(table) => std::slice::from_ref(table),
70 None => Tables::ALL,
71 };
72
73 for table in tables {
74 let mut primary_tx = tool.provider_factory.db_ref().tx()?;
75 let mut secondary_tx = second_db.tx()?;
76
77 primary_tx.disable_long_read_transaction_safety();
80 secondary_tx.disable_long_read_transaction_safety();
81
82 let table_name = table.name();
83
84 info!("Analyzing table {table_name}...");
85 let result = tables_to_generic!(table, |Table| find_diffs_advanced::<Table>(
86 &primary_tx,
87 &secondary_tx
88 ))?;
89 info!("Done analyzing table {table_name}!");
90
91 write_report(table_name, &result, self.output.as_ref())?;
92 }
93
94 Ok(())
95 }
96}
97
98fn write_report(table: &str, result: &TableDiffResult, output_dir: &Path) -> eyre::Result<()> {
100 info!("");
102 info!("Diff results for {table}:");
103
104 let discrepancies = result.discrepancies.len();
106 let extra_elements = result.extra_elements.len();
107
108 if discrepancies == 0 && extra_elements == 0 {
109 info!("No discrepancies or extra elements found in table {table}");
110 return Ok(());
111 }
112
113 fs::create_dir_all(output_dir)?;
115 let file_name = format!("{table}.txt");
116 let mut file = File::create(output_dir.join(&file_name))?;
117
118 writeln!(file, "Diff results for {table}")?;
120
121 if discrepancies > 0 {
122 writeln!(file, "Found {discrepancies} discrepancies in table {table}")?;
124
125 info!("Found {discrepancies} discrepancies in table {table}");
127 } else {
128 writeln!(file, "No discrepancies found in table {table}")?;
130
131 info!("No discrepancies found in table {table}");
133 }
134
135 if extra_elements > 0 {
136 writeln!(file, "Found {extra_elements} extra elements in table {table}")?;
138
139 info!("Found {extra_elements} extra elements in table {table}");
141 } else {
142 writeln!(file, "No extra elements found in table {table}")?;
143
144 info!("No extra elements found in table {table}");
146 }
147
148 info!("Writing diff results for {table} to {file_name}...");
149
150 if discrepancies > 0 {
151 writeln!(file, "Discrepancies:")?;
152 }
153
154 for discrepancy in result.discrepancies.values() {
155 writeln!(file, "{discrepancy}")?;
156 }
157
158 if extra_elements > 0 {
159 writeln!(file, "Extra elements:")?;
160 }
161
162 for extra_element in result.extra_elements.values() {
163 writeln!(file, "{extra_element}")?;
164 }
165
166 let full_file_name = output_dir.join(file_name);
167 info!("Done writing diff results for {table} to {}", full_file_name.display());
168 Ok(())
169}
170
171fn find_diffs_advanced<T: Table>(
174 primary_tx: &impl DbTx,
175 secondary_tx: &impl DbTx,
176) -> eyre::Result<TableDiffResult>
177where
178 T::Value: PartialEq,
179{
180 let mut primary_zip_cursor =
182 primary_tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
183 let primary_walker = primary_zip_cursor.walk(None)?;
184
185 let mut secondary_zip_cursor =
186 secondary_tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
187 let secondary_walker = secondary_zip_cursor.walk(None)?;
188 let zipped_cursor = primary_walker.zip(secondary_walker);
189
190 let mut primary_cursor =
192 primary_tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
193
194 let mut secondary_cursor =
195 secondary_tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
196
197 let mut result = TableDiffResult::default();
198
199 for (primary_entry, secondary_entry) in zipped_cursor {
204 let (primary_key, primary_value) = primary_entry?;
205 let (secondary_key, secondary_value) = secondary_entry?;
206
207 if primary_key != secondary_key {
208 let crossed_secondary =
210 secondary_cursor.seek_exact(primary_key.clone())?.map(|(_, value)| value);
211 result.try_push_discrepancy::<T>(
212 primary_key.clone(),
213 Some(primary_value),
214 crossed_secondary,
215 );
216
217 let crossed_primary =
219 primary_cursor.seek_exact(secondary_key.clone())?.map(|(_, value)| value);
220 result.try_push_discrepancy::<T>(
221 secondary_key.clone(),
222 crossed_primary,
223 Some(secondary_value),
224 );
225 } else {
226 result.try_push_discrepancy::<T>(
228 primary_key,
229 Some(primary_value),
230 Some(secondary_value),
231 );
232 }
233 }
234
235 Ok(result)
236}
237
238#[derive(Debug)]
240struct TableDiffElement<T: Table> {
241 #[expect(dead_code)]
243 key: T::Key,
244
245 #[expect(dead_code)]
247 first: T::Value,
248
249 #[expect(dead_code)]
251 second: T::Value,
252}
253
254#[derive(Default)]
259struct TableDiffResult {
260 discrepancies: BTreeMap<Vec<u8>, String>,
262
263 extra_elements: BTreeMap<Vec<u8>, String>,
265}
266
267impl TableDiffResult {
268 fn try_push_discrepancy<T>(
271 &mut self,
272 key: T::Key,
273 first: Option<T::Value>,
274 second: Option<T::Value>,
275 ) where
276 T: Table,
277 T::Value: PartialEq,
278 {
279 if first == second {
281 return
282 }
283
284 let encoded_key = key.clone().encode().into_vec();
285
286 if self.discrepancies.contains_key(&encoded_key) ||
289 self.extra_elements.contains_key(&encoded_key)
290 {
291 return
292 }
293
294 match (first, second) {
295 (Some(first), Some(second)) => {
296 self.discrepancies.insert(
297 encoded_key,
298 format!("{:#?}", TableDiffElement::<T> { key, first, second }),
299 );
300 }
301 (Some(first), None) => {
302 self.extra_elements.insert(
303 encoded_key,
304 format!("{:#?}", ExtraTableElement::<T>::First { key, value: first }),
305 );
306 }
307 (None, Some(second)) => {
308 self.extra_elements.insert(
309 encoded_key,
310 format!("{:#?}", ExtraTableElement::<T>::Second { key, value: second }),
311 );
312 }
313 (None, None) => {}
314 }
315 }
316}
317
318#[derive(Debug)]
320enum ExtraTableElement<T: Table> {
321 #[expect(dead_code)]
323 First { key: T::Key, value: T::Value },
324
325 #[expect(dead_code)]
327 Second { key: T::Key, value: T::Value },
328}