1use alloy_primitives::{hex, Address, BlockHash, B256};
2use clap::Parser;
3use reth_db::{
4 static_file::{
5 AccountChangesetMask, ColumnSelectorOne, ColumnSelectorTwo, HeaderWithHashMask,
6 ReceiptMask, TransactionMask, TransactionSenderMask,
7 },
8 RawDupSort,
9};
10use reth_db_api::{
11 cursor::{DbCursorRO, DbDupCursorRO},
12 database::Database,
13 models::{storage_sharded_key::StorageShardedKey, ShardedKey},
14 table::{Compress, Decompress, DupSort, Table},
15 tables,
16 transaction::DbTx,
17 RawKey, RawTable, TableViewer,
18};
19use reth_db_common::DbTool;
20use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
21use reth_node_builder::NodeTypesWithDB;
22use reth_primitives_traits::ValueWithSubKey;
23use reth_provider::{
24 providers::ProviderNodeTypes, ChangeSetReader, RocksDBProviderFactory,
25 StaticFileProviderFactory,
26};
27use reth_static_file_types::StaticFileSegment;
28use reth_storage_api::StorageChangeSetReader;
29use tracing::error;
30
31#[derive(Parser, Debug)]
33pub struct Command {
34 #[command(subcommand)]
35 subcommand: Subcommand,
36}
37
38#[derive(clap::Subcommand, Debug)]
39enum Subcommand {
40 Mdbx {
42 table: tables::Tables,
43
44 #[arg(value_parser = maybe_json_value_parser)]
46 key: String,
47
48 #[arg(value_parser = maybe_json_value_parser)]
50 subkey: Option<String>,
51
52 #[arg(value_parser = maybe_json_value_parser)]
54 end_key: Option<String>,
55
56 #[arg(value_parser = maybe_json_value_parser)]
58 end_subkey: Option<String>,
59
60 #[arg(long)]
62 raw: bool,
63 },
64 StaticFile {
66 segment: StaticFileSegment,
67
68 #[arg(value_parser = maybe_json_value_parser)]
70 key: String,
71
72 #[arg(value_parser = maybe_json_value_parser)]
74 subkey: Option<String>,
75
76 #[arg(long)]
78 raw: bool,
79 },
80 Rocksdb {
93 #[arg(value_enum)]
95 table: RocksDbTable,
96
97 #[arg(value_parser = maybe_json_value_parser)]
99 key: String,
100
101 #[arg(long)]
104 block: Option<u64>,
105
106 #[arg(long)]
108 storage_key: Option<String>,
109
110 #[arg(long)]
112 all_shards: bool,
113
114 #[arg(long)]
116 raw: bool,
117 },
118}
119
120#[derive(Debug, Clone, Copy, clap::ValueEnum)]
122pub enum RocksDbTable {
123 TransactionHashNumbers,
125 AccountsHistory,
127 StoragesHistory,
129}
130
131impl Command {
132 pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
134 match self.subcommand {
135 Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => {
136 table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })?
137 }
138 Subcommand::Rocksdb { table, key, block, storage_key, all_shards, raw } => {
139 get_rocksdb(tool, table, &key, block, storage_key.as_deref(), all_shards, raw)?;
140 }
141 Subcommand::StaticFile { segment, key, subkey, raw } => {
142 if let StaticFileSegment::StorageChangeSets = segment {
143 let storage_key =
144 table_subkey::<tables::StorageChangeSets>(subkey.as_deref()).ok();
145 let key = table_key::<tables::StorageChangeSets>(&key)?;
146
147 let provider = tool.provider_factory.static_file_provider();
148
149 if let Some(storage_key) = storage_key {
150 let entry = provider.get_storage_before_block(
151 key.block_number(),
152 key.address(),
153 storage_key,
154 )?;
155
156 if let Some(entry) = entry {
157 let se: reth_primitives_traits::StorageEntry = entry;
158 println!("{}", serde_json::to_string_pretty(&se)?);
159 } else {
160 error!(target: "reth::cli", "No content for the given table key.");
161 }
162 return Ok(());
163 }
164
165 let changesets = provider.storage_changeset(key.block_number())?;
166 let serializable: Vec<_> = changesets
167 .into_iter()
168 .map(|(addr, entry)| {
169 let se: reth_primitives_traits::StorageEntry = entry;
170 (addr, se)
171 })
172 .collect();
173 println!("{}", serde_json::to_string_pretty(&serializable)?);
174 return Ok(());
175 }
176
177 let (key, subkey, mask): (u64, _, _) = match segment {
178 StaticFileSegment::Headers => (
179 table_key::<tables::Headers>(&key)?,
180 None,
181 <HeaderWithHashMask<HeaderTy<N>>>::MASK,
182 ),
183 StaticFileSegment::Transactions => (
184 table_key::<tables::Transactions>(&key)?,
185 None,
186 <TransactionMask<TxTy<N>>>::MASK,
187 ),
188 StaticFileSegment::Receipts => (
189 table_key::<tables::Receipts>(&key)?,
190 None,
191 <ReceiptMask<ReceiptTy<N>>>::MASK,
192 ),
193 StaticFileSegment::TransactionSenders => (
194 table_key::<tables::TransactionSenders>(&key)?,
195 None,
196 TransactionSenderMask::MASK,
197 ),
198 StaticFileSegment::AccountChangeSets => {
199 let subkey =
200 table_subkey::<tables::AccountChangeSets>(subkey.as_deref()).ok();
201 (
202 table_key::<tables::AccountChangeSets>(&key)?,
203 subkey,
204 AccountChangesetMask::MASK,
205 )
206 }
207 StaticFileSegment::StorageChangeSets => {
208 unreachable!("storage changesets handled above");
209 }
210 };
211
212 if let StaticFileSegment::AccountChangeSets = segment {
214 let Some(subkey) = subkey else {
215 let changesets = tool
217 .provider_factory
218 .static_file_provider()
219 .account_block_changeset(key)?;
220
221 println!("{}", serde_json::to_string_pretty(&changesets)?);
222 return Ok(())
223 };
224
225 let account = tool
226 .provider_factory
227 .static_file_provider()
228 .get_account_before_block(key, subkey)?;
229
230 if let Some(account) = account {
231 println!("{}", serde_json::to_string_pretty(&account)?);
232 } else {
233 error!(target: "reth::cli", "No content for the given table key.");
234 }
235
236 return Ok(())
237 }
238
239 let content = tool.provider_factory.static_file_provider().find_static_file(
240 segment,
241 |provider| {
242 let mut cursor = provider.cursor()?;
243 cursor.get(key.into(), mask).map(|result| {
244 result.map(|vec| {
245 vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>()
246 })
247 })
248 },
249 )?;
250
251 match content {
252 Some(content) => {
253 if raw {
254 println!("{}", hex::encode_prefixed(&content[0]));
255 } else {
256 match segment {
257 StaticFileSegment::Headers => {
258 let header = HeaderTy::<N>::decompress(content[0].as_slice())?;
259 let block_hash = BlockHash::decompress(content[1].as_slice())?;
260 println!(
261 "Header\n{}\n\nBlockHash\n{}",
262 serde_json::to_string_pretty(&header)?,
263 serde_json::to_string_pretty(&block_hash)?
264 );
265 }
266 StaticFileSegment::Transactions => {
267 let transaction = TxTy::<N>::decompress(content[0].as_slice())?;
268 println!("{}", serde_json::to_string_pretty(&transaction)?);
269 }
270 StaticFileSegment::Receipts => {
271 let receipt =
272 ReceiptTy::<N>::decompress(content[0].as_slice())?;
273 println!("{}", serde_json::to_string_pretty(&receipt)?);
274 }
275 StaticFileSegment::TransactionSenders => {
276 let sender =
277 <<tables::TransactionSenders as Table>::Value>::decompress(
278 content[0].as_slice(),
279 )?;
280 println!("{}", serde_json::to_string_pretty(&sender)?);
281 }
282 StaticFileSegment::AccountChangeSets => {
283 unreachable!("account changeset static files are special cased before this match")
284 }
285 StaticFileSegment::StorageChangeSets => {
286 unreachable!("storage changeset static files are special cased before this match")
287 }
288 }
289 }
290 }
291 None => {
292 error!(target: "reth::cli", "No content for the given table key.");
293 }
294 };
295 }
296 }
297
298 Ok(())
299 }
300}
301
302fn get_rocksdb<N: ProviderNodeTypes>(
304 tool: &DbTool<N>,
305 table: RocksDbTable,
306 key: &str,
307 block: Option<u64>,
308 storage_key: Option<&str>,
309 all_shards: bool,
310 raw: bool,
311) -> eyre::Result<()> {
312 let rocksdb = tool.provider_factory.rocksdb_provider();
313
314 match table {
315 RocksDbTable::TransactionHashNumbers => {
316 if block.is_some() || all_shards || storage_key.is_some() {
317 return Err(eyre::eyre!(
318 "--block, --all-shards, and --storage-key are only supported for history tables"
319 ));
320 }
321 get_rocksdb_table::<tables::TransactionHashNumbers>(&rocksdb, key, raw)
322 }
323 RocksDbTable::AccountsHistory => {
324 if storage_key.is_some() {
325 return Err(eyre::eyre!("--storage-key is only supported for storages-history"));
326 }
327 get_rocksdb_account_history(&rocksdb, key, block, all_shards, raw)
328 }
329 RocksDbTable::StoragesHistory => {
330 get_rocksdb_storage_history(&rocksdb, key, storage_key, block, all_shards, raw)
331 }
332 }
333}
334
335fn parse_address(key: &str) -> eyre::Result<Address> {
337 let stripped = key.trim_matches('"');
339 stripped.parse::<Address>().map_err(|e| eyre::eyre!("failed to parse address: {e}"))
340}
341
342fn get_rocksdb_account_history(
346 rocksdb: &reth_provider::providers::RocksDBProvider,
347 key: &str,
348 block: Option<u64>,
349 all_shards: bool,
350 raw: bool,
351) -> eyre::Result<()> {
352 match parse_address(key) {
354 Ok(address) => {
355 let block_number = block.unwrap_or(u64::MAX);
356 let seek_key = ShardedKey::new(address, block_number);
357
358 if all_shards {
359 let start = ShardedKey::new(address, 0);
361 let iter = rocksdb.iter_from::<tables::AccountsHistory>(start)?;
362 for result in iter {
363 let (k, v) = result?;
364 if k.key != address {
365 break;
366 }
367 println!(
368 "{}",
369 serde_json::to_string_pretty(&serde_json::json!({
370 "highest_block_number": k.highest_block_number,
371 "value": v,
372 }))?
373 );
374 }
375 } else {
376 let mut iter = rocksdb.iter_from::<tables::AccountsHistory>(seek_key)?;
378 match iter.next() {
379 Some(Ok((k, v))) if k.key == address => {
380 if raw {
381 let raw_val = rocksdb.get_raw::<tables::AccountsHistory>(k)?;
382 if let Some(bytes) = raw_val {
383 println!("{}", hex::encode_prefixed(&bytes));
384 }
385 } else {
386 println!("{}", serde_json::to_string_pretty(&v)?);
387 }
388 }
389 _ => {
390 error!(target: "reth::cli", "No content for the given table key.");
391 }
392 }
393 }
394 Ok(())
395 }
396 Err(_) => {
397 if all_shards || block.is_some() {
400 return Err(eyre::eyre!(
401 "--block and --all-shards require a plain address, not a JSON key"
402 ));
403 }
404 get_rocksdb_table::<tables::AccountsHistory>(rocksdb, key, raw)
405 }
406 }
407}
408
409fn get_rocksdb_storage_history(
413 rocksdb: &reth_provider::providers::RocksDBProvider,
414 key: &str,
415 storage_key: Option<&str>,
416 block: Option<u64>,
417 all_shards: bool,
418 raw: bool,
419) -> eyre::Result<()> {
420 match parse_address(key) {
421 Ok(address) => {
422 let storage_key = storage_key
423 .map(|s| s.trim_matches('"').parse::<B256>())
424 .transpose()
425 .map_err(|e| eyre::eyre!("failed to parse storage key: {e}"))?
426 .unwrap_or_default();
427 let block_number = block.unwrap_or(u64::MAX);
428 let seek_key = StorageShardedKey::new(address, storage_key, block_number);
429
430 if all_shards {
431 let start = StorageShardedKey::new(address, storage_key, 0);
432 let iter = rocksdb.iter_from::<tables::StoragesHistory>(start)?;
433 for result in iter {
434 let (k, v) = result?;
435 if k.address != address || k.sharded_key.key != storage_key {
436 break;
437 }
438 println!(
439 "{}",
440 serde_json::to_string_pretty(&serde_json::json!({
441 "highest_block_number": k.sharded_key.highest_block_number,
442 "value": v,
443 }))?
444 );
445 }
446 } else {
447 let mut iter = rocksdb.iter_from::<tables::StoragesHistory>(seek_key)?;
448 match iter.next() {
449 Some(Ok((k, v)))
450 if k.address == address && k.sharded_key.key == storage_key =>
451 {
452 if raw {
453 let raw_val = rocksdb.get_raw::<tables::StoragesHistory>(k)?;
454 if let Some(bytes) = raw_val {
455 println!("{}", hex::encode_prefixed(&bytes));
456 }
457 } else {
458 println!("{}", serde_json::to_string_pretty(&v)?);
459 }
460 }
461 _ => {
462 error!(target: "reth::cli", "No content for the given table key.");
463 }
464 }
465 }
466 Ok(())
467 }
468 Err(_) => {
469 if all_shards || block.is_some() || storage_key.is_some() {
470 return Err(eyre::eyre!(
471 "--block, --all-shards, and --storage-key require a plain address, not a JSON key"
472 ));
473 }
474 get_rocksdb_table::<tables::StoragesHistory>(rocksdb, key, raw)
475 }
476 }
477}
478
479fn get_rocksdb_table<T: Table>(
481 rocksdb: &reth_provider::providers::RocksDBProvider,
482 key_str: &str,
483 raw: bool,
484) -> eyre::Result<()> {
485 let key = table_key::<T>(key_str)?;
486
487 if raw {
488 let content = rocksdb.get_raw::<T>(key)?;
489 match content {
490 Some(bytes) => println!("{}", hex::encode_prefixed(&bytes)),
491 None => error!(target: "reth::cli", "No content for the given table key."),
492 }
493 } else {
494 let content = rocksdb.get::<T>(key)?;
495 match content {
496 Some(value) => println!("{}", serde_json::to_string_pretty(&value)?),
497 None => error!(target: "reth::cli", "No content for the given table key."),
498 }
499 }
500
501 Ok(())
502}
503
504pub(crate) fn table_key<T: Table>(key: &str) -> Result<T::Key, eyre::Error> {
506 serde_json::from_str(key).map_err(|e| eyre::eyre!(e))
507}
508
509fn table_subkey<T: DupSort>(subkey: Option<&str>) -> Result<T::SubKey, eyre::Error> {
511 serde_json::from_str(subkey.unwrap_or_default()).map_err(|e| eyre::eyre!(e))
512}
513
514struct GetValueViewer<'a, N: NodeTypesWithDB> {
515 tool: &'a DbTool<N>,
516 key: String,
517 subkey: Option<String>,
518 end_key: Option<String>,
519 end_subkey: Option<String>,
520 raw: bool,
521}
522
523impl<N: ProviderNodeTypes> TableViewer<()> for GetValueViewer<'_, N> {
524 type Error = eyre::Report;
525
526 fn view<T: Table>(&self) -> Result<(), Self::Error> {
527 let key = table_key::<T>(&self.key)?;
528
529 if self.end_key.is_some() || self.end_subkey.is_some() {
533 return Err(eyre::eyre!("Only END_KEY can be given for non-DUPSORT tables"));
534 }
535
536 let end_key = self.subkey.clone();
537
538 if let Some(ref end_key_str) = end_key {
540 let end_key = table_key::<T>(end_key_str)?;
541
542 self.tool.provider_factory.db_ref().view(|tx| {
544 let mut cursor = tx.cursor_read::<T>()?;
545 let walker = cursor.walk_range(key..end_key)?;
546
547 for result in walker {
548 let (k, v) = result?;
549 let json_val = if self.raw {
550 let raw_key = RawKey::from(k);
551 serde_json::json!({
552 "key": hex::encode_prefixed(raw_key.raw_key()),
553 "val": hex::encode_prefixed(v.compress().as_ref()),
554 })
555 } else {
556 serde_json::json!({
557 "key": &k,
558 "val": &v,
559 })
560 };
561
562 println!("{}", serde_json::to_string_pretty(&json_val)?);
563 }
564
565 Ok::<_, eyre::Report>(())
566 })??;
567 } else {
568 let content = if self.raw {
570 self.tool
571 .get::<RawTable<T>>(RawKey::from(key))?
572 .map(|content| hex::encode_prefixed(content.raw_value()))
573 } else {
574 self.tool.get::<T>(key)?.as_ref().map(serde_json::to_string_pretty).transpose()?
575 };
576
577 match content {
578 Some(content) => {
579 println!("{content}");
580 }
581 None => {
582 error!(target: "reth::cli", "No content for the given table key.");
583 }
584 };
585 }
586
587 Ok(())
588 }
589
590 fn view_dupsort<T: DupSort>(&self) -> Result<(), Self::Error>
591 where
592 T::Value: reth_primitives_traits::ValueWithSubKey<SubKey = T::SubKey>,
593 {
594 let key = table_key::<T>(&self.key)?;
596
597 if let Some(ref end_key_str) = self.end_key {
599 let end_key = table_key::<T>(end_key_str)?;
600 let start_subkey = table_subkey::<T>(Some(
601 self.subkey.as_ref().expect("must have been given if end_key is given").as_str(),
602 ))?;
603 let end_subkey_parsed = self
604 .end_subkey
605 .as_ref()
606 .map(|s| table_subkey::<T>(Some(s.as_str())))
607 .transpose()?;
608
609 self.tool.provider_factory.db_ref().view(|tx| {
610 let mut cursor = tx.cursor_dup_read::<T>()?;
611
612 if let Some((decoded_key, _)) = cursor.seek(key.clone())? &&
615 decoded_key == key
616 {
617 cursor.seek_by_key_subkey(key.clone(), start_subkey.clone())?;
618 }
619
620 let mut current = cursor.current()?;
622
623 while let Some((decoded_key, decoded_value)) = current {
624 let decoded_subkey = decoded_value.get_subkey();
626
627 if (&decoded_key, Some(&decoded_subkey)) >=
629 (&end_key, end_subkey_parsed.as_ref())
630 {
631 break;
632 }
633
634 let json_val = if self.raw {
636 let raw_key = RawKey::from(decoded_key.clone());
637 serde_json::json!({
638 "key": hex::encode_prefixed(raw_key.raw_key()),
639 "val": hex::encode_prefixed(decoded_value.compress().as_ref()),
640 })
641 } else {
642 serde_json::json!({
643 "key": &decoded_key,
644 "val": &decoded_value,
645 })
646 };
647
648 println!("{}", serde_json::to_string_pretty(&json_val)?);
649
650 current = cursor.next()?;
652 }
653
654 Ok::<_, eyre::Report>(())
655 })??;
656 } else {
657 let subkey = table_subkey::<T>(self.subkey.as_deref())?;
659
660 let content = if self.raw {
661 self.tool
662 .get_dup::<RawDupSort<T>>(RawKey::from(key), RawKey::from(subkey))?
663 .map(|content| hex::encode_prefixed(content.raw_value()))
664 } else {
665 self.tool
666 .get_dup::<T>(key, subkey)?
667 .as_ref()
668 .map(serde_json::to_string_pretty)
669 .transpose()?
670 };
671
672 match content {
673 Some(content) => {
674 println!("{content}");
675 }
676 None => {
677 error!(target: "reth::cli", "No content for the given table subkey.");
678 }
679 };
680 }
681 Ok(())
682 }
683}
684
685pub(crate) fn maybe_json_value_parser(value: &str) -> Result<String, eyre::Error> {
687 if serde_json::from_str::<serde::de::IgnoredAny>(value).is_ok() {
688 Ok(value.to_string())
689 } else {
690 serde_json::to_string(&value).map_err(|e| eyre::eyre!(e))
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697 use alloy_primitives::{address, B256};
698 use clap::{Args, Parser};
699 use reth_db_api::{
700 models::{storage_sharded_key::StorageShardedKey, ShardedKey},
701 AccountsHistory, HashedAccounts, Headers, StageCheckpoints, StoragesHistory,
702 };
703 use std::str::FromStr;
704
705 #[derive(Parser)]
707 struct CommandParser<T: Args> {
708 #[command(flatten)]
709 args: T,
710 }
711
712 #[test]
713 fn parse_numeric_key_args() {
714 assert_eq!(table_key::<Headers>("123").unwrap(), 123);
715 assert_eq!(
716 table_key::<HashedAccounts>(
717 "\"0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac\""
718 )
719 .unwrap(),
720 B256::from_str("0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac")
721 .unwrap()
722 );
723 }
724
725 #[test]
726 fn parse_string_key_args() {
727 assert_eq!(
728 table_key::<StageCheckpoints>("\"MerkleExecution\"").unwrap(),
729 "MerkleExecution"
730 );
731 }
732
733 #[test]
734 fn parse_json_key_args() {
735 assert_eq!(
736 table_key::<StoragesHistory>(r#"{ "address": "0x01957911244e546ce519fbac6f798958fafadb41", "sharded_key": { "key": "0x0000000000000000000000000000000000000000000000000000000000000003", "highest_block_number": 18446744073709551615 } }"#).unwrap(),
737 StorageShardedKey::new(
738 address!("0x01957911244e546ce519fbac6f798958fafadb41"),
739 B256::from_str(
740 "0x0000000000000000000000000000000000000000000000000000000000000003"
741 )
742 .unwrap(),
743 18446744073709551615
744 )
745 );
746 }
747
748 #[test]
749 fn parse_json_key_for_account_history() {
750 assert_eq!(
751 table_key::<AccountsHistory>(r#"{ "key": "0x4448e1273fd5a8bfdb9ed111e96889c960eee145", "highest_block_number": 18446744073709551615 }"#).unwrap(),
752 ShardedKey::new(
753 address!("0x4448e1273fd5a8bfdb9ed111e96889c960eee145"),
754 18446744073709551615
755 )
756 );
757 }
758}