1use alloy_primitives::{keccak256, Address, BlockNumber, B256, U256};
2use clap::Parser;
3use parking_lot::Mutex;
4use reth_db_api::{
5 cursor::{DbCursorRO, DbDupCursorRO},
6 database::Database,
7 tables,
8 transaction::DbTx,
9};
10use reth_db_common::DbTool;
11use reth_node_builder::NodeTypesWithDB;
12use reth_provider::{
13 providers::{BlockchainProvider, ProviderNodeTypes},
14 StaticFileProviderFactory,
15};
16use reth_storage_api::{BlockNumReader, StateProvider, StateProviderFactory, StorageSettingsCache};
17use reth_tasks::spawn_scoped_os_thread;
18use std::{
19 collections::BTreeSet,
20 thread,
21 time::{Duration, Instant},
22};
23use tracing::info;
24
25const LOG_INTERVAL: Duration = Duration::from_secs(30);
27
28#[derive(Parser, Debug)]
30pub struct Command {
31 address: Address,
33
34 #[arg(long, short)]
36 block: Option<BlockNumber>,
37
38 #[arg(long, short, default_value = "100")]
40 limit: usize,
41
42 #[arg(long, short, default_value = "table")]
44 format: OutputFormat,
45}
46
47impl Command {
48 pub fn execute<N: NodeTypesWithDB + ProviderNodeTypes>(
50 self,
51 tool: &DbTool<N>,
52 ) -> eyre::Result<()> {
53 let address = self.address;
54 let limit = self.limit;
55
56 if let Some(block) = self.block {
57 self.execute_historical(tool, address, block, limit)
58 } else {
59 self.execute_current(tool, address, limit)
60 }
61 }
62
63 fn execute_current<N: NodeTypesWithDB + ProviderNodeTypes>(
64 &self,
65 tool: &DbTool<N>,
66 address: Address,
67 limit: usize,
68 ) -> eyre::Result<()> {
69 let use_hashed_state = tool.provider_factory.cached_storage_settings().use_hashed_state();
70
71 let entries = tool.provider_factory.db_ref().view(|tx| {
72 let (account, walker_entries) = if use_hashed_state {
73 let hashed_address = keccak256(address);
74 let account = tx.get::<tables::HashedAccounts>(hashed_address)?;
75 let mut cursor = tx.cursor_dup_read::<tables::HashedStorages>()?;
76 let walker = cursor.walk_dup(Some(hashed_address), None)?;
77 let mut entries = Vec::new();
78 let mut last_log = Instant::now();
79 for (idx, entry) in walker.enumerate() {
80 let (_, storage_entry) = entry?;
81 if storage_entry.value != U256::ZERO {
82 entries.push((storage_entry.key, storage_entry.value));
83 }
84 if entries.len() >= limit {
85 break;
86 }
87 if last_log.elapsed() >= LOG_INTERVAL {
88 info!(
89 target: "reth::cli",
90 address = %address,
91 slots_scanned = idx,
92 "Scanning storage slots"
93 );
94 last_log = Instant::now();
95 }
96 }
97 (account, entries)
98 } else {
99 let account = tx.get::<tables::PlainAccountState>(address)?;
101 let mut cursor = tx.cursor_dup_read::<tables::PlainStorageState>()?;
103 let walker = cursor.walk_dup(Some(address), None)?;
104 let mut entries = Vec::new();
105 let mut last_log = Instant::now();
106 for (idx, entry) in walker.enumerate() {
107 let (_, storage_entry) = entry?;
108 if storage_entry.value != U256::ZERO {
109 entries.push((storage_entry.key, storage_entry.value));
110 }
111 if entries.len() >= limit {
112 break;
113 }
114 if last_log.elapsed() >= LOG_INTERVAL {
115 info!(
116 target: "reth::cli",
117 address = %address,
118 slots_scanned = idx,
119 "Scanning storage slots"
120 );
121 last_log = Instant::now();
122 }
123 }
124 (account, entries)
125 };
126
127 Ok::<_, eyre::Report>((account, walker_entries))
128 })??;
129
130 let (account, storage_entries) = entries;
131
132 self.print_results(address, None, account, &storage_entries);
133
134 Ok(())
135 }
136
137 fn execute_historical<N: NodeTypesWithDB + ProviderNodeTypes>(
138 &self,
139 tool: &DbTool<N>,
140 address: Address,
141 block: BlockNumber,
142 limit: usize,
143 ) -> eyre::Result<()> {
144 let provider = BlockchainProvider::new(tool.provider_factory.clone())?
145 .history_by_block_number(block)?;
146
147 let account = provider.basic_account(&address)?;
149
150 let storage_settings = tool.provider_factory.cached_storage_settings();
152 let history_in_rocksdb = storage_settings.storage_v2;
153
154 let mut storage_keys = BTreeSet::new();
157
158 if history_in_rocksdb {
159 self.collect_staticfile_storage_keys(tool, address, &mut storage_keys)?;
160 } else {
161 self.collect_mdbx_storage_keys_parallel(tool, address, &mut storage_keys)?;
162 }
163
164 info!(
165 target: "reth::cli",
166 address = %address,
167 block = block,
168 total_keys = storage_keys.len(),
169 "Found storage keys to query"
170 );
171
172 let mut entries = Vec::new();
175 let mut last_log = Instant::now();
176
177 for (idx, key) in storage_keys.iter().enumerate() {
178 match provider.storage(address, *key) {
179 Ok(Some(value)) if value != U256::ZERO => {
180 entries.push((*key, value));
181 }
182 _ => {}
183 }
184
185 if entries.len() >= limit {
186 break;
187 }
188
189 if last_log.elapsed() >= LOG_INTERVAL {
190 info!(
191 target: "reth::cli",
192 address = %address,
193 block = block,
194 keys_total = storage_keys.len(),
195 slots_scanned = idx,
196 slots_found = entries.len(),
197 "Scanning historical storage slots"
198 );
199 last_log = Instant::now();
200 }
201 }
202
203 self.print_results(address, Some(block), account, &entries);
204
205 Ok(())
206 }
207
208 fn collect_staticfile_storage_keys<N: NodeTypesWithDB + ProviderNodeTypes>(
210 &self,
211 tool: &DbTool<N>,
212 address: Address,
213 keys: &mut BTreeSet<B256>,
214 ) -> eyre::Result<()> {
215 let tip = tool.provider_factory.provider()?.best_block_number()?;
216
217 if tip == 0 {
218 return Ok(());
219 }
220
221 info!(
222 target: "reth::cli",
223 address = %address,
224 tip,
225 "Scanning static file storage changesets"
226 );
227
228 let static_file_provider = tool.provider_factory.static_file_provider();
229 let walker = static_file_provider.walk_storage_changeset_range(0..=tip);
230
231 let mut total_scanned = 0usize;
232 let mut last_log = Instant::now();
233
234 for changeset_result in walker {
235 let (block_addr, storage_entry) = changeset_result?;
236 total_scanned += 1;
237
238 if block_addr.address() == address {
239 keys.insert(storage_entry.key);
240 }
241
242 if last_log.elapsed() >= LOG_INTERVAL {
243 info!(
244 target: "reth::cli",
245 address = %address,
246 entries_scanned = total_scanned,
247 unique_keys = keys.len(),
248 "Scanning static file storage changesets"
249 );
250 last_log = Instant::now();
251 }
252 }
253
254 info!(
255 target: "reth::cli",
256 address = %address,
257 total_entries = total_scanned,
258 unique_keys = keys.len(),
259 "Finished static file storage changeset scan"
260 );
261
262 Ok(())
263 }
264
265 fn collect_mdbx_storage_keys_parallel<N: NodeTypesWithDB + ProviderNodeTypes>(
267 &self,
268 tool: &DbTool<N>,
269 address: Address,
270 keys: &mut BTreeSet<B256>,
271 ) -> eyre::Result<()> {
272 const CHUNK_SIZE: u64 = 500_000; let num_threads = std::thread::available_parallelism()
274 .map(|p| p.get().saturating_sub(1).max(1))
275 .unwrap_or(4);
276
277 let tip = tool.provider_factory.provider()?.best_block_number()?;
279
280 if tip == 0 {
281 return Ok(());
282 }
283
284 info!(
285 target: "reth::cli",
286 address = %address,
287 tip,
288 chunk_size = CHUNK_SIZE,
289 num_threads,
290 "Starting parallel MDBX changeset scan"
291 );
292
293 let collected_keys: Mutex<BTreeSet<B256>> = Mutex::new(BTreeSet::new());
295 let total_entries_scanned = Mutex::new(0usize);
296
297 let mut chunks: Vec<(u64, u64)> = Vec::new();
299 let mut start = 0u64;
300 while start <= tip {
301 let end = (start + CHUNK_SIZE - 1).min(tip);
302 chunks.push((start, end));
303 start = end + 1;
304 }
305
306 let chunks_ref = &chunks;
307 let next_chunk = Mutex::new(0usize);
308 let next_chunk_ref = &next_chunk;
309 let collected_keys_ref = &collected_keys;
310 let total_entries_ref = &total_entries_scanned;
311
312 thread::scope(|s| {
313 let handles: Vec<_> = (0..num_threads)
314 .map(|thread_id| {
315 spawn_scoped_os_thread(s, "db-state-worker", move || {
316 loop {
317 let chunk_idx = {
319 let mut idx = next_chunk_ref.lock();
320 if *idx >= chunks_ref.len() {
321 return Ok::<_, eyre::Report>(());
322 }
323 let current = *idx;
324 *idx += 1;
325 current
326 };
327
328 let (chunk_start, chunk_end) = chunks_ref[chunk_idx];
329
330 tool.provider_factory.db_ref().view(|tx| {
332 tx.disable_long_read_transaction_safety();
333
334 let mut changeset_cursor =
335 tx.cursor_read::<tables::StorageChangeSets>()?;
336 let start_key =
337 reth_db_api::models::BlockNumberAddress((chunk_start, address));
338 let end_key =
339 reth_db_api::models::BlockNumberAddress((chunk_end, address));
340
341 let mut local_keys = BTreeSet::new();
342 let mut entries_in_chunk = 0usize;
343
344 if let Ok(walker) = changeset_cursor.walk_range(start_key..=end_key)
345 {
346 for (block_addr, storage_entry) in walker.flatten() {
347 if block_addr.address() == address {
348 local_keys.insert(storage_entry.key);
349 }
350 entries_in_chunk += 1;
351 }
352 }
353
354 collected_keys_ref.lock().extend(local_keys);
356 *total_entries_ref.lock() += entries_in_chunk;
357
358 info!(
359 target: "reth::cli",
360 thread_id,
361 chunk_start,
362 chunk_end,
363 entries_in_chunk,
364 "Thread completed chunk"
365 );
366
367 Ok::<_, eyre::Report>(())
368 })??;
369 }
370 })
371 })
372 .collect();
373
374 for handle in handles {
375 handle.join().map_err(|_| eyre::eyre!("Thread panicked"))??;
376 }
377
378 Ok::<_, eyre::Report>(())
379 })?;
380
381 let final_keys = collected_keys.into_inner();
382 let total = *total_entries_scanned.lock();
383
384 info!(
385 target: "reth::cli",
386 address = %address,
387 total_entries = total,
388 unique_keys = final_keys.len(),
389 "Finished parallel MDBX changeset scan"
390 );
391
392 keys.extend(final_keys);
393 Ok(())
394 }
395
396 fn print_results(
397 &self,
398 address: Address,
399 block: Option<BlockNumber>,
400 account: Option<reth_primitives_traits::Account>,
401 storage: &[(alloy_primitives::B256, U256)],
402 ) {
403 match self.format {
404 OutputFormat::Table => {
405 println!("Account: {address}");
406 if let Some(b) = block {
407 println!("Block: {b}");
408 } else {
409 println!("Block: latest");
410 }
411 println!();
412
413 if let Some(acc) = account {
414 println!("Nonce: {}", acc.nonce);
415 println!("Balance: {} wei", acc.balance);
416 if let Some(code_hash) = acc.bytecode_hash {
417 println!("Code hash: {code_hash}");
418 }
419 } else {
420 println!("Account not found");
421 }
422
423 println!();
424 println!("Storage ({} slots):", storage.len());
425 println!("{:-<130}", "");
426 println!("{:<66} | {:<64}", "Slot", "Value");
427 println!("{:-<130}", "");
428 for (key, value) in storage {
429 println!("{key} | {value:#066x}");
430 }
431 }
432 OutputFormat::Json => {
433 let output = serde_json::json!({
434 "address": address.to_string(),
435 "block": block,
436 "account": account.map(|a| serde_json::json!({
437 "nonce": a.nonce,
438 "balance": a.balance.to_string(),
439 "code_hash": a.bytecode_hash.map(|h| h.to_string()),
440 })),
441 "storage": storage.iter().map(|(k, v)| {
442 serde_json::json!({
443 "key": k.to_string(),
444 "value": format!("{v:#066x}"),
445 })
446 }).collect::<Vec<_>>(),
447 });
448 println!("{}", serde_json::to_string_pretty(&output).unwrap());
449 }
450 OutputFormat::Csv => {
451 println!("slot,value");
452 for (key, value) in storage {
453 println!("{key},{value:#066x}");
454 }
455 }
456 }
457 }
458}
459
460#[derive(Debug, Clone, Default, clap::ValueEnum)]
461pub enum OutputFormat {
462 #[default]
463 Table,
464 Json,
465 Csv,
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn parse_state_args() {
474 let cmd = Command::try_parse_from([
475 "state",
476 "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
477 "--block",
478 "1000000",
479 ])
480 .unwrap();
481 assert_eq!(
482 cmd.address,
483 "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".parse::<Address>().unwrap()
484 );
485 assert_eq!(cmd.block, Some(1000000));
486 }
487
488 #[test]
489 fn parse_state_args_no_block() {
490 let cmd = Command::try_parse_from(["state", "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"])
491 .unwrap();
492 assert_eq!(cmd.block, None);
493 }
494}