Skip to main content

reth_cli_commands/db/
state.rs

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
24/// Log progress every 30 seconds
25const LOG_INTERVAL: Duration = Duration::from_secs(30);
26
27/// The arguments for the `reth db state` command
28#[derive(Parser, Debug)]
29pub struct Command {
30    /// The account address to get state for
31    address: Address,
32
33    /// Block number to query state at (uses current state if not provided)
34    #[arg(long, short)]
35    block: Option<BlockNumber>,
36
37    /// Maximum number of storage slots to display
38    #[arg(long, short, default_value = "100")]
39    limit: usize,
40
41    /// Output format (table, json, csv)
42    #[arg(long, short, default_value = "table")]
43    format: OutputFormat,
44}
45
46impl Command {
47    /// Execute `db state` command
48    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                // Get account info
96                let account = tx.get::<tables::PlainAccountState>(address)?;
97                // Get storage entries
98                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        // Get account info at that block
144        let account = provider.basic_account(&address)?;
145
146        // Check storage settings to determine where history is stored
147        let storage_settings = tool.provider_factory.cached_storage_settings();
148        let history_in_rocksdb = storage_settings.storage_v2;
149
150        // For historical queries, enumerate keys from history indices only
151        // (not PlainStorageState, which reflects current state)
152        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        // Now query each key at the historical block using the StateProvider
169        // This handles both MDBX and RocksDB backends transparently
170        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    /// Collects storage keys from static file StorageChangeSets (storage_v2).
205    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    /// Collects storage keys from MDBX StorageChangeSets using parallel block range scanning.
262    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; // 500k blocks per thread
269        let num_threads = std::thread::available_parallelism()
270            .map(|p| p.get().saturating_sub(1).max(1))
271            .unwrap_or(4);
272
273        // Get the current tip block
274        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        // Shared state for collecting keys
290        let collected_keys: Mutex<BTreeSet<B256>> = Mutex::new(BTreeSet::new());
291        let total_entries_scanned = Mutex::new(0usize);
292
293        // Create chunk ranges
294        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                            // Get next chunk to process
314                            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                            // Open a new read transaction for this chunk
327                            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                                // Merge into global state
349                                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}