Compare commits

...

14 Commits

Author SHA1 Message Date
yongkangc
3bcb9bc3e0 feat: wire up RocksDB validation in init_genesis
Integrate the validate_against_persisted() method into init_genesis() so
CLI overrides are validated against persisted storage settings at startup.

- Add RocksDbSettingsMismatch variant to InitStorageError
- Call validation after genesis initialization
- Fail early if CLI flags don't match persisted settings
2026-01-20 09:35:44 +00:00
yongkangc
405bb65e8f test: add additional validation edge case tests
- Add test for matching false overrides against persisted false
- Add test verifying first mismatch is reported when multiple exist
- Add test for partial override matching
2026-01-20 09:25:55 +00:00
yongkangc
43799d9dec fix: address review feedback
- Add backticks around RocksDB in doc comments (clippy::doc_markdown)
- Make apply_to_settings() and validate_against_persisted() const fn
- Collapse nested if statements using let-chains
2026-01-20 09:25:55 +00:00
yongkangc
fd16708181 feat(storage): add startup validation for RocksDB CLI flags
Validates that --rocksdb.* flags match persisted storage layout.
Errors on mismatch since these are genesis-initialization-only settings.
2026-01-20 09:25:55 +00:00
yongkangc
3905e317ff refactor: remove helper function, inline rocksdb settings without logging 2026-01-20 09:23:18 +00:00
yongkangc
aa0f9e8888 fix: add missing tests and DRY up RocksDB logging
- Add test for all-explicit-false edge case
- Add test verifying non-rocksdb fields preserved
- Rename parameter defaults -> settings for clarity
- Extract storage_settings_with_logging() helper to DRY up with_genesis/init_genesis
- Fix clippy doc warnings (backticks around RocksDB)
2026-01-20 09:20:48 +00:00
yongkangc
5f8dd4bb03 fix: log actual enabled tables instead of 'all tables' 2026-01-20 08:59:24 +00:00
yongkangc
9e2d423bd5 feat(cli): add grouped enable behavior for RocksDB flags
- If any --rocksdb.* flag is set to true, all RocksDB tables are enabled
- Explicit --rocksdb.<table>=false overrides can disable specific tables
- Logs startup message when grouped behavior triggers
- DRY'd doc comments (genesis-only note moved to struct level)
- Added comprehensive tests for grouped enable behavior
2026-01-20 08:57:43 +00:00
yongkangc
df237b9efd refactor: simplify RocksDbArgs tests
Consolidate 12 tests down to 3 essential ones covering defaults,
parsing, and settings application.
2026-01-19 22:15:28 +00:00
yongkangc
1aad483ecb docs: regenerate CLI documentation for --rocksdb.* flags 2026-01-19 17:54:17 +00:00
yongkangc
bf13a52e75 fix: add rocksdb field to NodeCommand and fix clippy warnings
Wire RocksDbArgs through NodeCommand to enable CLI flags to be passed
through to NodeConfig. Also fix clippy warnings for doc_markdown
(backticks around RocksDB) and missing_const_for_fn.
2026-01-19 17:45:20 +00:00
yongkangc
cfa9a4ac2b feat: integrate RocksDbArgs into NodeConfig and launch code
- Add rocksdb field to NodeConfig
- Update init_genesis to apply RocksDB settings on top of static files settings
- All struct initializers and Clone impl updated
2026-01-19 15:17:08 +00:00
yongkangc
5166a6c778 fix: add edge case tests for RocksDB CLI flags
- Add test for bare flag without value (should require a value)
- Add test for space-separated value syntax
2026-01-19 15:04:37 +00:00
yongkangc
25f98a7730 feat(cli): add --rocksdb.* flags for RocksDB table routing
Introduces RocksDbArgs struct with --rocksdb.tx-hash, --rocksdb.storages-history,
and --rocksdb.account-history flags for controlling which tables route to RocksDB.

These flags are genesis-initialization-only (changing later requires re-sync).
2026-01-19 14:56:36 +00:00
8 changed files with 425 additions and 14 deletions

View File

@@ -10,7 +10,8 @@ use reth_node_builder::NodeBuilder;
use reth_node_core::{
args::{
DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, EraArgs, MetricArgs,
NetworkArgs, PayloadBuilderArgs, PruningArgs, RpcServerArgs, StaticFilesArgs, TxPoolArgs,
NetworkArgs, PayloadBuilderArgs, PruningArgs, RocksDbArgs, RpcServerArgs, StaticFilesArgs,
TxPoolArgs,
},
node_config::NodeConfig,
version,
@@ -102,6 +103,10 @@ pub struct NodeCommand<C: ChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs
#[command(flatten)]
pub pruning: PruningArgs,
/// All `RocksDB` table routing arguments
#[command(flatten)]
pub rocksdb: RocksDbArgs,
/// Engine cli arguments
#[command(flatten, next_help_heading = "Engine")]
pub engine: EngineArgs,
@@ -166,6 +171,7 @@ where
db,
dev,
pruning,
rocksdb,
engine,
era,
static_files,
@@ -187,6 +193,7 @@ where
db,
dev,
pruning,
rocksdb,
engine,
era,
static_files,

View File

@@ -66,9 +66,9 @@ use reth_node_metrics::{
};
use reth_provider::{
providers::{NodeTypesForProvider, ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
BlockHashReader, BlockNumReader, DatabaseProviderFactory, ProviderError, ProviderFactory,
ProviderResult, RocksDBProviderFactory, StageCheckpointReader, StaticFileProviderBuilder,
StaticFileProviderFactory,
BlockHashReader, BlockNumReader, DatabaseProviderFactory, MetadataProvider, ProviderError,
ProviderFactory, ProviderResult, RocksDBProviderFactory, StageCheckpointReader,
StaticFileProviderBuilder, StaticFileProviderFactory,
};
use reth_prune::{PruneModes, PrunerBuilder};
use reth_rpc_builder::config::RethRpcServerConfig;
@@ -676,19 +676,27 @@ where
/// Convenience function to [`Self::init_genesis`]
pub fn with_genesis(self) -> Result<Self, InitStorageError> {
init_genesis_with_settings(
self.provider_factory(),
self.node_config().static_files.to_settings(),
)?;
self.init_genesis()?;
Ok(self)
}
/// Write the genesis block and state if it has not already been written
/// Write the genesis block and state if it has not already been written.
///
/// After initialization, validates that any CLI `RocksDB` overrides match the persisted
/// storage settings. These settings are genesis-initialization-only and cannot be changed.
pub fn init_genesis(&self) -> Result<B256, InitStorageError> {
init_genesis_with_settings(
self.provider_factory(),
self.node_config().static_files.to_settings(),
)
let base_settings = self.node_config().static_files.to_settings();
let (settings, _) = self.node_config().rocksdb.apply_to_settings(base_settings);
let hash = init_genesis_with_settings(self.provider_factory(), settings)?;
// Validate CLI overrides match persisted settings
if let Some(persisted) = self.provider_factory().storage_settings()?
&& let Err(e) = self.node_config().rocksdb.validate_against_persisted(&persisted)
{
return Err(InitStorageError::RocksDbSettingsMismatch(e.to_string()));
}
Ok(hash)
}
/// Creates a new `WithMeteredProvider` container and attaches it to the

View File

@@ -80,5 +80,9 @@ pub use era::{DefaultEraHost, EraArgs, EraSourceArgs};
mod static_files;
pub use static_files::{StaticFilesArgs, MINIMAL_BLOCKS_PER_FILE};
/// `RocksDbArgs` for configuring RocksDB table routing.
mod rocksdb;
pub use rocksdb::{RocksDbArgs, RocksDbSettingsMismatchError};
mod error;
pub mod types;

View File

@@ -0,0 +1,337 @@
//! clap [Args](clap::Args) for `RocksDB` table routing configuration
use clap::{ArgAction, Args};
use reth_storage_api::StorageSettings;
use std::fmt;
/// Parameters for `RocksDB` table routing configuration.
///
/// These flags control which database tables are stored in `RocksDB` instead of MDBX.
/// All flags are genesis-initialization-only: changing them after genesis requires a re-sync.
#[derive(Debug, Args, PartialEq, Eq, Default, Clone, Copy)]
#[command(next_help_heading = "RocksDB")]
pub struct RocksDbArgs {
/// Route tx hash -> number table to `RocksDB` instead of MDBX.
#[arg(long = "rocksdb.tx-hash", action = ArgAction::Set)]
pub tx_hash: Option<bool>,
/// Route storages history tables to `RocksDB` instead of MDBX.
#[arg(long = "rocksdb.storages-history", action = ArgAction::Set)]
pub storages_history: Option<bool>,
/// Route account history tables to `RocksDB` instead of MDBX.
#[arg(long = "rocksdb.account-history", action = ArgAction::Set)]
pub account_history: Option<bool>,
}
impl RocksDbArgs {
/// Applies CLI overrides to the given defaults, returning the resulting settings.
///
/// If any flag is set to `true`, all `RocksDB` tables are enabled by default, then explicit
/// `false` overrides are applied. This grouped behavior ensures users get the full `RocksDB`
/// benefit without needing to specify all flags.
///
/// Returns `(settings, grouped_enabled)` where `grouped_enabled` is true if the grouped
/// behavior was triggered (any flag was true, enabling all tables).
pub fn apply_to_settings(&self, mut settings: StorageSettings) -> (StorageSettings, bool) {
let any_true = self.tx_hash == Some(true) ||
self.storages_history == Some(true) ||
self.account_history == Some(true);
if any_true {
settings.transaction_hash_numbers_in_rocksdb = true;
settings.storages_history_in_rocksdb = true;
settings.account_history_in_rocksdb = true;
}
if let Some(value) = self.tx_hash {
settings.transaction_hash_numbers_in_rocksdb = value;
}
if let Some(value) = self.storages_history {
settings.storages_history_in_rocksdb = value;
}
if let Some(value) = self.account_history {
settings.account_history_in_rocksdb = value;
}
(settings, any_true)
}
/// Returns true if any `RocksDB` table routing flag was explicitly set.
pub const fn has_overrides(&self) -> bool {
self.tx_hash.is_some() || self.storages_history.is_some() || self.account_history.is_some()
}
/// Validates that CLI overrides match the persisted storage settings.
///
/// This should be called at startup after loading the persisted settings but before
/// the pipeline starts. If any CLI override differs from the persisted value,
/// returns an error since these are genesis-initialization-only settings.
///
/// Returns `Ok(())` if:
/// - No CLI overrides are set (all `None`)
/// - All CLI overrides match the persisted values
///
/// Returns `Err` if any CLI override differs from the persisted value.
pub const fn validate_against_persisted(
&self,
persisted: &StorageSettings,
) -> Result<(), RocksDbSettingsMismatchError> {
if let Some(cli_value) = self.tx_hash &&
cli_value != persisted.transaction_hash_numbers_in_rocksdb
{
return Err(RocksDbSettingsMismatchError {
flag_name: "--rocksdb.tx-hash",
expected: persisted.transaction_hash_numbers_in_rocksdb,
got: cli_value,
});
}
if let Some(cli_value) = self.storages_history &&
cli_value != persisted.storages_history_in_rocksdb
{
return Err(RocksDbSettingsMismatchError {
flag_name: "--rocksdb.storages-history",
expected: persisted.storages_history_in_rocksdb,
got: cli_value,
});
}
if let Some(cli_value) = self.account_history &&
cli_value != persisted.account_history_in_rocksdb
{
return Err(RocksDbSettingsMismatchError {
flag_name: "--rocksdb.account-history",
expected: persisted.account_history_in_rocksdb,
got: cli_value,
});
}
Ok(())
}
}
/// Error returned when a CLI `RocksDB` flag differs from the persisted storage settings.
///
/// These settings are genesis-initialization-only and cannot be changed after the node
/// has been initialized.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RocksDbSettingsMismatchError {
/// The CLI flag name that mismatched.
pub flag_name: &'static str,
/// The expected value (from persisted settings).
pub expected: bool,
/// The value provided via CLI.
pub got: bool,
}
impl fmt::Display for RocksDbSettingsMismatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"`{}` differs from initialized layout (expected {}, got {}). \
This setting is genesis-only; re-sync required (remove datadir or use a new datadir).",
self.flag_name, self.expected, self.got
)
}
}
impl std::error::Error for RocksDbSettingsMismatchError {}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
struct CommandParser<T: Args> {
#[command(flatten)]
args: T,
}
#[test]
fn test_default_rocksdb_args() {
let args = CommandParser::<RocksDbArgs>::parse_from(["reth"]).args;
assert_eq!(args, RocksDbArgs::default());
}
#[test]
fn test_parse_all_flags() {
let args = CommandParser::<RocksDbArgs>::parse_from([
"reth",
"--rocksdb.tx-hash=true",
"--rocksdb.storages-history=false",
"--rocksdb.account-history=true",
])
.args;
assert_eq!(args.tx_hash, Some(true));
assert_eq!(args.storages_history, Some(false));
assert_eq!(args.account_history, Some(true));
}
#[test]
fn test_apply_to_settings_grouped_enable() {
let args =
RocksDbArgs { tx_hash: Some(true), storages_history: None, account_history: None };
let (result, grouped) = args.apply_to_settings(StorageSettings::legacy());
assert!(grouped, "should trigger grouped behavior when any flag is true");
assert!(result.transaction_hash_numbers_in_rocksdb);
assert!(result.storages_history_in_rocksdb, "grouped: all tables enabled");
assert!(result.account_history_in_rocksdb, "grouped: all tables enabled");
}
#[test]
fn test_apply_to_settings_explicit_disable() {
let args = RocksDbArgs {
tx_hash: Some(true),
storages_history: Some(false),
account_history: None,
};
let (result, grouped) = args.apply_to_settings(StorageSettings::legacy());
assert!(grouped);
assert!(result.transaction_hash_numbers_in_rocksdb);
assert!(!result.storages_history_in_rocksdb, "explicit false overrides grouped enable");
assert!(result.account_history_in_rocksdb);
}
#[test]
fn test_apply_to_settings_no_grouped_when_all_none() {
let args = RocksDbArgs::default();
let (result, grouped) = args.apply_to_settings(StorageSettings::legacy());
assert!(!grouped, "no grouped behavior when no flags set");
assert!(!result.transaction_hash_numbers_in_rocksdb);
assert!(!result.storages_history_in_rocksdb);
assert!(!result.account_history_in_rocksdb);
}
#[test]
fn test_apply_to_settings_all_explicit_false() {
let args = RocksDbArgs {
tx_hash: Some(false),
storages_history: Some(false),
account_history: Some(false),
};
let (result, grouped) = args.apply_to_settings(StorageSettings::legacy());
assert!(!grouped, "no grouped when all explicit false");
assert!(!result.transaction_hash_numbers_in_rocksdb);
assert!(!result.storages_history_in_rocksdb);
assert!(!result.account_history_in_rocksdb);
}
#[test]
fn test_apply_to_settings_preserves_non_rocksdb_fields() {
let base = StorageSettings::legacy().with_receipts_in_static_files(true);
let args =
RocksDbArgs { tx_hash: Some(true), storages_history: None, account_history: None };
let (result, _) = args.apply_to_settings(base);
assert!(result.receipts_in_static_files, "non-rocksdb settings preserved");
}
#[test]
fn test_validate_no_overrides_passes() {
let args = RocksDbArgs::default();
let persisted = StorageSettings::legacy().with_transaction_hash_numbers_in_rocksdb(true);
assert!(args.validate_against_persisted(&persisted).is_ok());
}
#[test]
fn test_validate_matching_overrides_passes() {
let args = RocksDbArgs {
tx_hash: Some(true),
storages_history: Some(false),
account_history: Some(false),
};
let persisted = StorageSettings::legacy().with_transaction_hash_numbers_in_rocksdb(true);
assert!(args.validate_against_persisted(&persisted).is_ok());
}
#[test]
fn test_validate_mismatched_tx_hash_fails() {
let args = RocksDbArgs { tx_hash: Some(true), ..Default::default() };
let persisted = StorageSettings::legacy();
let err = args.validate_against_persisted(&persisted).unwrap_err();
assert_eq!(err.flag_name, "--rocksdb.tx-hash");
assert!(!err.expected);
assert!(err.got);
assert!(err.to_string().contains("--rocksdb.tx-hash"));
assert!(err.to_string().contains("genesis-only"));
}
#[test]
fn test_validate_mismatched_storages_history_fails() {
let args = RocksDbArgs { storages_history: Some(false), ..Default::default() };
let persisted = StorageSettings::legacy().with_storages_history_in_rocksdb(true);
let err = args.validate_against_persisted(&persisted).unwrap_err();
assert_eq!(err.flag_name, "--rocksdb.storages-history");
assert!(err.expected);
assert!(!err.got);
}
#[test]
fn test_validate_mismatched_account_history_fails() {
let args = RocksDbArgs { account_history: Some(true), ..Default::default() };
let persisted = StorageSettings::legacy();
let err = args.validate_against_persisted(&persisted).unwrap_err();
assert_eq!(err.flag_name, "--rocksdb.account-history");
assert!(!err.expected);
assert!(err.got);
}
#[test]
fn test_error_message_format() {
let err = RocksDbSettingsMismatchError {
flag_name: "--rocksdb.tx-hash",
expected: false,
got: true,
};
let msg = err.to_string();
assert!(msg.contains("--rocksdb.tx-hash"));
assert!(msg.contains("expected false"));
assert!(msg.contains("got true"));
assert!(msg.contains("genesis-only"));
assert!(msg.contains("re-sync required"));
}
#[test]
fn test_validate_matching_false_overrides_passes() {
// Test that explicitly setting false matches persisted false
let args = RocksDbArgs {
tx_hash: Some(false),
storages_history: Some(false),
account_history: Some(false),
};
let persisted = StorageSettings::legacy(); // All rocksdb flags are false
assert!(args.validate_against_persisted(&persisted).is_ok());
}
#[test]
fn test_validate_multiple_mismatches_returns_first() {
// When multiple fields mismatch, the first one (tx_hash) is reported
let args = RocksDbArgs {
tx_hash: Some(true),
storages_history: Some(true),
account_history: Some(true),
};
let persisted = StorageSettings::legacy(); // All false
let err = args.validate_against_persisted(&persisted).unwrap_err();
// Should report the first mismatch (tx_hash is checked first)
assert_eq!(err.flag_name, "--rocksdb.tx-hash");
}
#[test]
fn test_validate_partial_override_with_match() {
// Only one CLI override set, and it matches
let args = RocksDbArgs { storages_history: Some(true), ..Default::default() };
let persisted = StorageSettings::legacy().with_storages_history_in_rocksdb(true);
assert!(args.validate_against_persisted(&persisted).is_ok());
}
}

View File

@@ -3,7 +3,7 @@
use crate::{
args::{
DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, NetworkArgs, PayloadBuilderArgs,
PruningArgs, RpcServerArgs, StaticFilesArgs, TxPoolArgs,
PruningArgs, RocksDbArgs, RpcServerArgs, StaticFilesArgs, TxPoolArgs,
},
dirs::{ChainPath, DataDirPath},
utils::get_single_header,
@@ -150,6 +150,9 @@ pub struct NodeConfig<ChainSpec> {
/// All static files related arguments
pub static_files: StaticFilesArgs,
/// All `RocksDB` table routing arguments
pub rocksdb: RocksDbArgs,
}
impl NodeConfig<ChainSpec> {
@@ -181,6 +184,7 @@ impl<ChainSpec> NodeConfig<ChainSpec> {
engine: EngineArgs::default(),
era: EraArgs::default(),
static_files: StaticFilesArgs::default(),
rocksdb: RocksDbArgs::default(),
}
}
@@ -255,6 +259,7 @@ impl<ChainSpec> NodeConfig<ChainSpec> {
engine,
era,
static_files,
rocksdb,
..
} = self;
NodeConfig {
@@ -274,6 +279,7 @@ impl<ChainSpec> NodeConfig<ChainSpec> {
engine,
era,
static_files,
rocksdb,
}
}
@@ -544,6 +550,7 @@ impl<ChainSpec> NodeConfig<ChainSpec> {
engine: self.engine,
era: self.era,
static_files: self.static_files,
rocksdb: self.rocksdb,
}
}
@@ -585,6 +592,7 @@ impl<ChainSpec> Clone for NodeConfig<ChainSpec> {
engine: self.engine.clone(),
era: self.era.clone(),
static_files: self.static_files,
rocksdb: self.rocksdb,
}
}
}

View File

@@ -75,6 +75,9 @@ pub enum InitStorageError {
/// State root doesn't match the expected one.
#[error("state root mismatch: {_0}")]
StateRootMismatch(GotExpected<B256>),
/// `RocksDB` CLI flag differs from persisted storage settings.
#[error("{_0}")]
RocksDbSettingsMismatch(String),
}
impl From<DatabaseError> for InitStorageError {

View File

@@ -897,6 +897,28 @@ Pruning:
--prune.bodies.before <BLOCK_NUMBER>
Prune storage history before the specified block number. The specified block number is not pruned
RocksDB:
--rocksdb.tx-hash <TX_HASH>
Route tx hash -> number table to `RocksDB` instead of MDBX.
Note: genesis-initialization-only, changing later requires re-sync.
[possible values: true, false]
--rocksdb.storages-history <STORAGES_HISTORY>
Route storages history tables to `RocksDB` instead of MDBX.
Note: genesis-initialization-only, changing later requires re-sync.
[possible values: true, false]
--rocksdb.account-history <ACCOUNT_HISTORY>
Route account history tables to `RocksDB` instead of MDBX.
Note: genesis-initialization-only, changing later requires re-sync.
[possible values: true, false]
Engine:
--engine.persistence-threshold <PERSISTENCE_THRESHOLD>
Configure persistence threshold for the engine. This determines how many canonical blocks must be in-memory, ahead of the last persisted block, before flushing canonical blocks to disk again.

View File

@@ -897,6 +897,28 @@ Pruning:
--prune.bodies.before <BLOCK_NUMBER>
Prune storage history before the specified block number. The specified block number is not pruned
RocksDB:
--rocksdb.tx-hash <TX_HASH>
Route tx hash -> number table to `RocksDB` instead of MDBX.
Note: genesis-initialization-only, changing later requires re-sync.
[possible values: true, false]
--rocksdb.storages-history <STORAGES_HISTORY>
Route storages history tables to `RocksDB` instead of MDBX.
Note: genesis-initialization-only, changing later requires re-sync.
[possible values: true, false]
--rocksdb.account-history <ACCOUNT_HISTORY>
Route account history tables to `RocksDB` instead of MDBX.
Note: genesis-initialization-only, changing later requires re-sync.
[possible values: true, false]
Engine:
--engine.persistence-threshold <PERSISTENCE_THRESHOLD>
Configure persistence threshold for the engine. This determines how many canonical blocks must be in-memory, ahead of the last persisted block, before flushing canonical blocks to disk again.