From abfb6d3965be119b1f97f60bdb1630a3a040cb48 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Tue, 9 Dec 2025 21:37:06 +0100 Subject: [PATCH] feat(cli): Allow walking a range of an MDBX table using `db mdbx get` (#20233) --- crates/cli/commands/src/db/get.rs | 202 ++++++++++++++---- crates/primitives-traits/src/lib.rs | 2 +- crates/primitives-traits/src/storage.rs | 20 ++ crates/storage/db-api/src/tables/mod.rs | 5 +- crates/storage/db-models/src/accounts.rs | 10 +- crates/trie/common/src/storage.rs | 17 ++ .../docs/pages/cli/op-reth/db/get/mdbx.mdx | 8 +- docs/vocs/docs/pages/cli/reth/db/get/mdbx.mdx | 8 +- 8 files changed, 226 insertions(+), 46 deletions(-) diff --git a/crates/cli/commands/src/db/get.rs b/crates/cli/commands/src/db/get.rs index f3727b6dc6..8f78873b7a 100644 --- a/crates/cli/commands/src/db/get.rs +++ b/crates/cli/commands/src/db/get.rs @@ -8,12 +8,17 @@ use reth_db::{ RawDupSort, }; use reth_db_api::{ - table::{Decompress, DupSort, Table}, - tables, RawKey, RawTable, Receipts, TableViewer, Transactions, + cursor::{DbCursorRO, DbDupCursorRO}, + database::Database, + table::{Compress, Decompress, DupSort, Table}, + tables, + transaction::DbTx, + RawKey, RawTable, Receipts, TableViewer, Transactions, }; use reth_db_common::DbTool; use reth_node_api::{HeaderTy, ReceiptTy, TxTy}; use reth_node_builder::NodeTypesWithDB; +use reth_primitives_traits::ValueWithSubKey; use reth_provider::{providers::ProviderNodeTypes, StaticFileProviderFactory}; use reth_static_file_types::StaticFileSegment; use tracing::error; @@ -39,6 +44,14 @@ enum Subcommand { #[arg(value_parser = maybe_json_value_parser)] subkey: Option, + /// Optional end key for range query (exclusive upper bound) + #[arg(value_parser = maybe_json_value_parser)] + end_key: Option, + + /// Optional end subkey for range query (exclusive upper bound) + #[arg(value_parser = maybe_json_value_parser)] + end_subkey: Option, + /// Output bytes instead of human-readable decoded value #[arg(long)] raw: bool, @@ -61,8 +74,8 @@ impl Command { /// Execute `db get` command pub fn execute(self, tool: &DbTool) -> eyre::Result<()> { match self.subcommand { - Subcommand::Mdbx { table, key, subkey, raw } => { - table.view(&GetValueViewer { tool, key, subkey, raw })? + Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => { + table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })? } Subcommand::StaticFile { segment, key, raw } => { let (key, mask): (u64, _) = match segment { @@ -154,6 +167,8 @@ struct GetValueViewer<'a, N: NodeTypesWithDB> { tool: &'a DbTool, key: String, subkey: Option, + end_key: Option, + end_subkey: Option, raw: bool, } @@ -163,53 +178,158 @@ impl TableViewer<()> for GetValueViewer<'_, N> { fn view(&self) -> Result<(), Self::Error> { let key = table_key::(&self.key)?; - let content = if self.raw { - self.tool - .get::>(RawKey::from(key))? - .map(|content| hex::encode_prefixed(content.raw_value())) - } else { - self.tool.get::(key)?.as_ref().map(serde_json::to_string_pretty).transpose()? - }; + // A non-dupsort table cannot have subkeys. The `subkey` arg becomes the `end_key`. First we + // check that `end_key` and `end_subkey` weren't previously given, as that wouldn't be + // valid. + if self.end_key.is_some() || self.end_subkey.is_some() { + return Err(eyre::eyre!("Only END_KEY can be given for non-DUPSORT tables")); + } - match content { - Some(content) => { - println!("{content}"); - } - None => { - error!(target: "reth::cli", "No content for the given table key."); - } - }; + let end_key = self.subkey.clone(); + + // Check if we're doing a range query + if let Some(ref end_key_str) = end_key { + let end_key = table_key::(end_key_str)?; + + // Use walk_range to iterate over the range + self.tool.provider_factory.db_ref().view(|tx| { + let mut cursor = tx.cursor_read::()?; + let walker = cursor.walk_range(key..end_key)?; + + for result in walker { + let (k, v) = result?; + let json_val = if self.raw { + let raw_key = RawKey::from(k); + serde_json::json!({ + "key": hex::encode_prefixed(raw_key.raw_key()), + "val": hex::encode_prefixed(v.compress().as_ref()), + }) + } else { + serde_json::json!({ + "key": &k, + "val": &v, + }) + }; + + println!("{}", serde_json::to_string_pretty(&json_val)?); + } + + Ok::<_, eyre::Report>(()) + })??; + } else { + // Single key lookup + let content = if self.raw { + self.tool + .get::>(RawKey::from(key))? + .map(|content| hex::encode_prefixed(content.raw_value())) + } else { + self.tool.get::(key)?.as_ref().map(serde_json::to_string_pretty).transpose()? + }; + + match content { + Some(content) => { + println!("{content}"); + } + None => { + error!(target: "reth::cli", "No content for the given table key."); + } + }; + } Ok(()) } - fn view_dupsort(&self) -> Result<(), Self::Error> { + fn view_dupsort(&self) -> Result<(), Self::Error> + where + T::Value: reth_primitives_traits::ValueWithSubKey, + { // get a key for given table let key = table_key::(&self.key)?; - // process dupsort table - let subkey = table_subkey::(self.subkey.as_deref())?; - - let content = if self.raw { - self.tool - .get_dup::>(RawKey::from(key), RawKey::from(subkey))? - .map(|content| hex::encode_prefixed(content.raw_value())) - } else { - self.tool - .get_dup::(key, subkey)? + // Check if we're doing a range query + if let Some(ref end_key_str) = self.end_key { + let end_key = table_key::(end_key_str)?; + let start_subkey = table_subkey::(Some( + self.subkey.as_ref().expect("must have been given if end_key is given").as_str(), + ))?; + let end_subkey_parsed = self + .end_subkey .as_ref() - .map(serde_json::to_string_pretty) - .transpose()? - }; + .map(|s| table_subkey::(Some(s.as_str()))) + .transpose()?; - match content { - Some(content) => { - println!("{content}"); - } - None => { - error!(target: "reth::cli", "No content for the given table subkey."); - } - }; + self.tool.provider_factory.db_ref().view(|tx| { + let mut cursor = tx.cursor_dup_read::()?; + + // Seek to the starting key. If there is actually a key at the starting key then + // seek to the subkey within it. + if let Some((decoded_key, _)) = cursor.seek(key.clone())? && + decoded_key == key + { + cursor.seek_by_key_subkey(key.clone(), start_subkey.clone())?; + } + + // Get the current position to start iteration + let mut current = cursor.current()?; + + while let Some((decoded_key, decoded_value)) = current { + // Extract the subkey using the ValueWithSubKey trait + let decoded_subkey = decoded_value.get_subkey(); + + // Check if we've reached the end (exclusive) + if (&decoded_key, Some(&decoded_subkey)) >= + (&end_key, end_subkey_parsed.as_ref()) + { + break; + } + + // Output the entry with both key and subkey + let json_val = if self.raw { + let raw_key = RawKey::from(decoded_key.clone()); + serde_json::json!({ + "key": hex::encode_prefixed(raw_key.raw_key()), + "val": hex::encode_prefixed(decoded_value.compress().as_ref()), + }) + } else { + serde_json::json!({ + "key": &decoded_key, + "val": &decoded_value, + }) + }; + + println!("{}", serde_json::to_string_pretty(&json_val)?); + + // Move to next entry + current = cursor.next()?; + } + + Ok::<_, eyre::Report>(()) + })??; + } else { + // Single key/subkey lookup + let subkey = table_subkey::(self.subkey.as_deref())?; + + let content = if self.raw { + self.tool + .get_dup::>(RawKey::from(key), RawKey::from(subkey))? + .map(|content| hex::encode_prefixed(content.raw_value())) + } else { + self.tool + .get_dup::(key, subkey)? + .as_ref() + .map(serde_json::to_string_pretty) + .transpose()? + }; + + match content { + Some(content) => { + println!("{content}"); + } + None => { + error!(target: "reth::cli", "No content for the given table subkey."); + } + }; + } Ok(()) } } diff --git a/crates/primitives-traits/src/lib.rs b/crates/primitives-traits/src/lib.rs index 5400f52a20..18fb6292bd 100644 --- a/crates/primitives-traits/src/lib.rs +++ b/crates/primitives-traits/src/lib.rs @@ -164,7 +164,7 @@ pub use alloy_primitives::{logs_bloom, Log, LogData}; pub mod proofs; mod storage; -pub use storage::StorageEntry; +pub use storage::{StorageEntry, ValueWithSubKey}; pub mod sync; diff --git a/crates/primitives-traits/src/storage.rs b/crates/primitives-traits/src/storage.rs index c6b9b1e11c..4383f03cf9 100644 --- a/crates/primitives-traits/src/storage.rs +++ b/crates/primitives-traits/src/storage.rs @@ -1,5 +1,17 @@ use alloy_primitives::{B256, U256}; +/// Trait for `DupSort` table values that contain a subkey. +/// +/// This trait allows extracting the subkey from a value during database iteration, +/// enabling proper range queries and filtering on `DupSort` tables. +pub trait ValueWithSubKey { + /// The type of the subkey. + type SubKey; + + /// Extract the subkey from the value. + fn get_subkey(&self) -> Self::SubKey; +} + /// Account storage entry. /// /// `key` is the subkey when used as a value in the `StorageChangeSets` table. @@ -21,6 +33,14 @@ impl StorageEntry { } } +impl ValueWithSubKey for StorageEntry { + type SubKey = B256; + + fn get_subkey(&self) -> Self::SubKey { + self.key + } +} + impl From<(B256, U256)> for StorageEntry { fn from((key, value): (B256, U256)) -> Self { Self { key, value } diff --git a/crates/storage/db-api/src/tables/mod.rs b/crates/storage/db-api/src/tables/mod.rs index 483048383a..903d4ca762 100644 --- a/crates/storage/db-api/src/tables/mod.rs +++ b/crates/storage/db-api/src/tables/mod.rs @@ -94,7 +94,10 @@ pub trait TableViewer { /// Operate on the dupsort table in a generic way. /// /// By default, the `view` function is invoked unless overridden. - fn view_dupsort(&self) -> Result { + fn view_dupsort(&self) -> Result + where + T::Value: reth_primitives_traits::ValueWithSubKey, + { self.view::() } } diff --git a/crates/storage/db-models/src/accounts.rs b/crates/storage/db-models/src/accounts.rs index cbae5d84aa..409d80abaa 100644 --- a/crates/storage/db-models/src/accounts.rs +++ b/crates/storage/db-models/src/accounts.rs @@ -1,5 +1,5 @@ use alloy_primitives::Address; -use reth_primitives_traits::Account; +use reth_primitives_traits::{Account, ValueWithSubKey}; /// Account as it is saved in the database. /// @@ -15,6 +15,14 @@ pub struct AccountBeforeTx { pub info: Option, } +impl ValueWithSubKey for AccountBeforeTx { + type SubKey = Address; + + fn get_subkey(&self) -> Self::SubKey { + self.address + } +} + // NOTE: Removing reth_codec and manually encode subkey // and compress second part of the value. If we have compression // over whole value (Even SubKey) that would mess up fetching of values with seek_by_key_subkey diff --git a/crates/trie/common/src/storage.rs b/crates/trie/common/src/storage.rs index 1e56739386..77d037ff2e 100644 --- a/crates/trie/common/src/storage.rs +++ b/crates/trie/common/src/storage.rs @@ -1,4 +1,5 @@ use super::{BranchNodeCompact, StoredNibblesSubKey}; +use reth_primitives_traits::ValueWithSubKey; /// Account storage trie node. /// @@ -12,6 +13,14 @@ pub struct StorageTrieEntry { pub node: BranchNodeCompact, } +impl ValueWithSubKey for StorageTrieEntry { + type SubKey = StoredNibblesSubKey; + + fn get_subkey(&self) -> Self::SubKey { + self.nibbles.clone() + } +} + // NOTE: Removing reth_codec and manually encode subkey // and compress second part of the value. If we have compression // over whole value (Even SubKey) that would mess up fetching of values with seek_by_key_subkey @@ -46,6 +55,14 @@ pub struct TrieChangeSetsEntry { pub node: Option, } +impl ValueWithSubKey for TrieChangeSetsEntry { + type SubKey = StoredNibblesSubKey; + + fn get_subkey(&self) -> Self::SubKey { + self.nibbles.clone() + } +} + #[cfg(any(test, feature = "reth-codec"))] impl reth_codecs::Compact for TrieChangeSetsEntry { fn to_compact(&self, buf: &mut B) -> usize diff --git a/docs/vocs/docs/pages/cli/op-reth/db/get/mdbx.mdx b/docs/vocs/docs/pages/cli/op-reth/db/get/mdbx.mdx index d06d8ab417..3e0f225bc8 100644 --- a/docs/vocs/docs/pages/cli/op-reth/db/get/mdbx.mdx +++ b/docs/vocs/docs/pages/cli/op-reth/db/get/mdbx.mdx @@ -6,7 +6,7 @@ Gets the content of a database table for the given key $ op-reth db get mdbx --help ``` ```txt -Usage: op-reth db get mdbx [OPTIONS] [SUBKEY] +Usage: op-reth db get mdbx [OPTIONS]
[SUBKEY] [END_KEY] [END_SUBKEY] Arguments:
@@ -18,6 +18,12 @@ Arguments: [SUBKEY] The subkey to get content for + [END_KEY] + Optional end key for range query (exclusive upper bound) + + [END_SUBKEY] + Optional end subkey for range query (exclusive upper bound) + Options: --raw Output bytes instead of human-readable decoded value diff --git a/docs/vocs/docs/pages/cli/reth/db/get/mdbx.mdx b/docs/vocs/docs/pages/cli/reth/db/get/mdbx.mdx index b909d0b0bc..1983cfe7b2 100644 --- a/docs/vocs/docs/pages/cli/reth/db/get/mdbx.mdx +++ b/docs/vocs/docs/pages/cli/reth/db/get/mdbx.mdx @@ -6,7 +6,7 @@ Gets the content of a database table for the given key $ reth db get mdbx --help ``` ```txt -Usage: reth db get mdbx [OPTIONS]
[SUBKEY] +Usage: reth db get mdbx [OPTIONS]
[SUBKEY] [END_KEY] [END_SUBKEY] Arguments:
@@ -18,6 +18,12 @@ Arguments: [SUBKEY] The subkey to get content for + [END_KEY] + Optional end key for range query (exclusive upper bound) + + [END_SUBKEY] + Optional end subkey for range query (exclusive upper bound) + Options: --raw Output bytes instead of human-readable decoded value