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