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#[derive(Parser, Debug)]
46pub struct Command {
47 #[arg(long)]
51 pub(crate) dry_run: bool,
52
53 #[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 #[arg(long, default_value_t = DEFAULT_STORAGE_LOOKAHEAD, value_parser = RangedU64ValueParser::<usize>::new().range(1..))]
59 pub(crate) storage_lookahead: usize,
60
61 #[arg(long = "metrics", value_name = "ADDR:PORT", value_parser = parse_socket_address)]
65 pub(crate) metrics: Option<SocketAddr>,
66}
67
68impl Command {
69 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 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 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 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 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 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
190fn 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 let mut provider_rw = tool.provider_factory.provider_rw()?;
233
234 let finish_checkpoint = provider_rw.get_stage_checkpoint(StageId::Finish)?.unwrap_or_default();
236 info!("Database block tip: {}", finish_checkpoint.block_number);
237
238 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 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 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 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 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 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 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 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 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 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
400fn output_progress(last_account: Nibbles, start_time: Instant, inconsistent_nodes: u64) {
402 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 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 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#[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}