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 = subkey
144 .as_deref()
145 .map(table_subkey::<tables::StorageChangeSets>)
146 .transpose()?;
147 let key = table_key::<tables::StorageChangeSets>(&key)?;
148
149 let provider = tool.provider_factory.static_file_provider();
150
151 if let Some(storage_key) = storage_key {
152 let entry = provider.get_storage_before_block(
153 key.block_number(),
154 key.address(),
155 storage_key,
156 )?;
157
158 if let Some(entry) = entry {
159 let se: reth_primitives_traits::StorageEntry = entry;
160 println!("{}", serde_json::to_string_pretty(&se)?);
161 } else {
162 error!(target: "reth::cli", "No content for the given table key.");
163 }
164 return Ok(());
165 }
166
167 let changesets = provider.storage_changeset(key.block_number())?;
168 let serializable: Vec<_> = changesets
169 .into_iter()
170 .map(|(addr, entry)| {
171 let se: reth_primitives_traits::StorageEntry = entry;
172 (addr, se)
173 })
174 .collect();
175 println!("{}", serde_json::to_string_pretty(&serializable)?);
176 return Ok(());
177 }
178
179 let (key, subkey, mask): (u64, _, _) = match segment {
180 StaticFileSegment::Headers => (
181 table_key::<tables::Headers>(&key)?,
182 None,
183 <HeaderWithHashMask<HeaderTy<N>>>::MASK,
184 ),
185 StaticFileSegment::Transactions => (
186 table_key::<tables::Transactions>(&key)?,
187 None,
188 <TransactionMask<TxTy<N>>>::MASK,
189 ),
190 StaticFileSegment::Receipts => (
191 table_key::<tables::Receipts>(&key)?,
192 None,
193 <ReceiptMask<ReceiptTy<N>>>::MASK,
194 ),
195 StaticFileSegment::TransactionSenders => (
196 table_key::<tables::TransactionSenders>(&key)?,
197 None,
198 TransactionSenderMask::MASK,
199 ),
200 StaticFileSegment::AccountChangeSets => {
201 let subkey = subkey
202 .as_deref()
203 .map(table_subkey::<tables::AccountChangeSets>)
204 .transpose()?;
205 (
206 table_key::<tables::AccountChangeSets>(&key)?,
207 subkey,
208 AccountChangesetMask::MASK,
209 )
210 }
211 StaticFileSegment::StorageChangeSets => {
212 unreachable!("storage changesets handled above");
213 }
214 };
215
216 if let StaticFileSegment::AccountChangeSets = segment {
218 let Some(subkey) = subkey else {
219 let changesets = tool
221 .provider_factory
222 .static_file_provider()
223 .account_block_changeset(key)?;
224
225 println!("{}", serde_json::to_string_pretty(&changesets)?);
226 return Ok(())
227 };
228
229 let account = tool
230 .provider_factory
231 .static_file_provider()
232 .get_account_before_block(key, subkey)?;
233
234 if let Some(account) = account {
235 println!("{}", serde_json::to_string_pretty(&account)?);
236 } else {
237 error!(target: "reth::cli", "No content for the given table key.");
238 }
239
240 return Ok(())
241 }
242
243 let content = tool.provider_factory.static_file_provider().find_static_file(
244 segment,
245 |provider| {
246 let mut cursor = provider.cursor()?;
247 cursor.get(key.into(), mask).map(|result| {
248 result.map(|vec| {
249 vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>()
250 })
251 })
252 },
253 )?;
254
255 match content {
256 Some(content) => {
257 if raw {
258 println!("{}", hex::encode_prefixed(&content[0]));
259 } else {
260 match segment {
261 StaticFileSegment::Headers => {
262 let header = HeaderTy::<N>::decompress(content[0].as_slice())?;
263 let block_hash = BlockHash::decompress(content[1].as_slice())?;
264 println!(
265 "Header\n{}\n\nBlockHash\n{}",
266 serde_json::to_string_pretty(&header)?,
267 serde_json::to_string_pretty(&block_hash)?
268 );
269 }
270 StaticFileSegment::Transactions => {
271 let transaction = TxTy::<N>::decompress(content[0].as_slice())?;
272 println!("{}", serde_json::to_string_pretty(&transaction)?);
273 }
274 StaticFileSegment::Receipts => {
275 let receipt =
276 ReceiptTy::<N>::decompress(content[0].as_slice())?;
277 println!("{}", serde_json::to_string_pretty(&receipt)?);
278 }
279 StaticFileSegment::TransactionSenders => {
280 let sender =
281 <<tables::TransactionSenders as Table>::Value>::decompress(
282 content[0].as_slice(),
283 )?;
284 println!("{}", serde_json::to_string_pretty(&sender)?);
285 }
286 StaticFileSegment::AccountChangeSets => {
287 unreachable!("account changeset static files are special cased before this match")
288 }
289 StaticFileSegment::StorageChangeSets => {
290 unreachable!("storage changeset static files are special cased before this match")
291 }
292 }
293 }
294 }
295 None => {
296 error!(target: "reth::cli", "No content for the given table key.");
297 }
298 };
299 }
300 }
301
302 Ok(())
303 }
304}
305
306fn get_rocksdb<N: ProviderNodeTypes>(
308 tool: &DbTool<N>,
309 table: RocksDbTable,
310 key: &str,
311 block: Option<u64>,
312 storage_key: Option<&str>,
313 all_shards: bool,
314 raw: bool,
315) -> eyre::Result<()> {
316 let rocksdb = tool.provider_factory.rocksdb_provider();
317
318 match table {
319 RocksDbTable::TransactionHashNumbers => {
320 if block.is_some() || all_shards || storage_key.is_some() {
321 return Err(eyre::eyre!(
322 "--block, --all-shards, and --storage-key are only supported for history tables"
323 ));
324 }
325 get_rocksdb_table::<tables::TransactionHashNumbers>(&rocksdb, key, raw)
326 }
327 RocksDbTable::AccountsHistory => {
328 if storage_key.is_some() {
329 return Err(eyre::eyre!("--storage-key is only supported for storages-history"));
330 }
331 get_rocksdb_account_history(&rocksdb, key, block, all_shards, raw)
332 }
333 RocksDbTable::StoragesHistory => {
334 get_rocksdb_storage_history(&rocksdb, key, storage_key, block, all_shards, raw)
335 }
336 }
337}
338
339fn parse_address(key: &str) -> eyre::Result<Address> {
341 let stripped = key.trim_matches('"');
343 stripped.parse::<Address>().map_err(|e| eyre::eyre!("failed to parse address: {e}"))
344}
345
346fn looks_like_address(key: &str) -> bool {
349 key.trim_matches('"').starts_with("0x")
350}
351
352fn get_rocksdb_account_history(
356 rocksdb: &reth_provider::providers::RocksDBProvider,
357 key: &str,
358 block: Option<u64>,
359 all_shards: bool,
360 raw: bool,
361) -> eyre::Result<()> {
362 match parse_address(key) {
364 Ok(address) => {
365 let block_number = block.unwrap_or(u64::MAX);
366 let seek_key = ShardedKey::new(address, block_number);
367
368 if all_shards {
369 let start = ShardedKey::new(address, 0);
371 let iter = rocksdb.iter_from::<tables::AccountsHistory>(start)?;
372 for result in iter {
373 let (k, v) = result?;
374 if k.key != address {
375 break;
376 }
377 println!(
378 "{}",
379 serde_json::to_string_pretty(&serde_json::json!({
380 "highest_block_number": k.highest_block_number,
381 "value": v,
382 }))?
383 );
384 }
385 } else {
386 let mut iter = rocksdb.iter_from::<tables::AccountsHistory>(seek_key)?;
388 match iter.next() {
389 Some(Ok((k, v))) if k.key == address => {
390 if raw {
391 let raw_val = rocksdb.get_raw::<tables::AccountsHistory>(k)?;
392 if let Some(bytes) = raw_val {
393 println!("{}", hex::encode_prefixed(&bytes));
394 }
395 } else {
396 println!("{}", serde_json::to_string_pretty(&v)?);
397 }
398 }
399 Some(Err(e)) => return Err(e.into()),
400 _ => {
401 error!(target: "reth::cli", "No content for the given table key.");
402 }
403 }
404 }
405 Ok(())
406 }
407 Err(e) if looks_like_address(key) => Err(e),
408 Err(_) => {
409 if all_shards || block.is_some() {
412 return Err(eyre::eyre!(
413 "--block and --all-shards require a plain address, not a JSON key"
414 ));
415 }
416 get_rocksdb_table::<tables::AccountsHistory>(rocksdb, key, raw)
417 }
418 }
419}
420
421fn get_rocksdb_storage_history(
425 rocksdb: &reth_provider::providers::RocksDBProvider,
426 key: &str,
427 storage_key: Option<&str>,
428 block: Option<u64>,
429 all_shards: bool,
430 raw: bool,
431) -> eyre::Result<()> {
432 match parse_address(key) {
433 Ok(address) => {
434 let storage_key = storage_key
435 .map(|s| s.trim_matches('"').parse::<B256>())
436 .transpose()
437 .map_err(|e| eyre::eyre!("failed to parse storage key: {e}"))?
438 .unwrap_or_default();
439 let block_number = block.unwrap_or(u64::MAX);
440 let seek_key = StorageShardedKey::new(address, storage_key, block_number);
441
442 if all_shards {
443 let start = StorageShardedKey::new(address, storage_key, 0);
444 let iter = rocksdb.iter_from::<tables::StoragesHistory>(start)?;
445 for result in iter {
446 let (k, v) = result?;
447 if k.address != address || k.sharded_key.key != storage_key {
448 break;
449 }
450 println!(
451 "{}",
452 serde_json::to_string_pretty(&serde_json::json!({
453 "highest_block_number": k.sharded_key.highest_block_number,
454 "value": v,
455 }))?
456 );
457 }
458 } else {
459 let mut iter = rocksdb.iter_from::<tables::StoragesHistory>(seek_key)?;
460 match iter.next() {
461 Some(Ok((k, v)))
462 if k.address == address && k.sharded_key.key == storage_key =>
463 {
464 if raw {
465 let raw_val = rocksdb.get_raw::<tables::StoragesHistory>(k)?;
466 if let Some(bytes) = raw_val {
467 println!("{}", hex::encode_prefixed(&bytes));
468 }
469 } else {
470 println!("{}", serde_json::to_string_pretty(&v)?);
471 }
472 }
473 Some(Err(e)) => return Err(e.into()),
474 _ => {
475 error!(target: "reth::cli", "No content for the given table key.");
476 }
477 }
478 }
479 Ok(())
480 }
481 Err(e) if looks_like_address(key) => Err(e),
482 Err(_) => {
483 if all_shards || block.is_some() || storage_key.is_some() {
484 return Err(eyre::eyre!(
485 "--block, --all-shards, and --storage-key require a plain address, not a JSON key"
486 ));
487 }
488 get_rocksdb_table::<tables::StoragesHistory>(rocksdb, key, raw)
489 }
490 }
491}
492
493fn get_rocksdb_table<T: Table>(
495 rocksdb: &reth_provider::providers::RocksDBProvider,
496 key_str: &str,
497 raw: bool,
498) -> eyre::Result<()> {
499 let key = table_key::<T>(key_str)?;
500
501 if raw {
502 let content = rocksdb.get_raw::<T>(key)?;
503 match content {
504 Some(bytes) => println!("{}", hex::encode_prefixed(&bytes)),
505 None => error!(target: "reth::cli", "No content for the given table key."),
506 }
507 } else {
508 let content = rocksdb.get::<T>(key)?;
509 match content {
510 Some(value) => println!("{}", serde_json::to_string_pretty(&value)?),
511 None => error!(target: "reth::cli", "No content for the given table key."),
512 }
513 }
514
515 Ok(())
516}
517
518pub(crate) fn table_key<T: Table>(key: &str) -> Result<T::Key, eyre::Error> {
520 serde_json::from_str(key).map_err(|e| eyre::eyre!(e))
521}
522
523fn table_subkey<T: DupSort>(subkey: &str) -> Result<T::SubKey, eyre::Error> {
525 serde_json::from_str(subkey).map_err(|e| eyre::eyre!(e))
526}
527
528struct GetValueViewer<'a, N: NodeTypesWithDB> {
529 tool: &'a DbTool<N>,
530 key: String,
531 subkey: Option<String>,
532 end_key: Option<String>,
533 end_subkey: Option<String>,
534 raw: bool,
535}
536
537impl<N: ProviderNodeTypes> TableViewer<()> for GetValueViewer<'_, N> {
538 type Error = eyre::Report;
539
540 fn view<T: Table>(&self) -> Result<(), Self::Error> {
541 let key = table_key::<T>(&self.key)?;
542
543 if self.end_key.is_some() || self.end_subkey.is_some() {
547 return Err(eyre::eyre!("Only END_KEY can be given for non-DUPSORT tables"));
548 }
549
550 let end_key = self.subkey.clone();
551
552 if let Some(ref end_key_str) = end_key {
554 let end_key = table_key::<T>(end_key_str)?;
555
556 self.tool.provider_factory.db_ref().view(|tx| {
558 let mut cursor = tx.cursor_read::<T>()?;
559 let walker = cursor.walk_range(key..end_key)?;
560
561 for result in walker {
562 let (k, v) = result?;
563 let json_val = if self.raw {
564 let raw_key = RawKey::from(k);
565 serde_json::json!({
566 "key": hex::encode_prefixed(raw_key.raw_key()),
567 "val": hex::encode_prefixed(v.compress().as_ref()),
568 })
569 } else {
570 serde_json::json!({
571 "key": &k,
572 "val": &v,
573 })
574 };
575
576 println!("{}", serde_json::to_string_pretty(&json_val)?);
577 }
578
579 Ok::<_, eyre::Report>(())
580 })??;
581 } else {
582 let content = if self.raw {
584 self.tool
585 .get::<RawTable<T>>(RawKey::from(key))?
586 .map(|content| hex::encode_prefixed(content.raw_value()))
587 } else {
588 self.tool.get::<T>(key)?.as_ref().map(serde_json::to_string_pretty).transpose()?
589 };
590
591 match content {
592 Some(content) => {
593 println!("{content}");
594 }
595 None => {
596 error!(target: "reth::cli", "No content for the given table key.");
597 }
598 };
599 }
600
601 Ok(())
602 }
603
604 fn view_dupsort<T: DupSort>(&self) -> Result<(), Self::Error>
605 where
606 T::Value: reth_primitives_traits::ValueWithSubKey<SubKey = T::SubKey>,
607 {
608 let key = table_key::<T>(&self.key)?;
610
611 if let Some(ref end_key_str) = self.end_key {
613 let end_key = table_key::<T>(end_key_str)?;
614 let Some(start_subkey) = self.subkey.as_deref() else {
615 eyre::bail!("a subkey is required when end_key is given");
616 };
617 let start_subkey = table_subkey::<T>(start_subkey)?;
618 let end_subkey_parsed =
619 self.end_subkey.as_deref().map(table_subkey::<T>).transpose()?;
620
621 self.tool.provider_factory.db_ref().view(|tx| {
622 let mut cursor = tx.cursor_dup_read::<T>()?;
623
624 if let Some((decoded_key, _)) = cursor.seek(key.clone())? &&
627 decoded_key == key
628 {
629 cursor.seek_by_key_subkey(key.clone(), start_subkey.clone())?;
630 }
631
632 let mut current = cursor.current()?;
634
635 while let Some((decoded_key, decoded_value)) = current {
636 let decoded_subkey = decoded_value.get_subkey();
638
639 if (&decoded_key, Some(&decoded_subkey)) >=
641 (&end_key, end_subkey_parsed.as_ref())
642 {
643 break;
644 }
645
646 let json_val = if self.raw {
648 let raw_key = RawKey::from(decoded_key.clone());
649 serde_json::json!({
650 "key": hex::encode_prefixed(raw_key.raw_key()),
651 "val": hex::encode_prefixed(decoded_value.compress().as_ref()),
652 })
653 } else {
654 serde_json::json!({
655 "key": &decoded_key,
656 "val": &decoded_value,
657 })
658 };
659
660 println!("{}", serde_json::to_string_pretty(&json_val)?);
661
662 current = cursor.next()?;
664 }
665
666 Ok::<_, eyre::Report>(())
667 })??;
668 } else {
669 let Some(subkey) = self.subkey.as_deref() else {
671 eyre::bail!("a subkey is required for dupsort tables");
672 };
673 let subkey = table_subkey::<T>(subkey)?;
674
675 let content = if self.raw {
676 self.tool
677 .get_dup::<RawDupSort<T>>(RawKey::from(key), RawKey::from(subkey))?
678 .map(|content| hex::encode_prefixed(content.raw_value()))
679 } else {
680 self.tool
681 .get_dup::<T>(key, subkey)?
682 .as_ref()
683 .map(serde_json::to_string_pretty)
684 .transpose()?
685 };
686
687 match content {
688 Some(content) => {
689 println!("{content}");
690 }
691 None => {
692 error!(target: "reth::cli", "No content for the given table subkey.");
693 }
694 };
695 }
696 Ok(())
697 }
698}
699
700pub(crate) fn maybe_json_value_parser(value: &str) -> Result<String, eyre::Error> {
702 if serde_json::from_str::<serde::de::IgnoredAny>(value).is_ok() {
703 Ok(value.to_string())
704 } else {
705 serde_json::to_string(&value).map_err(|e| eyre::eyre!(e))
706 }
707}
708
709#[cfg(test)]
710mod tests {
711
712 use super::*;
713 use alloy_primitives::{address, b256, B256};
714 use clap::{Args, Parser};
715 use reth_db_api::{
716 models::{storage_sharded_key::StorageShardedKey, ShardedKey},
717 AccountsHistory, HashedAccounts, Headers, StageCheckpoints, StoragesHistory,
718 };
719
720 #[derive(Parser)]
722 struct CommandParser<T: Args> {
723 #[command(flatten)]
724 args: T,
725 }
726
727 #[test]
728 fn parse_numeric_key_args() {
729 assert_eq!(table_key::<Headers>("123").unwrap(), 123);
730 assert_eq!(
731 table_key::<HashedAccounts>(
732 "\"0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac\""
733 )
734 .unwrap(),
735 b256!("0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac")
736 );
737 }
738
739 #[test]
740 fn parse_string_key_args() {
741 assert_eq!(
742 table_key::<StageCheckpoints>("\"MerkleExecution\"").unwrap(),
743 "MerkleExecution"
744 );
745 }
746
747 #[test]
748 fn parse_json_key_args() {
749 assert_eq!(
750 table_key::<StoragesHistory>(r#"{ "address": "0x01957911244e546ce519fbac6f798958fafadb41", "sharded_key": { "key": "0x0000000000000000000000000000000000000000000000000000000000000003", "highest_block_number": 18446744073709551615 } }"#).unwrap(),
751 StorageShardedKey::new(
752 address!("0x01957911244e546ce519fbac6f798958fafadb41"),
753 B256::with_last_byte(3),
754 18446744073709551615
755 )
756 );
757 }
758
759 #[test]
760 fn parse_json_key_for_account_history() {
761 assert_eq!(
762 table_key::<AccountsHistory>(r#"{ "key": "0x4448e1273fd5a8bfdb9ed111e96889c960eee145", "highest_block_number": 18446744073709551615 }"#).unwrap(),
763 ShardedKey::new(
764 address!("0x4448e1273fd5a8bfdb9ed111e96889c960eee145"),
765 18446744073709551615
766 )
767 );
768 }
769}