Skip to content

Commit 0c03a47

Browse files
mediocregopherlwedge99
authored andcommitted
feat(trie): Add helper sub-command (paradigmxyz#18301)
1 parent fbc3955 commit 0c03a47

File tree

12 files changed

+1714
-13
lines changed

12 files changed

+1714
-13
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli/commands/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ reth-static-file-types = { workspace = true, features = ["clap"] }
5151
reth-static-file.workspace = true
5252
reth-trie = { workspace = true, features = ["metrics"] }
5353
reth-trie-db = { workspace = true, features = ["metrics"] }
54-
reth-trie-common = { workspace = true, optional = true }
54+
reth-trie-common.workspace = true
5555
reth-primitives-traits.workspace = true
5656
reth-discv4.workspace = true
5757
reth-discv5.workspace = true
@@ -68,6 +68,7 @@ futures.workspace = true
6868
tokio.workspace = true
6969

7070
# misc
71+
humantime.workspace = true
7172
human_bytes.workspace = true
7273
eyre.workspace = true
7374
clap = { workspace = true, features = ["derive", "env"] }
@@ -119,7 +120,7 @@ arbitrary = [
119120
"reth-codecs/arbitrary",
120121
"reth-prune-types?/arbitrary",
121122
"reth-stages-types?/arbitrary",
122-
"reth-trie-common?/arbitrary",
123+
"reth-trie-common/arbitrary",
123124
"alloy-consensus/arbitrary",
124125
"reth-primitives-traits/arbitrary",
125126
"reth-ethereum-primitives/arbitrary",

crates/cli/commands/src/db/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod clear;
1313
mod diff;
1414
mod get;
1515
mod list;
16+
mod repair_trie;
1617
mod stats;
1718
/// DB List TUI
1819
mod tui;
@@ -48,6 +49,8 @@ pub enum Subcommands {
4849
},
4950
/// Deletes all table entries
5051
Clear(clear::Command),
52+
/// Verifies trie consistency and outputs any inconsistencies
53+
RepairTrie(repair_trie::Command),
5154
/// Lists current and local database versions
5255
Version,
5356
/// Returns the full database path
@@ -135,6 +138,10 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
135138
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
136139
command.execute(provider_factory)?;
137140
}
141+
Subcommands::RepairTrie(command) => {
142+
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
143+
command.execute(provider_factory)?;
144+
}
138145
Subcommands::Version => {
139146
let local_db_version = match get_db_version(&db_path) {
140147
Ok(version) => Some(version),
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use clap::Parser;
2+
use reth_db_api::{
3+
cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO},
4+
database::Database,
5+
tables,
6+
transaction::{DbTx, DbTxMut},
7+
};
8+
use reth_node_builder::NodeTypesWithDB;
9+
use reth_provider::ProviderFactory;
10+
use reth_trie::{
11+
verify::{Output, Verifier},
12+
Nibbles,
13+
};
14+
use reth_trie_common::{StorageTrieEntry, StoredNibbles, StoredNibblesSubKey};
15+
use reth_trie_db::{DatabaseHashedCursorFactory, DatabaseTrieCursorFactory};
16+
use std::time::{Duration, Instant};
17+
use tracing::{info, warn};
18+
19+
/// The arguments for the `reth db repair-trie` command
20+
#[derive(Parser, Debug)]
21+
pub struct Command {
22+
/// Only show inconsistencies without making any repairs
23+
#[arg(long)]
24+
dry_run: bool,
25+
}
26+
27+
impl Command {
28+
/// Execute `db repair-trie` command
29+
pub fn execute<N: NodeTypesWithDB>(
30+
self,
31+
provider_factory: ProviderFactory<N>,
32+
) -> eyre::Result<()> {
33+
// Get a database transaction directly from the database
34+
let db = provider_factory.db_ref();
35+
let mut tx = db.tx_mut()?;
36+
tx.disable_long_read_transaction_safety();
37+
38+
// Create the hashed cursor factory
39+
let hashed_cursor_factory = DatabaseHashedCursorFactory::new(&tx);
40+
41+
// Create the trie cursor factory
42+
let trie_cursor_factory = DatabaseTrieCursorFactory::new(&tx);
43+
44+
// Create the verifier
45+
let verifier = Verifier::new(trie_cursor_factory, hashed_cursor_factory)?;
46+
47+
let mut account_trie_cursor = tx.cursor_write::<tables::AccountsTrie>()?;
48+
let mut storage_trie_cursor = tx.cursor_dup_write::<tables::StoragesTrie>()?;
49+
50+
let mut inconsistent_nodes = 0;
51+
let start_time = Instant::now();
52+
let mut last_progress_time = Instant::now();
53+
54+
// Iterate over the verifier and repair inconsistencies
55+
for output_result in verifier {
56+
let output = output_result?;
57+
58+
if let Output::Progress(path) = output {
59+
// Output progress every 5 seconds
60+
if last_progress_time.elapsed() > Duration::from_secs(5) {
61+
output_progress(path, start_time, inconsistent_nodes);
62+
last_progress_time = Instant::now();
63+
}
64+
continue
65+
};
66+
67+
warn!("Inconsistency found, will repair: {output:?}");
68+
inconsistent_nodes += 1;
69+
70+
if self.dry_run {
71+
continue;
72+
}
73+
74+
match output {
75+
Output::AccountExtra(path, _node) => {
76+
// Extra account node in trie, remove it
77+
let nibbles = StoredNibbles(path);
78+
if account_trie_cursor.seek_exact(nibbles)?.is_some() {
79+
account_trie_cursor.delete_current()?;
80+
}
81+
}
82+
Output::StorageExtra(account, path, _node) => {
83+
// Extra storage node in trie, remove it
84+
let nibbles = StoredNibblesSubKey(path);
85+
if storage_trie_cursor
86+
.seek_by_key_subkey(account, nibbles.clone())?
87+
.filter(|e| e.nibbles == nibbles)
88+
.is_some()
89+
{
90+
storage_trie_cursor.delete_current()?;
91+
}
92+
}
93+
Output::AccountWrong { path, expected: node, .. } |
94+
Output::AccountMissing(path, node) => {
95+
// Wrong/missing account node value, upsert it
96+
let nibbles = StoredNibbles(path);
97+
account_trie_cursor.upsert(nibbles, &node)?;
98+
}
99+
Output::StorageWrong { account, path, expected: node, .. } |
100+
Output::StorageMissing(account, path, node) => {
101+
// Wrong/missing storage node value, upsert it
102+
let nibbles = StoredNibblesSubKey(path);
103+
let entry = StorageTrieEntry { nibbles, node };
104+
storage_trie_cursor.upsert(account, &entry)?;
105+
}
106+
Output::Progress(_) => {
107+
unreachable!()
108+
}
109+
}
110+
}
111+
112+
if inconsistent_nodes > 0 {
113+
if self.dry_run {
114+
info!("Found {} inconsistencies (dry run - no changes made)", inconsistent_nodes);
115+
} else {
116+
info!("Repaired {} inconsistencies", inconsistent_nodes);
117+
tx.commit()?;
118+
info!("Changes committed to database");
119+
}
120+
} else {
121+
info!("No inconsistencies found");
122+
}
123+
124+
Ok(())
125+
}
126+
}
127+
128+
/// Output progress information based on the last seen account path.
129+
fn output_progress(last_account: Nibbles, start_time: Instant, inconsistent_nodes: u64) {
130+
// Calculate percentage based on position in the trie path space
131+
// For progress estimation, we'll use the first few nibbles as an approximation
132+
133+
// Convert the first 16 nibbles (8 bytes) to a u64 for progress calculation
134+
let mut current_value: u64 = 0;
135+
let nibbles_to_use = last_account.len().min(16);
136+
137+
for i in 0..nibbles_to_use {
138+
current_value = (current_value << 4) | (last_account.get(i).unwrap_or(0) as u64);
139+
}
140+
// Shift left to fill remaining bits if we have fewer than 16 nibbles
141+
if nibbles_to_use < 16 {
142+
current_value <<= (16 - nibbles_to_use) * 4;
143+
}
144+
145+
let progress_percent = current_value as f64 / u64::MAX as f64 * 100.0;
146+
let progress_percent_str = format!("{progress_percent:.2}");
147+
148+
// Calculate ETA based on current speed
149+
let elapsed = start_time.elapsed();
150+
let elapsed_secs = elapsed.as_secs_f64();
151+
152+
let estimated_total_time =
153+
if progress_percent > 0.0 { elapsed_secs / (progress_percent / 100.0) } else { 0.0 };
154+
let remaining_time = estimated_total_time - elapsed_secs;
155+
let eta_duration = Duration::from_secs(remaining_time as u64);
156+
157+
info!(
158+
progress_percent = progress_percent_str,
159+
eta = %humantime::format_duration(eta_duration),
160+
inconsistent_nodes,
161+
"Repairing trie tables",
162+
);
163+
}

crates/trie/trie/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ revm-state.workspace = true
5757
triehash.workspace = true
5858

5959
# misc
60+
assert_matches.workspace = true
6061
criterion.workspace = true
6162
parking_lot.workspace = true
6263
pretty_assertions.workspace = true

crates/trie/trie/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,6 @@ pub mod test_utils;
6363
/// Collection of mock types for testing.
6464
#[cfg(test)]
6565
pub mod mock;
66+
67+
/// Verification of existing stored trie nodes against state data.
68+
pub mod verify;

0 commit comments

Comments
 (0)