Skip to main content

reth_cli_commands/db/
diff.rs

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)]
26/// The arguments for the `reth db diff` command
27pub struct Command {
28    /// The path to the data dir for all reth files and subdirectories.
29    #[arg(long, verbatim_doc_comment)]
30    secondary_datadir: PlatformPath<DataDirPath>,
31
32    /// Arguments for the second database
33    #[command(flatten)]
34    second_db: DatabaseArgs,
35
36    /// The table name to diff. If not specified, all tables are diffed.
37    #[arg(long, verbatim_doc_comment)]
38    table: Option<Tables>,
39
40    /// The output directory for the diff report.
41    #[arg(long, verbatim_doc_comment)]
42    output: PlatformPath<PathBuf>,
43}
44
45impl Command {
46    /// Execute the `db diff` command.
47    ///
48    /// This first opens the `db/` folder from the secondary datadir, where the second database is
49    /// opened read-only.
50    ///
51    /// The tool will then iterate through all key-value pairs for the primary and secondary
52    /// databases. The value for each key will be compared with its corresponding value in the
53    /// other database. If the values are different, a discrepancy will be recorded in-memory. If
54    /// one key is present in one database but not the other, this will be recorded as an "extra
55    /// element" for that database.
56    ///
57    /// The discrepancies and extra elements, along with a brief summary of the diff results are
58    /// then written to a file in the output directory.
59    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        // open second db
65        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            // disable long read transaction safety, since this will run for a while and it's
78            // expected that the node is not running
79            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
98/// Writes the diff report for a table, if there are any differences.
99fn write_report(table: &str, result: &TableDiffResult, output_dir: &Path) -> eyre::Result<()> {
100    // Pretty info summary header: newline then header
101    info!("");
102    info!("Diff results for {table}:");
103
104    // analyze the result and print some stats
105    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    // create directory and open file
114    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    // Make a pretty summary header for the table
119    writeln!(file, "Diff results for {table}")?;
120
121    if discrepancies > 0 {
122        // write to file
123        writeln!(file, "Found {discrepancies} discrepancies in table {table}")?;
124
125        // also print to info
126        info!("Found {discrepancies} discrepancies in table {table}");
127    } else {
128        // write to file
129        writeln!(file, "No discrepancies found in table {table}")?;
130
131        // also print to info
132        info!("No discrepancies found in table {table}");
133    }
134
135    if extra_elements > 0 {
136        // write to file
137        writeln!(file, "Found {extra_elements} extra elements in table {table}")?;
138
139        // also print to info
140        info!("Found {extra_elements} extra elements in table {table}");
141    } else {
142        writeln!(file, "No extra elements found in table {table}")?;
143
144        // also print to info
145        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
171/// This diff algorithm is slightly different, it will walk _each_ table, cross-checking for the
172/// element in the other table.
173fn 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    // initialize the zipped walker
181    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    // initialize the cursors for seeking when we are cross checking elements
191    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    // this loop will walk both tables, cross-checking for the element in the other table.
200    // it basically just loops through both tables at the same time. if the keys are different, it
201    // will check each key in the other table. if the keys are the same, it will compare the
202    // values
203    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            // if the keys are different, we need to check if the key is in the other table
209            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            // now do the same for the primary table
218            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            // the keys are the same, so we need to compare the values
227            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/// Includes a table element between two databases with the same key, but different values
239#[derive(Debug)]
240struct TableDiffElement<T: Table> {
241    /// The key for the element
242    #[expect(dead_code)]
243    key: T::Key,
244
245    /// The element from the first table
246    #[expect(dead_code)]
247    first: T::Value,
248
249    /// The element from the second table
250    #[expect(dead_code)]
251    second: T::Value,
252}
253
254/// The diff result for an entire table, with elements already formatted for the report. If the
255/// tables had the same number of elements, there will be no extra elements.
256///
257/// Both maps are keyed by the encoded table key, which sorts in database order.
258#[derive(Default)]
259struct TableDiffResult {
260    /// All elements of the database that are different
261    discrepancies: BTreeMap<Vec<u8>, String>,
262
263    /// Any extra elements, and the table they are in
264    extra_elements: BTreeMap<Vec<u8>, String>,
265}
266
267impl TableDiffResult {
268    /// Try to push a diff result into the discrepancy set, only pushing if the given elements are
269    /// different, and the discrepancy does not exist anywhere already.
270    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        // nothing to record if both elements are equal or missing
280        if first == second {
281            return
282        }
283
284        let encoded_key = key.clone().encode().into_vec();
285
286        // do not bother recording if the key is already in the discrepancies or extra elements
287        // maps
288        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/// A single extra element from a table
319#[derive(Debug)]
320enum ExtraTableElement<T: Table> {
321    /// The extra element that is in the first table
322    #[expect(dead_code)]
323    First { key: T::Key, value: T::Value },
324
325    /// The extra element that is in the second table
326    #[expect(dead_code)]
327    Second { key: T::Key, value: T::Value },
328}