diff --git a/Cargo.lock b/Cargo.lock index 5f7b1a77977f..ab9e1c42db9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7673,6 +7673,7 @@ dependencies = [ "shellexpand", "strum", "tempfile", + "thiserror", "tokio", "toml", "tracing", diff --git a/book/cli/reth/node.md b/book/cli/reth/node.md index 773ec4af91ec..98f677c158e7 100644 --- a/book/cli/reth/node.md +++ b/book/cli/reth/node.md @@ -580,7 +580,60 @@ Dev testnet: Pruning: --full - Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored. This flag takes priority over pruning configuration in reth.toml + Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored + + --block-interval + Minimum pruning interval measured in blocks + + [default: 0] + + --prune.senderrecovery.full + Prunes all sender recovery data + + --prune.senderrecovery.distance + Prune sender recovery data before the `head-N` block number. In other words, keep last N + 1 blocks + + --prune.senderrecovery.before + Prune sender recovery data before the specified block number. The specified block number is not pruned + + --prune.transactionlookup.full + Prunes all transaction lookup data + + --prune.transactionlookup.distance + Prune transaction lookup data before the `head-N` block number. In other words, keep last N + 1 blocks + + --prune.transactionlookup.before + Prune transaction lookup data before the specified block number. The specified block number is not pruned + + --prune.receipts.full + Prunes all receipt data + + --prune.receipts.distance + Prune receipts before the `head-N` block number. In other words, keep last N + 1 blocks + + --prune.receipts.before + Prune receipts before the specified block number. The specified block number is not pruned + + --prune.accounthistory.full + Prunes all account history + + --prune.accounthistory.distance + Prune account before the `head-N` block number. In other words, keep last N + 1 blocks + + --prune.accounthistory.before + Prune account history before the specified block number. The specified block number is not pruned + + --prune.storagehistory.full + Prunes all storage history data + + --prune.storagehistory.distance + Prune storage history before the `head-N` block number. In other words, keep last N + 1 blocks + + --prune.storagehistory.before + Prune storage history before the specified block number. The specified block number is not pruned + + --prune.receiptslogfilter + Configure receipts log filter. Format: <`address`>:<`prune_mode`>[,<`address`>:<`prune_mode`>...] Where <`prune_mode`> can be 'full', 'distance:<`blocks`>', or 'before:<`block_number`>' Engine: --engine.experimental diff --git a/crates/node/builder/src/launch/common.rs b/crates/node/builder/src/launch/common.rs index d93d3bb0a7c8..8e1d065389c3 100644 --- a/crates/node/builder/src/launch/common.rs +++ b/crates/node/builder/src/launch/common.rs @@ -979,8 +979,29 @@ mod tests { fn test_save_prune_config() { with_tempdir("prune-store-test", |config_path| { let mut reth_config = Config::default(); - let node_config = - NodeConfig { pruning: PruningArgs { full: true }, ..NodeConfig::test() }; + let node_config = NodeConfig { + pruning: PruningArgs { + full: true, + block_interval: 0, + sender_recovery_full: false, + sender_recovery_distance: None, + sender_recovery_before: None, + transaction_lookup_full: false, + transaction_lookup_distance: None, + transaction_lookup_before: None, + receipts_full: false, + receipts_distance: None, + receipts_before: None, + account_history_full: false, + account_history_distance: None, + account_history_before: None, + storage_history_full: false, + storage_history_distance: None, + storage_history_before: None, + receipts_log_filter: vec![], + }, + ..NodeConfig::test() + }; LaunchContext::save_pruning_config_if_full_node( &mut reth_config, &node_config, diff --git a/crates/node/core/Cargo.toml b/crates/node/core/Cargo.toml index 8381a668f706..0d507853d770 100644 --- a/crates/node/core/Cargo.toml +++ b/crates/node/core/Cargo.toml @@ -57,6 +57,7 @@ derive_more.workspace = true toml.workspace = true serde.workspace = true strum = { workspace = true, features = ["derive"] } +thiserror.workspace = true # io dirs-next = "2.0.0" diff --git a/crates/node/core/src/args/error.rs b/crates/node/core/src/args/error.rs new file mode 100644 index 000000000000..7119501ac93a --- /dev/null +++ b/crates/node/core/src/args/error.rs @@ -0,0 +1,22 @@ +use std::num::ParseIntError; + +/// Error while parsing a `ReceiptsLogPruneConfig` +#[derive(thiserror::Error, Debug)] +#[allow(clippy::enum_variant_names)] +pub(crate) enum ReceiptsLogError { + /// The format of the filter is invalid. + #[error("invalid filter format: {0}")] + InvalidFilterFormat(String), + /// Address is invalid. + #[error("address is invalid: {0}")] + InvalidAddress(String), + /// The prune mode is not one of full, distance, before. + #[error("prune mode is invalid: {0}")] + InvalidPruneMode(String), + /// The distance value supplied is invalid. + #[error("distance is invalid: {0}")] + InvalidDistance(ParseIntError), + /// The block number supplied is invalid. + #[error("block number is invalid: {0}")] + InvalidBlockNumber(ParseIntError), +} diff --git a/crates/node/core/src/args/mod.rs b/crates/node/core/src/args/mod.rs index 6775c68c3522..0218abddb08c 100644 --- a/crates/node/core/src/args/mod.rs +++ b/crates/node/core/src/args/mod.rs @@ -58,4 +58,5 @@ pub use benchmark_args::BenchmarkArgs; pub mod utils; +mod error; pub mod types; diff --git a/crates/node/core/src/args/pruning.rs b/crates/node/core/src/args/pruning.rs index e4d4808f6ac6..acef58a09bc9 100644 --- a/crates/node/core/src/args/pruning.rs +++ b/crates/node/core/src/args/pruning.rs @@ -1,51 +1,250 @@ //! Pruning and full node arguments +use crate::args::error::ReceiptsLogError; use clap::Args; use reth_chainspec::ChainSpec; use reth_config::config::PruneConfig; +use reth_primitives::{Address, BlockNumber}; use reth_prune_types::{PruneMode, PruneModes, ReceiptsLogPruneConfig, MINIMUM_PRUNING_DISTANCE}; +use std::collections::BTreeMap; /// Parameters for pruning and full node #[derive(Debug, Clone, Args, PartialEq, Eq, Default)] #[command(next_help_heading = "Pruning")] pub struct PruningArgs { /// Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored. - /// This flag takes priority over pruning configuration in reth.toml. #[arg(long, default_value_t = false)] pub full: bool, + + /// Minimum pruning interval measured in blocks. + #[arg(long, default_value_t = 0)] + pub block_interval: u64, + + // Sender Recovery + /// Prunes all sender recovery data. + #[arg(long = "prune.senderrecovery.full", conflicts_with_all = &["sender_recovery_distance", "sender_recovery_before"])] + pub sender_recovery_full: bool, + /// Prune sender recovery data before the `head-N` block number. In other words, keep last N + + /// 1 blocks. + #[arg(long = "prune.senderrecovery.distance", value_name = "BLOCKS", conflicts_with_all = &["sender_recovery_full", "sender_recovery_before"])] + pub sender_recovery_distance: Option, + /// Prune sender recovery data before the specified block number. The specified block number is + /// not pruned. + #[arg(long = "prune.senderrecovery.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["sender_recovery_full", "sender_recovery_distance"])] + pub sender_recovery_before: Option, + + // Transaction Lookup + /// Prunes all transaction lookup data. + #[arg(long = "prune.transactionlookup.full", conflicts_with_all = &["transaction_lookup_distance", "transaction_lookup_before"])] + pub transaction_lookup_full: bool, + /// Prune transaction lookup data before the `head-N` block number. In other words, keep last N + /// + 1 blocks. + #[arg(long = "prune.transactionlookup.distance", value_name = "BLOCKS", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_before"])] + pub transaction_lookup_distance: Option, + /// Prune transaction lookup data before the specified block number. The specified block number + /// is not pruned. + #[arg(long = "prune.transactionlookup.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_distance"])] + pub transaction_lookup_before: Option, + + // Receipts + /// Prunes all receipt data. + #[arg(long = "prune.receipts.full", conflicts_with_all = &["receipts_distance", "receipts_before"])] + pub receipts_full: bool, + /// Prune receipts before the `head-N` block number. In other words, keep last N + 1 blocks. + #[arg(long = "prune.receipts.distance", value_name = "BLOCKS", conflicts_with_all = &["receipts_full", "receipts_before"])] + pub receipts_distance: Option, + /// Prune receipts before the specified block number. The specified block number is not pruned. + #[arg(long = "prune.receipts.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["receipts_full", "receipts_distance"])] + pub receipts_before: Option, + + // Account History + /// Prunes all account history. + #[arg(long = "prune.accounthistory.full", conflicts_with_all = &["account_history_distance", "account_history_before"])] + pub account_history_full: bool, + /// Prune account before the `head-N` block number. In other words, keep last N + 1 blocks. + #[arg(long = "prune.accounthistory.distance", value_name = "BLOCKS", conflicts_with_all = &["account_history_full", "account_history_before"])] + pub account_history_distance: Option, + /// Prune account history before the specified block number. The specified block number is not + /// pruned. + #[arg(long = "prune.accounthistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["account_history_full", "account_history_distance"])] + pub account_history_before: Option, + + // Storage History + /// Prunes all storage history data. + #[arg(long = "prune.storagehistory.full", conflicts_with_all = &["storage_history_distance", "storage_history_before"])] + pub storage_history_full: bool, + /// Prune storage history before the `head-N` block number. In other words, keep last N + 1 + /// blocks. + #[arg(long = "prune.storagehistory.distance", value_name = "BLOCKS", conflicts_with_all = &["storage_history_full", "storage_history_before"])] + pub storage_history_distance: Option, + /// Prune storage history before the specified block number. The specified block number is not + /// pruned. + #[arg(long = "prune.storagehistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["storage_history_full", "storage_history_distance"])] + pub storage_history_before: Option, + + // Receipts Log Filter + /// Configure receipts log filter. Format: + /// <`address`>:<`prune_mode`>[,<`address`>:<`prune_mode`>...] Where <`prune_mode`> can be + /// 'full', 'distance:<`blocks`>', or 'before:<`block_number`>' + #[arg(long = "prune.receiptslogfilter", value_name = "FILTER_CONFIG", value_delimiter = ',', value_parser = parse_receipts_log_filter)] + pub receipts_log_filter: Vec, } impl PruningArgs { /// Returns pruning configuration. pub fn prune_config(&self, chain_spec: &ChainSpec) -> Option { - if !self.full { - return None - } + // Initialise with a default prune configuration. + let mut config = PruneConfig::default(); - Some(PruneConfig { - block_interval: 5, - segments: PruneModes { - sender_recovery: Some(PruneMode::Full), - transaction_lookup: None, - // prune all receipts if chain doesn't have deposit contract specified in chain spec - receipts: chain_spec - .deposit_contract - .as_ref() - .map(|contract| PruneMode::Before(contract.block)) - .or(Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE))), - account_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), - storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), - receipts_log_filter: ReceiptsLogPruneConfig( - chain_spec + // If --full is set, use full node defaults. + if self.full { + config = PruneConfig { + block_interval: 5, + segments: PruneModes { + sender_recovery: Some(PruneMode::Full), + transaction_lookup: None, + // prune all receipts if chain doesn't have deposit contract specified in chain + // spec + receipts: chain_spec .deposit_contract .as_ref() - .map(|contract| (contract.address, PruneMode::Before(contract.block))) - .into_iter() - .collect(), - ), - }, - }) + .map(|contract| PruneMode::Before(contract.block)) + .or(Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE))), + account_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), + storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), + receipts_log_filter: ReceiptsLogPruneConfig( + chain_spec + .deposit_contract + .as_ref() + .map(|contract| (contract.address, PruneMode::Before(contract.block))) + .into_iter() + .collect(), + ), + }, + } + } + + // Override with any explicitly set prune.* flags. + if let Some(mode) = self.sender_recovery_prune_mode() { + config.segments.sender_recovery = Some(mode); + } + if let Some(mode) = self.transaction_lookup_prune_mode() { + config.segments.transaction_lookup = Some(mode); + } + if let Some(mode) = self.receipts_prune_mode() { + config.segments.receipts = Some(mode); + } + if let Some(mode) = self.account_history_prune_mode() { + config.segments.account_history = Some(mode); + } + if let Some(mode) = self.storage_history_prune_mode() { + config.segments.storage_history = Some(mode); + } + + Some(config) + } + const fn sender_recovery_prune_mode(&self) -> Option { + if self.sender_recovery_full { + Some(PruneMode::Full) + } else if let Some(distance) = self.sender_recovery_distance { + Some(PruneMode::Distance(distance)) + } else if let Some(block_number) = self.sender_recovery_before { + Some(PruneMode::Before(block_number)) + } else { + None + } + } + + const fn transaction_lookup_prune_mode(&self) -> Option { + if self.transaction_lookup_full { + Some(PruneMode::Full) + } else if let Some(distance) = self.transaction_lookup_distance { + Some(PruneMode::Distance(distance)) + } else if let Some(block_number) = self.transaction_lookup_before { + Some(PruneMode::Before(block_number)) + } else { + None + } + } + + const fn receipts_prune_mode(&self) -> Option { + if self.receipts_full { + Some(PruneMode::Full) + } else if let Some(distance) = self.receipts_distance { + Some(PruneMode::Distance(distance)) + } else if let Some(block_number) = self.receipts_before { + Some(PruneMode::Before(block_number)) + } else { + None + } + } + + const fn account_history_prune_mode(&self) -> Option { + if self.account_history_full { + Some(PruneMode::Full) + } else if let Some(distance) = self.account_history_distance { + Some(PruneMode::Distance(distance)) + } else if let Some(block_number) = self.account_history_before { + Some(PruneMode::Before(block_number)) + } else { + None + } + } + + const fn storage_history_prune_mode(&self) -> Option { + if self.storage_history_full { + Some(PruneMode::Full) + } else if let Some(distance) = self.storage_history_distance { + Some(PruneMode::Distance(distance)) + } else if let Some(block_number) = self.storage_history_before { + Some(PruneMode::Before(block_number)) + } else { + None + } + } +} + +pub(crate) fn parse_receipts_log_filter( + value: &str, +) -> Result { + let mut config = BTreeMap::new(); + // Split out each of the filters. + let filters = value.split(','); + for filter in filters { + let parts: Vec<&str> = filter.split(':').collect(); + if parts.len() < 2 { + return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string())); + } + // Parse the address + let address = parts[0] + .parse::
() + .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?; + + // Parse the prune mode + let prune_mode = match parts[1] { + "full" => PruneMode::Full, + s if s.starts_with("distance") => { + if parts.len() < 3 { + return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string())); + } + let distance = + parts[2].parse::().map_err(ReceiptsLogError::InvalidDistance)?; + PruneMode::Distance(distance) + } + s if s.starts_with("before") => { + if parts.len() < 3 { + return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string())); + } + let block_number = parts[2] + .parse::() + .map_err(ReceiptsLogError::InvalidBlockNumber)?; + PruneMode::Before(block_number) + } + _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())), + }; + config.insert(address, prune_mode); } + Ok(ReceiptsLogPruneConfig(config)) } #[cfg(test)] @@ -66,4 +265,62 @@ mod tests { let args = CommandParser::::parse_from(["reth"]).args; assert_eq!(args, default_args); } + + #[test] + fn test_parse_receipts_log_filter() { + let filter1 = "0x0000000000000000000000000000000000000001:full"; + let filter2 = "0x0000000000000000000000000000000000000002:distance:1000"; + let filter3 = "0x0000000000000000000000000000000000000003:before:5000000"; + let filters = [filter1, filter2, filter3].join(","); + + // Args can be parsed. + let result = parse_receipts_log_filter(&filters); + assert!(result.is_ok()); + let config = result.unwrap(); + assert_eq!(config.0.len(), 3); + + // Check that the args were parsed correctly. + let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap(); + let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap(); + let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap(); + + assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full)); + assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000))); + assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_filter_format() { + let result = parse_receipts_log_filter("invalid_format"); + assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_)))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_address() { + let result = parse_receipts_log_filter("invalid_address:full"); + assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_)))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_prune_mode() { + let result = + parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode"); + assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_)))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_distance() { + let result = parse_receipts_log_filter( + "0x0000000000000000000000000000000000000000:distance:invalid_distance", + ); + assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_)))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_block_number() { + let result = parse_receipts_log_filter( + "0x0000000000000000000000000000000000000000:before:invalid_block", + ); + assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_)))); + } } diff --git a/crates/node/core/src/node_config.rs b/crates/node/core/src/node_config.rs index 7a2856d07d38..b99c13dc6c4a 100644 --- a/crates/node/core/src/node_config.rs +++ b/crates/node/core/src/node_config.rs @@ -229,7 +229,7 @@ impl NodeConfig { } /// Set the pruning args for the node - pub const fn with_pruning(mut self, pruning: PruningArgs) -> Self { + pub fn with_pruning(mut self, pruning: PruningArgs) -> Self { self.pruning = pruning; self }