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