Skip to main content

reth_cli_commands/db/
repair_trie.rs

1use alloy_consensus::BlockHeader as AlloyBlockHeader;
2use clap::{builder::RangedU64ValueParser, Parser};
3use metrics::{self, Counter};
4use reth_chainspec::EthChainSpec;
5use reth_cli_util::parse_socket_address;
6use reth_db::DatabaseEnv;
7use reth_db_api::{
8    cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO},
9    transaction::{DbTx, DbTxMut},
10};
11use reth_db_common::DbTool;
12use reth_node_core::{
13    dirs::{ChainPath, DataDirPath},
14    version::version_metadata,
15};
16use reth_node_metrics::{
17    chain::ChainSpecInfo,
18    hooks::Hooks,
19    server::{MetricServer, MetricServerConfig},
20    version::VersionInfo,
21};
22use reth_provider::{
23    providers::ProviderNodeTypes, ChainSpecProvider, HeaderProvider, StageCheckpointReader,
24};
25use reth_stages::StageId;
26use reth_storage_api::StorageSettingsCache;
27use reth_tasks::TaskExecutor;
28use reth_trie::{
29    verify::{default_storage_workers, Output, DEFAULT_STORAGE_LOOKAHEAD},
30    Nibbles,
31};
32use reth_trie_db::{
33    DatabaseHashedCursorFactory, DatabaseStateRoot, DatabaseTrieCursorFactory, DatabaseVerifier,
34    StorageTrieEntryLike, TrieTableAdapter,
35};
36use std::{
37    net::SocketAddr,
38    time::{Duration, Instant},
39};
40use tracing::{info, warn};
41
42const PROGRESS_PERIOD: Duration = Duration::from_secs(5);
43
44/// The arguments for the `reth db repair-trie` command
45#[derive(Parser, Debug)]
46pub struct Command {
47    /// Only show inconsistencies without making any repairs.
48    ///
49    /// Requires the node to be stopped so all workers read the same database snapshot.
50    #[arg(long)]
51    pub(crate) dry_run: bool,
52
53    /// Number of storage verification workers. Defaults to the number of available CPUs.
54    #[arg(long, default_value_t = default_storage_workers(), hide_default_value = true, value_parser = RangedU64ValueParser::<usize>::new().range(1..))]
55    pub(crate) storage_workers: usize,
56
57    /// Maximum storage roots computed or in flight ahead of the account walk.
58    #[arg(long, default_value_t = DEFAULT_STORAGE_LOOKAHEAD, value_parser = RangedU64ValueParser::<usize>::new().range(1..))]
59    pub(crate) storage_lookahead: usize,
60
61    /// Enable Prometheus metrics.
62    ///
63    /// The metrics will be served at the given interface and port.
64    #[arg(long = "metrics", value_name = "ADDR:PORT", value_parser = parse_socket_address)]
65    pub(crate) metrics: Option<SocketAddr>,
66}
67
68impl Command {
69    /// Execute `db repair-trie` command
70    pub fn execute<N: ProviderNodeTypes<DB = DatabaseEnv>>(
71        self,
72        tool: &DbTool<N>,
73        task_executor: TaskExecutor,
74        data_dir: &ChainPath<DataDirPath>,
75    ) -> eyre::Result<()> {
76        // Set up metrics server if requested
77        let _metrics_handle = if let Some(listen_addr) = self.metrics {
78            let chain_name = tool.provider_factory.chain_spec().chain().to_string();
79            let executor = task_executor.clone();
80            let pprof_dump_dir = data_dir.pprof_dumps();
81
82            let handle = task_executor.spawn_critical_task("metrics server", async move {
83                let config = MetricServerConfig::new(
84                    listen_addr,
85                    VersionInfo {
86                        version: version_metadata().cargo_pkg_version.as_ref(),
87                        build_timestamp: version_metadata().vergen_build_timestamp.as_ref(),
88                        cargo_features: version_metadata().vergen_cargo_features.as_ref(),
89                        git_sha: version_metadata().vergen_git_sha.as_ref(),
90                        target_triple: version_metadata().vergen_cargo_target_triple.as_ref(),
91                        build_profile: version_metadata().build_profile_name.as_ref(),
92                    },
93                    ChainSpecInfo { name: chain_name },
94                    executor,
95                    Hooks::builder().build(),
96                    pprof_dump_dir,
97                );
98
99                // Spawn the metrics server
100                if let Err(e) = MetricServer::new(config).serve().await {
101                    tracing::error!("Metrics server error: {}", e);
102                }
103            });
104
105            Some(handle)
106        } else {
107            None
108        };
109
110        if self.dry_run {
111            verify_only(tool, self.storage_workers, self.storage_lookahead)?
112        } else {
113            verify_and_repair(tool, self.storage_workers, self.storage_lookahead)?
114        }
115
116        Ok(())
117    }
118}
119
120fn verify_only<N: ProviderNodeTypes<DB = DatabaseEnv>>(
121    tool: &DbTool<N>,
122    storage_workers: usize,
123    storage_lookahead: usize,
124) -> eyre::Result<()> {
125    // Log the database block tip from Finish stage checkpoint
126    let finish_checkpoint = tool
127        .provider_factory
128        .provider()?
129        .get_stage_checkpoint(StageId::Finish)?
130        .unwrap_or_default();
131    info!("Database block tip: {}", finish_checkpoint.block_number);
132
133    let db = tool.provider_factory.db_ref();
134    reth_trie_db::with_adapter!(tool.provider_factory, |A| do_verify_only::<A>(
135        db,
136        storage_workers,
137        storage_lookahead
138    ))
139}
140
141fn do_verify_only<A: TrieTableAdapter>(
142    db: &DatabaseEnv,
143    storage_workers: usize,
144    storage_lookahead: usize,
145) -> eyre::Result<()> {
146    let db = db.clone().with_disable_long_read_transaction_safety();
147    let verifier = DatabaseVerifier::<_, A>::new(&db, storage_workers, storage_lookahead)?;
148
149    let metrics = RepairTrieMetrics::new();
150
151    let mut inconsistent_nodes = 0;
152    let start_time = Instant::now();
153    let mut last_progress_time = Instant::now();
154
155    // Iterate over the verifier and repair inconsistencies
156    for output_result in verifier {
157        let output = output_result?;
158
159        if let Output::Progress(path) = output {
160            if last_progress_time.elapsed() > PROGRESS_PERIOD {
161                output_progress(path, start_time, inconsistent_nodes);
162                last_progress_time = Instant::now();
163            }
164        } else {
165            warn!("Inconsistency found: {output:?}");
166            inconsistent_nodes += 1;
167
168            // Record metrics based on output type
169            match output {
170                Output::AccountExtra(_, _) |
171                Output::AccountWrong { .. } |
172                Output::AccountMissing(_, _) => {
173                    metrics.account_inconsistencies.increment(1);
174                }
175                Output::StorageExtra(_, _, _) |
176                Output::StorageWrong { .. } |
177                Output::StorageMissing(_, _, _) => {
178                    metrics.storage_inconsistencies.increment(1);
179                }
180                Output::Progress(_) => unreachable!(),
181            }
182        }
183    }
184
185    info!("Found {} inconsistencies (dry run - no changes made)", inconsistent_nodes);
186
187    Ok(())
188}
189
190/// Checks that the merkle stage has completed running up to the account and storage hashing stages.
191fn verify_checkpoints(provider: impl StageCheckpointReader) -> eyre::Result<()> {
192    let account_hashing_checkpoint =
193        provider.get_stage_checkpoint(StageId::AccountHashing)?.unwrap_or_default();
194    let storage_hashing_checkpoint =
195        provider.get_stage_checkpoint(StageId::StorageHashing)?.unwrap_or_default();
196    let merkle_checkpoint =
197        provider.get_stage_checkpoint(StageId::MerkleExecute)?.unwrap_or_default();
198
199    if account_hashing_checkpoint.block_number != merkle_checkpoint.block_number {
200        return Err(eyre::eyre!(
201            "MerkleExecute stage checkpoint ({}) != AccountHashing stage checkpoint ({}), you must first complete the pipeline sync by running `reth node`",
202            merkle_checkpoint.block_number,
203            account_hashing_checkpoint.block_number,
204        ))
205    }
206
207    if storage_hashing_checkpoint.block_number != merkle_checkpoint.block_number {
208        return Err(eyre::eyre!(
209            "MerkleExecute stage checkpoint ({}) != StorageHashing stage checkpoint ({}), you must first complete the pipeline sync by running `reth node`",
210            merkle_checkpoint.block_number,
211            storage_hashing_checkpoint.block_number,
212        ))
213    }
214
215    let merkle_checkpoint_progress =
216        provider.get_stage_checkpoint_progress(StageId::MerkleExecute)?;
217    if merkle_checkpoint_progress.is_some_and(|progress| !progress.is_empty()) {
218        return Err(eyre::eyre!(
219            "MerkleExecute sync stage in-progress, you must first complete the pipeline sync by running `reth node`",
220        ))
221    }
222
223    Ok(())
224}
225
226fn verify_and_repair<N: ProviderNodeTypes<DB = DatabaseEnv>>(
227    tool: &DbTool<N>,
228    storage_workers: usize,
229    storage_lookahead: usize,
230) -> eyre::Result<()> {
231    // Get a read-write database provider
232    let mut provider_rw = tool.provider_factory.provider_rw()?;
233
234    // Log the database block tip from Finish stage checkpoint
235    let finish_checkpoint = provider_rw.get_stage_checkpoint(StageId::Finish)?.unwrap_or_default();
236    info!("Database block tip: {}", finish_checkpoint.block_number);
237
238    // Check that a pipeline sync isn't in progress.
239    verify_checkpoints(provider_rw.as_ref())?;
240
241    let inconsistent_nodes = reth_trie_db::with_adapter!(tool.provider_factory, |A| {
242        do_verify_and_repair::<_, A>(
243            &mut provider_rw,
244            tool.provider_factory.db_ref(),
245            finish_checkpoint.block_number,
246            storage_workers,
247            storage_lookahead,
248        )?
249    });
250
251    if inconsistent_nodes == 0 {
252        info!("No inconsistencies found");
253    } else {
254        provider_rw.commit()?;
255        info!("Repaired {} inconsistencies and committed changes", inconsistent_nodes);
256    }
257
258    Ok(())
259}
260
261fn do_verify_and_repair<N: ProviderNodeTypes<DB = DatabaseEnv>, A: TrieTableAdapter>(
262    provider_rw: &mut reth_provider::DatabaseProviderRW<N::DB, N>,
263    db: &N::DB,
264    block_number: u64,
265    storage_workers: usize,
266    storage_lookahead: usize,
267) -> eyre::Result<usize> {
268    // Create cursors for making modifications with
269    let tx = provider_rw.tx_mut();
270    tx.disable_long_read_transaction_safety();
271    let mut account_trie_cursor = tx.cursor_write::<A::AccountTrieTable>()?;
272    let mut storage_trie_cursor = tx.cursor_dup_write::<A::StorageTrieTable>()?;
273
274    // Open every read snapshot before any repairs are applied.
275    let db = db.clone().with_disable_long_read_transaction_safety();
276    let verifier = DatabaseVerifier::<_, A>::new(&db, storage_workers, storage_lookahead)?;
277
278    let metrics = RepairTrieMetrics::new();
279
280    let mut inconsistent_nodes = 0;
281    let start_time = Instant::now();
282    let mut last_progress_time = Instant::now();
283
284    // Iterate over the verifier and repair inconsistencies
285    for output_result in verifier {
286        let output = output_result?;
287
288        if !matches!(output, Output::Progress(_)) {
289            warn!("Inconsistency found, will repair: {output:?}");
290            inconsistent_nodes += 1;
291
292            // Record metrics based on output type
293            match &output {
294                Output::AccountExtra(_, _) |
295                Output::AccountWrong { .. } |
296                Output::AccountMissing(_, _) => {
297                    metrics.account_inconsistencies.increment(1);
298                }
299                Output::StorageExtra(_, _, _) |
300                Output::StorageWrong { .. } |
301                Output::StorageMissing(_, _, _) => {
302                    metrics.storage_inconsistencies.increment(1);
303                }
304                Output::Progress(_) => {}
305            }
306        }
307
308        match output {
309            Output::AccountExtra(path, _node) => {
310                // Extra account node in trie, remove it
311                let key: A::AccountKey = path.into();
312                if account_trie_cursor.seek_exact(key)?.is_some() {
313                    account_trie_cursor.delete_current()?;
314                }
315            }
316            Output::StorageExtra(account, path, _node) => {
317                // Extra storage node in trie, remove it
318                let subkey: A::StorageSubKey = path.into();
319                if storage_trie_cursor
320                    .seek_by_key_subkey(account, subkey.clone())?
321                    .filter(|e| *e.nibbles() == subkey)
322                    .is_some()
323                {
324                    storage_trie_cursor.delete_current()?;
325                }
326            }
327            Output::AccountWrong { path, expected: node, .. } |
328            Output::AccountMissing(path, node) => {
329                // Wrong/missing account node value, upsert it
330                let key: A::AccountKey = path.into();
331                account_trie_cursor.upsert(key, &node)?;
332            }
333            Output::StorageWrong { account, path, expected: node, .. } |
334            Output::StorageMissing(account, path, node) => {
335                // Wrong/missing storage node value, upsert it
336                // (We can't just use `upsert` method with a dup cursor, it's not properly
337                // supported)
338                let subkey: A::StorageSubKey = path.into();
339                let entry = A::StorageValue::new(subkey.clone(), node);
340                if storage_trie_cursor
341                    .seek_by_key_subkey(account, subkey.clone())?
342                    .filter(|v| *v.nibbles() == subkey)
343                    .is_some()
344                {
345                    storage_trie_cursor.delete_current()?;
346                }
347                storage_trie_cursor.upsert(account, &entry)?;
348            }
349            Output::Progress(path) => {
350                if last_progress_time.elapsed() > PROGRESS_PERIOD {
351                    output_progress(path, start_time, inconsistent_nodes);
352                    last_progress_time = Instant::now();
353                }
354            }
355        }
356    }
357
358    drop(account_trie_cursor);
359    drop(storage_trie_cursor);
360
361    if inconsistent_nodes > 0 {
362        // Refuse to commit repaired trie tables unless they reproduce the canonical tip state
363        // root.
364        verify_repaired_state_root::<_, A>(provider_rw, block_number)?;
365    }
366
367    Ok(inconsistent_nodes as usize)
368}
369
370fn verify_repaired_state_root<N: ProviderNodeTypes, A: TrieTableAdapter>(
371    provider_rw: &reth_provider::DatabaseProviderRW<N::DB, N>,
372    block_number: u64,
373) -> eyre::Result<()>
374where
375    <N::DB as reth_db_api::database::Database>::TXMut: DbTxMut + DbTx,
376{
377    type DbStateRoot<'a, TX, A> = reth_trie::StateRoot<
378        DatabaseTrieCursorFactory<&'a TX, A>,
379        DatabaseHashedCursorFactory<&'a TX>,
380    >;
381
382    let expected_state_root = provider_rw
383        .header_by_number(block_number)?
384        .ok_or_else(|| {
385            eyre::eyre!("Missing canonical header at database block tip {block_number}")
386        })?
387        .state_root();
388
389    let computed_state_root = DbStateRoot::<_, A>::from_tx(provider_rw.tx_ref()).root()?;
390
391    if computed_state_root != expected_state_root {
392        return Err(eyre::eyre!(
393            "Repaired trie state root mismatch at block {block_number}: computed {computed_state_root}, header {expected_state_root}",
394        ))
395    }
396
397    Ok(())
398}
399
400/// Output progress information based on the last seen account path.
401fn output_progress(last_account: Nibbles, start_time: Instant, inconsistent_nodes: u64) {
402    // Calculate percentage based on position in the trie path space
403    // For progress estimation, we'll use the first few nibbles as an approximation
404
405    // Convert the first 16 nibbles (8 bytes) to a u64 for progress calculation
406    let mut current_value: u64 = 0;
407    let nibbles_to_use = last_account.len().min(16);
408
409    for i in 0..nibbles_to_use {
410        current_value = (current_value << 4) | (last_account.get(i).unwrap_or(0) as u64);
411    }
412    // Shift left to fill remaining bits if we have fewer than 16 nibbles
413    if nibbles_to_use < 16 {
414        current_value <<= (16 - nibbles_to_use) * 4;
415    }
416
417    let progress_percent = current_value as f64 / u64::MAX as f64 * 100.0;
418    let progress_percent_str = format!("{progress_percent:.2}");
419
420    // Calculate ETA based on current speed
421    let elapsed = start_time.elapsed();
422    let elapsed_secs = elapsed.as_secs_f64();
423
424    let estimated_total_time =
425        if progress_percent > 0.0 { elapsed_secs / (progress_percent / 100.0) } else { 0.0 };
426    let remaining_time = estimated_total_time - elapsed_secs;
427    let eta_duration = Duration::from_secs(remaining_time as u64);
428
429    info!(
430        progress_percent = progress_percent_str,
431        eta = %humantime::format_duration(eta_duration),
432        inconsistent_nodes,
433        "Repairing trie tables",
434    );
435}
436
437/// Metrics for tracking trie repair inconsistencies
438#[derive(Debug)]
439struct RepairTrieMetrics {
440    account_inconsistencies: Counter,
441    storage_inconsistencies: Counter,
442}
443
444impl RepairTrieMetrics {
445    fn new() -> Self {
446        Self {
447            account_inconsistencies: metrics::counter!(
448                "db.repair_trie.inconsistencies_found",
449                "type" => "account"
450            ),
451            storage_inconsistencies: metrics::counter!(
452                "db.repair_trie.inconsistencies_found",
453                "type" => "storage"
454            ),
455        }
456    }
457}