reth_cli_commands/db/
get.rs

1use alloy_primitives::{hex, BlockHash};
2use clap::Parser;
3use reth_db::{
4    static_file::{
5        ColumnSelectorOne, ColumnSelectorTwo, HeaderWithHashMask, ReceiptMask, TransactionMask,
6        TransactionSenderMask,
7    },
8    RawDupSort,
9};
10use reth_db_api::{
11    cursor::{DbCursorRO, DbDupCursorRO},
12    database::Database,
13    table::{Compress, Decompress, DupSort, Table},
14    tables,
15    transaction::DbTx,
16    RawKey, RawTable, Receipts, TableViewer, Transactions,
17};
18use reth_db_common::DbTool;
19use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
20use reth_node_builder::NodeTypesWithDB;
21use reth_primitives_traits::ValueWithSubKey;
22use reth_provider::{providers::ProviderNodeTypes, StaticFileProviderFactory};
23use reth_static_file_types::StaticFileSegment;
24use tracing::error;
25
26/// The arguments for the `reth db get` command
27#[derive(Parser, Debug)]
28pub struct Command {
29    #[command(subcommand)]
30    subcommand: Subcommand,
31}
32
33#[derive(clap::Subcommand, Debug)]
34enum Subcommand {
35    /// Gets the content of a database table for the given key
36    Mdbx {
37        table: tables::Tables,
38
39        /// The key to get content for
40        #[arg(value_parser = maybe_json_value_parser)]
41        key: String,
42
43        /// The subkey to get content for
44        #[arg(value_parser = maybe_json_value_parser)]
45        subkey: Option<String>,
46
47        /// Optional end key for range query (exclusive upper bound)
48        #[arg(value_parser = maybe_json_value_parser)]
49        end_key: Option<String>,
50
51        /// Optional end subkey for range query (exclusive upper bound)
52        #[arg(value_parser = maybe_json_value_parser)]
53        end_subkey: Option<String>,
54
55        /// Output bytes instead of human-readable decoded value
56        #[arg(long)]
57        raw: bool,
58    },
59    /// Gets the content of a static file segment for the given key
60    StaticFile {
61        segment: StaticFileSegment,
62
63        /// The key to get content for
64        #[arg(value_parser = maybe_json_value_parser)]
65        key: String,
66
67        /// Output bytes instead of human-readable decoded value
68        #[arg(long)]
69        raw: bool,
70    },
71}
72
73impl Command {
74    /// Execute `db get` command
75    pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
76        match self.subcommand {
77            Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => {
78                table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })?
79            }
80            Subcommand::StaticFile { segment, key, raw } => {
81                let (key, mask): (u64, _) = match segment {
82                    StaticFileSegment::Headers => (
83                        table_key::<tables::Headers>(&key)?,
84                        <HeaderWithHashMask<HeaderTy<N>>>::MASK,
85                    ),
86                    StaticFileSegment::Transactions => {
87                        (table_key::<tables::Transactions>(&key)?, <TransactionMask<TxTy<N>>>::MASK)
88                    }
89                    StaticFileSegment::Receipts => {
90                        (table_key::<tables::Receipts>(&key)?, <ReceiptMask<ReceiptTy<N>>>::MASK)
91                    }
92                    StaticFileSegment::TransactionSenders => (
93                        table_key::<tables::TransactionSenders>(&key)?,
94                        <TransactionSenderMask>::MASK,
95                    ),
96                };
97
98                let content = tool
99                    .provider_factory
100                    .static_file_provider()
101                    .get_segment_provider(segment, key)?
102                    .cursor()?
103                    .get(key.into(), mask)
104                    .map(|result| {
105                        result.map(|vec| vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>())
106                    })?;
107
108                match content {
109                    Some(content) => {
110                        if raw {
111                            println!("{}", hex::encode_prefixed(&content[0]));
112                        } else {
113                            match segment {
114                                StaticFileSegment::Headers => {
115                                    let header = HeaderTy::<N>::decompress(content[0].as_slice())?;
116                                    let block_hash = BlockHash::decompress(content[1].as_slice())?;
117                                    println!(
118                                        "Header\n{}\n\nBlockHash\n{}",
119                                        serde_json::to_string_pretty(&header)?,
120                                        serde_json::to_string_pretty(&block_hash)?
121                                    );
122                                }
123                                StaticFileSegment::Transactions => {
124                                    let transaction = <<Transactions as Table>::Value>::decompress(
125                                        content[0].as_slice(),
126                                    )?;
127                                    println!("{}", serde_json::to_string_pretty(&transaction)?);
128                                }
129                                StaticFileSegment::Receipts => {
130                                    let receipt = <<Receipts as Table>::Value>::decompress(
131                                        content[0].as_slice(),
132                                    )?;
133                                    println!("{}", serde_json::to_string_pretty(&receipt)?);
134                                }
135                                StaticFileSegment::TransactionSenders => {
136                                    let sender =
137                                        <<tables::TransactionSenders as Table>::Value>::decompress(
138                                            content[0].as_slice(),
139                                        )?;
140                                    println!("{}", serde_json::to_string_pretty(&sender)?);
141                                }
142                            }
143                        }
144                    }
145                    None => {
146                        error!(target: "reth::cli", "No content for the given table key.");
147                    }
148                };
149            }
150        }
151
152        Ok(())
153    }
154}
155
156/// Get an instance of key for given table
157pub(crate) fn table_key<T: Table>(key: &str) -> Result<T::Key, eyre::Error> {
158    serde_json::from_str(key).map_err(|e| eyre::eyre!(e))
159}
160
161/// Get an instance of subkey for given dupsort table
162fn table_subkey<T: DupSort>(subkey: Option<&str>) -> Result<T::SubKey, eyre::Error> {
163    serde_json::from_str(subkey.unwrap_or_default()).map_err(|e| eyre::eyre!(e))
164}
165
166struct GetValueViewer<'a, N: NodeTypesWithDB> {
167    tool: &'a DbTool<N>,
168    key: String,
169    subkey: Option<String>,
170    end_key: Option<String>,
171    end_subkey: Option<String>,
172    raw: bool,
173}
174
175impl<N: ProviderNodeTypes> TableViewer<()> for GetValueViewer<'_, N> {
176    type Error = eyre::Report;
177
178    fn view<T: Table>(&self) -> Result<(), Self::Error> {
179        let key = table_key::<T>(&self.key)?;
180
181        // A non-dupsort table cannot have subkeys. The `subkey` arg becomes the `end_key`. First we
182        // check that `end_key` and `end_subkey` weren't previously given, as that wouldn't be
183        // valid.
184        if self.end_key.is_some() || self.end_subkey.is_some() {
185            return Err(eyre::eyre!("Only END_KEY can be given for non-DUPSORT tables"));
186        }
187
188        let end_key = self.subkey.clone();
189
190        // Check if we're doing a range query
191        if let Some(ref end_key_str) = end_key {
192            let end_key = table_key::<T>(end_key_str)?;
193
194            // Use walk_range to iterate over the range
195            self.tool.provider_factory.db_ref().view(|tx| {
196                let mut cursor = tx.cursor_read::<T>()?;
197                let walker = cursor.walk_range(key..end_key)?;
198
199                for result in walker {
200                    let (k, v) = result?;
201                    let json_val = if self.raw {
202                        let raw_key = RawKey::from(k);
203                        serde_json::json!({
204                            "key": hex::encode_prefixed(raw_key.raw_key()),
205                            "val": hex::encode_prefixed(v.compress().as_ref()),
206                        })
207                    } else {
208                        serde_json::json!({
209                            "key": &k,
210                            "val": &v,
211                        })
212                    };
213
214                    println!("{}", serde_json::to_string_pretty(&json_val)?);
215                }
216
217                Ok::<_, eyre::Report>(())
218            })??;
219        } else {
220            // Single key lookup
221            let content = if self.raw {
222                self.tool
223                    .get::<RawTable<T>>(RawKey::from(key))?
224                    .map(|content| hex::encode_prefixed(content.raw_value()))
225            } else {
226                self.tool.get::<T>(key)?.as_ref().map(serde_json::to_string_pretty).transpose()?
227            };
228
229            match content {
230                Some(content) => {
231                    println!("{content}");
232                }
233                None => {
234                    error!(target: "reth::cli", "No content for the given table key.");
235                }
236            };
237        }
238
239        Ok(())
240    }
241
242    fn view_dupsort<T: DupSort>(&self) -> Result<(), Self::Error>
243    where
244        T::Value: reth_primitives_traits::ValueWithSubKey<SubKey = T::SubKey>,
245    {
246        // get a key for given table
247        let key = table_key::<T>(&self.key)?;
248
249        // Check if we're doing a range query
250        if let Some(ref end_key_str) = self.end_key {
251            let end_key = table_key::<T>(end_key_str)?;
252            let start_subkey = table_subkey::<T>(Some(
253                self.subkey.as_ref().expect("must have been given if end_key is given").as_str(),
254            ))?;
255            let end_subkey_parsed = self
256                .end_subkey
257                .as_ref()
258                .map(|s| table_subkey::<T>(Some(s.as_str())))
259                .transpose()?;
260
261            self.tool.provider_factory.db_ref().view(|tx| {
262                let mut cursor = tx.cursor_dup_read::<T>()?;
263
264                // Seek to the starting key. If there is actually a key at the starting key then
265                // seek to the subkey within it.
266                if let Some((decoded_key, _)) = cursor.seek(key.clone())? &&
267                    decoded_key == key
268                {
269                    cursor.seek_by_key_subkey(key.clone(), start_subkey.clone())?;
270                }
271
272                // Get the current position to start iteration
273                let mut current = cursor.current()?;
274
275                while let Some((decoded_key, decoded_value)) = current {
276                    // Extract the subkey using the ValueWithSubKey trait
277                    let decoded_subkey = decoded_value.get_subkey();
278
279                    // Check if we've reached the end (exclusive)
280                    if (&decoded_key, Some(&decoded_subkey)) >=
281                        (&end_key, end_subkey_parsed.as_ref())
282                    {
283                        break;
284                    }
285
286                    // Output the entry with both key and subkey
287                    let json_val = if self.raw {
288                        let raw_key = RawKey::from(decoded_key.clone());
289                        serde_json::json!({
290                            "key": hex::encode_prefixed(raw_key.raw_key()),
291                            "val": hex::encode_prefixed(decoded_value.compress().as_ref()),
292                        })
293                    } else {
294                        serde_json::json!({
295                            "key": &decoded_key,
296                            "val": &decoded_value,
297                        })
298                    };
299
300                    println!("{}", serde_json::to_string_pretty(&json_val)?);
301
302                    // Move to next entry
303                    current = cursor.next()?;
304                }
305
306                Ok::<_, eyre::Report>(())
307            })??;
308        } else {
309            // Single key/subkey lookup
310            let subkey = table_subkey::<T>(self.subkey.as_deref())?;
311
312            let content = if self.raw {
313                self.tool
314                    .get_dup::<RawDupSort<T>>(RawKey::from(key), RawKey::from(subkey))?
315                    .map(|content| hex::encode_prefixed(content.raw_value()))
316            } else {
317                self.tool
318                    .get_dup::<T>(key, subkey)?
319                    .as_ref()
320                    .map(serde_json::to_string_pretty)
321                    .transpose()?
322            };
323
324            match content {
325                Some(content) => {
326                    println!("{content}");
327                }
328                None => {
329                    error!(target: "reth::cli", "No content for the given table subkey.");
330                }
331            };
332        }
333        Ok(())
334    }
335}
336
337/// Map the user input value to json
338pub(crate) fn maybe_json_value_parser(value: &str) -> Result<String, eyre::Error> {
339    if serde_json::from_str::<serde::de::IgnoredAny>(value).is_ok() {
340        Ok(value.to_string())
341    } else {
342        serde_json::to_string(&value).map_err(|e| eyre::eyre!(e))
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use alloy_primitives::{address, B256};
350    use clap::{Args, Parser};
351    use reth_db_api::{
352        models::{storage_sharded_key::StorageShardedKey, ShardedKey},
353        AccountsHistory, HashedAccounts, Headers, StageCheckpoints, StoragesHistory,
354    };
355    use std::str::FromStr;
356
357    /// A helper type to parse Args more easily
358    #[derive(Parser)]
359    struct CommandParser<T: Args> {
360        #[command(flatten)]
361        args: T,
362    }
363
364    #[test]
365    fn parse_numeric_key_args() {
366        assert_eq!(table_key::<Headers>("123").unwrap(), 123);
367        assert_eq!(
368            table_key::<HashedAccounts>(
369                "\"0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac\""
370            )
371            .unwrap(),
372            B256::from_str("0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac")
373                .unwrap()
374        );
375    }
376
377    #[test]
378    fn parse_string_key_args() {
379        assert_eq!(
380            table_key::<StageCheckpoints>("\"MerkleExecution\"").unwrap(),
381            "MerkleExecution"
382        );
383    }
384
385    #[test]
386    fn parse_json_key_args() {
387        assert_eq!(
388            table_key::<StoragesHistory>(r#"{ "address": "0x01957911244e546ce519fbac6f798958fafadb41", "sharded_key": { "key": "0x0000000000000000000000000000000000000000000000000000000000000003", "highest_block_number": 18446744073709551615 } }"#).unwrap(),
389            StorageShardedKey::new(
390                address!("0x01957911244e546ce519fbac6f798958fafadb41"),
391                B256::from_str(
392                    "0x0000000000000000000000000000000000000000000000000000000000000003"
393                )
394                .unwrap(),
395                18446744073709551615
396            )
397        );
398    }
399
400    #[test]
401    fn parse_json_key_for_account_history() {
402        assert_eq!(
403            table_key::<AccountsHistory>(r#"{ "key": "0x4448e1273fd5a8bfdb9ed111e96889c960eee145", "highest_block_number": 18446744073709551615 }"#).unwrap(),
404            ShardedKey::new(
405                address!("0x4448e1273fd5a8bfdb9ed111e96889c960eee145"),
406                18446744073709551615
407            )
408        );
409    }
410}