reth_cli_commands/db/checksum/
mod.rs1use crate::{
2 common::CliNodeTypes,
3 db::get::{maybe_json_value_parser, table_key},
4};
5use alloy_primitives::map::foldhash::fast::FixedState;
6use clap::Parser;
7use itertools::Itertools;
8use reth_chainspec::EthereumHardforks;
9use reth_db::{static_file::iter_static_files, DatabaseEnv};
10use reth_db_api::{
11 cursor::DbCursorRO,
12 table::{Decode, Table},
13 transaction::DbTx,
14 DatabaseError, RawKey, RawTable, TableViewer, Tables,
15};
16use reth_db_common::DbTool;
17use reth_node_builder::{NodeTypesWithDB, NodeTypesWithDBAdapter};
18use reth_provider::{providers::ProviderNodeTypes, DBProvider, StaticFileProviderFactory};
19use reth_static_file_types::{ChangesetOffset, StaticFileSegment};
20use std::{
21 hash::{BuildHasher, Hasher},
22 ops::Bound,
23 time::{Duration, Instant},
24};
25use tracing::{info, warn};
26
27mod rocksdb;
28
29const PROGRESS_LOG_INTERVAL: usize = 100_000;
31
32#[derive(Parser, Debug)]
33pub struct Command {
35 #[command(subcommand)]
36 subcommand: Subcommand,
37}
38
39#[derive(clap::Subcommand, Debug)]
40enum Subcommand {
41 Mdbx {
43 table: Tables,
45
46 #[arg(long, value_parser = maybe_json_value_parser)]
48 start_key: Option<String>,
49
50 #[arg(long, value_parser = maybe_json_value_parser)]
52 end_key: Option<String>,
53
54 #[arg(long)]
57 limit: Option<usize>,
58 },
59 StaticFile {
61 #[arg(value_enum)]
63 segment: StaticFileSegment,
64
65 #[arg(long)]
67 start_block: Option<u64>,
68
69 #[arg(long)]
71 end_block: Option<u64>,
72
73 #[arg(long)]
75 limit: Option<usize>,
76 },
77 Rocksdb {
79 #[arg(value_enum)]
81 table: rocksdb::RocksDbTable,
82
83 #[arg(long)]
85 limit: Option<usize>,
86 },
87}
88
89impl Command {
90 pub fn execute<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
92 self,
93 tool: &DbTool<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
94 ) -> eyre::Result<()> {
95 warn!("This command should be run without the node running!");
96
97 match self.subcommand {
98 Subcommand::Mdbx { table, start_key, end_key, limit } => {
99 table.view(&ChecksumViewer { tool, start_key, end_key, limit })?;
100 }
101 Subcommand::StaticFile { segment, start_block, end_block, limit } => {
102 checksum_static_file(tool, segment, start_block, end_block, limit)?;
103 }
104 Subcommand::Rocksdb { table, limit } => {
105 rocksdb::checksum_rocksdb(tool, table, limit)?;
106 }
107 }
108
109 Ok(())
110 }
111}
112
113fn checksum_hasher() -> impl Hasher {
115 FixedState::with_seed(u64::from_be_bytes(*b"RETHRETH")).build_hasher()
116}
117
118fn checksum_static_file<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
119 tool: &DbTool<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
120 segment: StaticFileSegment,
121 start_block: Option<u64>,
122 end_block: Option<u64>,
123 limit: Option<usize>,
124) -> eyre::Result<()> {
125 let static_file_provider = tool.provider_factory.static_file_provider();
126 if let Err(err) = static_file_provider.check_consistency(&tool.provider_factory.provider()?) {
127 warn!("Error checking consistency of static files: {err}");
128 }
129
130 let static_files = iter_static_files(static_file_provider.directory())?;
131
132 let ranges = static_files
133 .get(segment)
134 .ok_or_else(|| eyre::eyre!("No static files found for segment: {}", segment))?;
135
136 let start_time = Instant::now();
137 let limit = limit.unwrap_or(usize::MAX);
138 let mut checksummer = Checksummer::new(checksum_hasher(), limit);
139
140 let start_block = start_block.unwrap_or(0);
141 let end_block = end_block.unwrap_or(u64::MAX);
142 let is_change_based = segment.is_change_based();
143
144 info!(
145 "Computing checksum for {} static files, start_block={}, end_block={}, limit={:?}",
146 segment,
147 start_block,
148 end_block,
149 if limit == usize::MAX { None } else { Some(limit) }
150 );
151
152 let mut reached_limit = false;
153 for (block_range, _header) in ranges.iter().sorted_by_key(|(range, _)| range.start()) {
154 if block_range.end() < start_block || block_range.start() > end_block {
155 continue;
156 }
157
158 let fixed_block_range = static_file_provider.find_fixed_range(segment, block_range.start());
159 let jar_provider = static_file_provider
160 .get_segment_provider_for_range(segment, || Some(fixed_block_range), None)?
161 .ok_or_else(|| {
162 eyre::eyre!(
163 "Failed to get segment provider for segment {} at range {}",
164 segment,
165 block_range
166 )
167 })?;
168
169 let mut cursor = jar_provider.cursor()?;
170
171 if is_change_based {
172 let offsets = jar_provider.read_changeset_offsets()?.ok_or_else(|| {
173 eyre::eyre!(
174 "Missing changeset offsets sidecar for segment {} at range {}",
175 segment,
176 block_range
177 )
178 })?;
179 let input = ChangeBasedChecksumInput {
180 segment,
181 block_range_start: block_range.start(),
182 start_block,
183 end_block,
184 offsets: &offsets,
185 };
186
187 reached_limit = checksum_change_based_segment(&mut checksummer, input, &mut cursor)?;
188 } else {
189 while let Some(row) = cursor.next_row()? {
190 if checksummer.write_row(&row) {
191 reached_limit = true;
192 break;
193 }
194 }
195 }
196
197 drop(jar_provider);
199 static_file_provider.remove_cached_provider(segment, fixed_block_range.end());
200
201 if reached_limit {
202 break;
203 }
204 }
205
206 let (checksum, total) = checksummer.finish();
207 let elapsed = start_time.elapsed();
208
209 info!(
210 "Checksum for static file segment `{}`: {:#x} ({} entries, elapsed: {:?})",
211 segment, checksum, total, elapsed
212 );
213
214 Ok(())
215}
216
217pub(crate) struct ChecksumViewer<'a, N: NodeTypesWithDB> {
218 tool: &'a DbTool<N>,
219 start_key: Option<String>,
220 end_key: Option<String>,
221 limit: Option<usize>,
222}
223
224impl<N: NodeTypesWithDB> ChecksumViewer<'_, N> {
225 pub(crate) const fn new(tool: &'_ DbTool<N>) -> ChecksumViewer<'_, N> {
226 ChecksumViewer { tool, start_key: None, end_key: None, limit: None }
227 }
228}
229
230impl<N: ProviderNodeTypes> TableViewer<(u64, Duration)> for ChecksumViewer<'_, N> {
231 type Error = eyre::Report;
232
233 fn view<T: Table>(&self) -> Result<(u64, Duration), Self::Error> {
234 let provider =
235 self.tool.provider_factory.provider()?.disable_long_read_transaction_safety();
236 let tx = provider.tx_ref();
237 log_checksum_start(&self.start_key, &self.end_key, self.limit);
238
239 let mut cursor = tx.cursor_read::<RawTable<T>>()?;
240 let parse_key = |key: &str| table_key::<T>(key).map(RawKey::new);
241 let start = self.start_key.as_deref().map(parse_key).transpose()?;
242 let end = self.end_key.as_deref().map(parse_key).transpose()?;
243 let bound = |key: Option<RawKey<T::Key>>| key.map_or(Bound::Unbounded, Bound::Included);
244 let mut entries = cursor
245 .walk_range((bound(start), bound(end)))?
246 .map(|entry| entry.map(|(k, v)| (k.into_key(), v.into_value())));
247
248 checksum_entries(
249 &mut entries,
250 self.limit.unwrap_or(usize::MAX),
251 &|key| Ok(serde_json::to_string(&T::Key::decode(key)?).unwrap_or_default()),
252 T::NAME,
253 )
254 }
255}
256
257fn log_checksum_start(start: &Option<String>, end: &Option<String>, limit: Option<usize>) {
258 info!("Start computing checksum, start={:?}, end={:?}, limit={:?}", start, end, limit);
259}
260
261fn checksum_entries(
265 entries: &mut dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), DatabaseError>>,
266 limit: usize,
267 key_json: &dyn Fn(&[u8]) -> eyre::Result<String>,
268 table: &str,
269) -> eyre::Result<(u64, Duration)> {
270 let start_time = Instant::now();
271 let mut hasher = checksum_hasher();
272 let mut total = 0;
273
274 let mut enumerate_start_key = None;
275 let mut enumerate_end_key = None;
276 for (index, entry) in entries.enumerate() {
277 let (k, v) = entry?;
278
279 if index.is_multiple_of(PROGRESS_LOG_INTERVAL) {
280 info!("Hashed {index} entries.");
281 }
282
283 hasher.write(&k);
284 hasher.write(&v);
285
286 if enumerate_start_key.is_none() {
287 enumerate_start_key = Some(k.clone());
288 }
289 enumerate_end_key = Some(k);
290
291 total = index + 1;
292 if total >= limit {
293 break;
294 }
295 }
296
297 info!("Hashed {total} entries.");
298 if let (Some(s), Some(e)) = (enumerate_start_key, enumerate_end_key) {
299 info!("start-key: {}", key_json(&s)?);
300 info!("end-key: {}", key_json(&e)?);
301 }
302
303 let checksum = hasher.finish();
304 let elapsed = start_time.elapsed();
305
306 info!("Checksum for table `{}`: {:#x} (elapsed: {:?})", table, checksum, elapsed);
307
308 Ok((checksum, elapsed))
309}
310
311struct Checksummer<H> {
313 hasher: H,
314 total: usize,
315 limit: usize,
316}
317
318impl<H: Hasher> Checksummer<H> {
319 fn new(hasher: H, limit: usize) -> Self {
320 Self { hasher, total: 0, limit }
321 }
322
323 fn write_row(&mut self, row: &[&[u8]]) -> bool {
325 for col in row {
326 self.hasher.write(col);
327 }
328 self.advance()
329 }
330
331 fn write_entry(&mut self, key: &[u8], value: &[u8]) -> bool {
334 self.hasher.write(key);
335 self.hasher.write(value);
336 self.advance()
337 }
338
339 fn advance(&mut self) -> bool {
340 self.total += 1;
341 if self.total.is_multiple_of(PROGRESS_LOG_INTERVAL) {
342 info!("Hashed {} entries.", self.total);
343 }
344 self.total >= self.limit
345 }
346
347 fn finish(self) -> (u64, usize) {
348 (self.hasher.finish(), self.total)
349 }
350}
351
352fn split_storage_changeset_row(block_number: u64, row: &[u8]) -> eyre::Result<([u8; 28], &[u8])> {
361 if row.len() < 20 {
362 return Err(eyre::eyre!(
363 "Storage changeset row too short: expected at least 20 bytes, got {}",
364 row.len()
365 ));
366 }
367
368 let mut key_buf = [0u8; 28];
369 key_buf[..8].copy_from_slice(&block_number.to_be_bytes());
370 key_buf[8..].copy_from_slice(&row[..20]);
371 Ok((key_buf, &row[20..]))
372}
373
374struct ChangeBasedChecksumInput<'a> {
375 segment: StaticFileSegment,
376 block_range_start: u64,
377 start_block: u64,
378 end_block: u64,
379 offsets: &'a [ChangesetOffset],
380}
381
382fn checksum_change_based_segment<H: Hasher>(
383 checksummer: &mut Checksummer<H>,
384 input: ChangeBasedChecksumInput<'_>,
385 cursor: &mut reth_db::static_file::StaticFileCursor<'_>,
386) -> eyre::Result<bool> {
387 let ChangeBasedChecksumInput { segment, block_range_start, start_block, end_block, offsets } =
388 input;
389 let is_storage = segment.is_storage_change_sets();
390 let mut reached_limit = false;
391
392 for (offset_index, offset) in offsets.iter().enumerate() {
393 let block_number = block_range_start + offset_index as u64;
394 let include = block_number >= start_block && block_number <= end_block;
395
396 for _ in 0..offset.num_changes() {
397 let row = cursor.next_row()?.ok_or_else(|| {
398 eyre::eyre!(
399 "Unexpected EOF while checksumming {} static file at range starting {}",
400 segment,
401 block_range_start
402 )
403 })?;
404
405 if !include {
406 continue;
407 }
408
409 let done = if is_storage {
413 let col = row[0];
417 let (key, value) = split_storage_changeset_row(block_number, col)?;
418 checksummer.write_entry(&key, value)
419 } else {
420 checksummer.write_entry(&block_number.to_be_bytes(), row[0])
423 };
424
425 if done {
426 reached_limit = true;
427 break;
428 }
429 }
430
431 if reached_limit {
432 break;
433 }
434 }
435
436 if !reached_limit && cursor.next_row()?.is_some() {
437 return Err(eyre::eyre!(
438 "Changeset offsets do not cover all rows for {} at range starting {}",
439 segment,
440 block_range_start
441 ));
442 }
443
444 Ok(reached_limit)
445}