Skip to main content

reth_node_core/args/
pruning.rs

1//! Pruning and full node arguments
2
3use crate::{args::error::ReceiptsLogError, primitives::EthereumHardfork};
4use alloy_primitives::{Address, BlockNumber};
5use clap::{builder::RangedU64ValueParser, Args};
6use reth_chainspec::EthereumHardforks;
7use reth_config::config::PruneConfig;
8use reth_prune_types::{
9    PruneMode, PruneModes, ReceiptsLogPruneConfig, MINIMUM_DISTANCE, MINIMUM_UNWIND_SAFE_DISTANCE,
10};
11use std::{collections::BTreeMap, ops::Not, sync::OnceLock};
12
13/// Global static pruning defaults
14static PRUNING_DEFAULTS: OnceLock<DefaultPruningValues> = OnceLock::new();
15
16/// Default values for `--full` and `--minimal` pruning modes that can be customized.
17///
18/// Global defaults can be set via [`DefaultPruningValues::try_init`].
19#[derive(Debug, Clone)]
20pub struct DefaultPruningValues {
21    /// Prune modes for `--full` flag.
22    ///
23    /// Note: `bodies_history` is ignored when `full_bodies_history_use_pre_merge` is `true`.
24    pub full_prune_modes: PruneModes,
25    /// If `true`, `--full` will set `bodies_history` to prune everything before the merge block
26    /// (Paris hardfork). If `false`, uses `full_prune_modes.bodies_history` directly.
27    pub full_bodies_history_use_pre_merge: bool,
28    /// Prune modes for `--minimal` flag.
29    pub minimal_prune_modes: PruneModes,
30}
31
32impl DefaultPruningValues {
33    /// Initialize the global pruning defaults with this configuration.
34    ///
35    /// Returns `Err(self)` if already initialized.
36    pub fn try_init(self) -> Result<(), Self> {
37        PRUNING_DEFAULTS.set(self)
38    }
39
40    /// Get a reference to the global pruning defaults.
41    pub fn get_global() -> &'static Self {
42        PRUNING_DEFAULTS.get_or_init(Self::default)
43    }
44
45    /// Set the prune modes for `--full` flag.
46    pub fn with_full_prune_modes(mut self, modes: PruneModes) -> Self {
47        self.full_prune_modes = modes;
48        self
49    }
50
51    /// Set whether `--full` should use pre-merge pruning for bodies history.
52    ///
53    /// When `true` (default), bodies are pruned before the Paris hardfork block.
54    /// When `false`, uses `full_prune_modes.bodies_history` directly.
55    pub const fn with_full_bodies_history_use_pre_merge(mut self, use_pre_merge: bool) -> Self {
56        self.full_bodies_history_use_pre_merge = use_pre_merge;
57        self
58    }
59
60    /// Set the prune modes for `--minimal` flag.
61    pub fn with_minimal_prune_modes(mut self, modes: PruneModes) -> Self {
62        self.minimal_prune_modes = modes;
63        self
64    }
65}
66
67impl Default for DefaultPruningValues {
68    fn default() -> Self {
69        Self {
70            full_prune_modes: PruneModes {
71                sender_recovery: Some(PruneMode::Full),
72                transaction_lookup: None,
73                receipts: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
74                account_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
75                storage_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
76                // This field is ignored when full_bodies_history_use_pre_merge is true
77                bodies_history: None,
78                receipts_log_filter: Default::default(),
79            },
80            full_bodies_history_use_pre_merge: true,
81            minimal_prune_modes: PruneModes {
82                sender_recovery: Some(PruneMode::Full),
83                transaction_lookup: Some(PruneMode::Full),
84                receipts: Some(PruneMode::Distance(MINIMUM_DISTANCE)),
85                account_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
86                storage_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
87                bodies_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
88                receipts_log_filter: Default::default(),
89            },
90        }
91    }
92}
93
94/// Parameters for pruning and full node
95#[derive(Debug, Clone, Args, PartialEq, Eq, Default)]
96#[command(next_help_heading = "Pruning")]
97pub struct PruningArgs {
98    /// Run full node. Only the most recent [`MINIMUM_UNWIND_SAFE_DISTANCE`] block states are
99    /// stored.
100    #[arg(long, default_value_t = false, conflicts_with = "minimal")]
101    pub full: bool,
102
103    /// Run minimal storage mode with maximum pruning and smaller static files.
104    ///
105    /// This mode configures the node to use minimal disk space by:
106    /// - Fully pruning sender recovery, transaction lookup, receipts
107    /// - Leaving 10,064 blocks for account, storage history and block bodies
108    /// - Using 10,000 blocks per static file segment
109    #[arg(long, default_value_t = false, conflicts_with = "full")]
110    pub minimal: bool,
111
112    /// Minimum pruning interval measured in blocks.
113    #[arg(long = "prune.block-interval", alias = "block-interval", value_parser = RangedU64ValueParser::<u64>::new().range(1..))]
114    pub block_interval: Option<u64>,
115
116    // Sender Recovery
117    /// Prunes all sender recovery data.
118    #[arg(long = "prune.sender-recovery.full", alias = "prune.senderrecovery.full", conflicts_with_all = &["sender_recovery_distance", "sender_recovery_before"])]
119    pub sender_recovery_full: bool,
120    /// Prune sender recovery data before the `head-N` block number. In other words, keep last N +
121    /// 1 blocks.
122    #[arg(long = "prune.sender-recovery.distance", alias = "prune.senderrecovery.distance", value_name = "BLOCKS", conflicts_with_all = &["sender_recovery_full", "sender_recovery_before"])]
123    pub sender_recovery_distance: Option<u64>,
124    /// Prune sender recovery data before the specified block number. The specified block number is
125    /// not pruned.
126    #[arg(long = "prune.sender-recovery.before", alias = "prune.senderrecovery.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["sender_recovery_full", "sender_recovery_distance"])]
127    pub sender_recovery_before: Option<BlockNumber>,
128
129    // Transaction Lookup
130    /// Prunes all transaction lookup data.
131    #[arg(long = "prune.transaction-lookup.full", alias = "prune.transactionlookup.full", conflicts_with_all = &["transaction_lookup_distance", "transaction_lookup_before"])]
132    pub transaction_lookup_full: bool,
133    /// Prune transaction lookup data before the `head-N` block number. In other words, keep last N
134    /// + 1 blocks.
135    #[arg(long = "prune.transaction-lookup.distance", alias = "prune.transactionlookup.distance", value_name = "BLOCKS", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_before"])]
136    pub transaction_lookup_distance: Option<u64>,
137    /// Prune transaction lookup data before the specified block number. The specified block number
138    /// is not pruned.
139    #[arg(long = "prune.transaction-lookup.before", alias = "prune.transactionlookup.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_distance"])]
140    pub transaction_lookup_before: Option<BlockNumber>,
141
142    // Receipts
143    /// Prunes all receipt data.
144    #[arg(long = "prune.receipts.full", conflicts_with_all = &["receipts_pre_merge", "receipts_distance", "receipts_before"])]
145    pub receipts_full: bool,
146    /// Prune receipts before the merge block.
147    #[arg(long = "prune.receipts.pre-merge", conflicts_with_all = &["receipts_full", "receipts_distance", "receipts_before"])]
148    pub receipts_pre_merge: bool,
149    /// Prune receipts before the `head-N` block number. In other words, keep last N + 1 blocks.
150    #[arg(long = "prune.receipts.distance", value_name = "BLOCKS", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_before"])]
151    pub receipts_distance: Option<u64>,
152    /// Prune receipts before the specified block number. The specified block number is not pruned.
153    #[arg(long = "prune.receipts.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_distance"])]
154    pub receipts_before: Option<BlockNumber>,
155    // Receipts Log Filter
156    /// Configure receipts log filter. Format:
157    /// <`address`>:<`prune_mode`>... where <`prune_mode`> can be 'full', 'distance:<`blocks`>', or
158    /// 'before:<`block_number`>'
159    #[arg(long = "prune.receiptslogfilter", value_name = "FILTER_CONFIG", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_distance",  "receipts_before"], value_parser = parse_receipts_log_filter)]
160    pub receipts_log_filter: Option<ReceiptsLogPruneConfig>,
161
162    // Account History
163    /// Prunes all account history.
164    #[arg(long = "prune.account-history.full", alias = "prune.accounthistory.full", conflicts_with_all = &["account_history_distance", "account_history_before"])]
165    pub account_history_full: bool,
166    /// Prune account before the `head-N` block number. In other words, keep last N + 1 blocks.
167    #[arg(long = "prune.account-history.distance", alias = "prune.accounthistory.distance", value_name = "BLOCKS", conflicts_with_all = &["account_history_full", "account_history_before"])]
168    pub account_history_distance: Option<u64>,
169    /// Prune account history before the specified block number. The specified block number is not
170    /// pruned.
171    #[arg(long = "prune.account-history.before", alias = "prune.accounthistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["account_history_full", "account_history_distance"])]
172    pub account_history_before: Option<BlockNumber>,
173
174    // Storage History
175    /// Prunes all storage history data.
176    #[arg(long = "prune.storage-history.full", alias = "prune.storagehistory.full", conflicts_with_all = &["storage_history_distance", "storage_history_before"])]
177    pub storage_history_full: bool,
178    /// Prune storage history before the `head-N` block number. In other words, keep last N + 1
179    /// blocks.
180    #[arg(long = "prune.storage-history.distance", alias = "prune.storagehistory.distance", value_name = "BLOCKS", conflicts_with_all = &["storage_history_full", "storage_history_before"])]
181    pub storage_history_distance: Option<u64>,
182    /// Prune storage history before the specified block number. The specified block number is not
183    /// pruned.
184    #[arg(long = "prune.storage-history.before", alias = "prune.storagehistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["storage_history_full", "storage_history_distance"])]
185    pub storage_history_before: Option<BlockNumber>,
186
187    // Bodies
188    /// Prune bodies before the merge block.
189    #[arg(long = "prune.bodies.pre-merge", value_name = "BLOCKS", conflicts_with_all = &["bodies_distance", "bodies_before"])]
190    pub bodies_pre_merge: bool,
191    /// Prune bodies before the `head-N` block number. In other words, keep last N + 1
192    /// blocks.
193    #[arg(long = "prune.bodies.distance", value_name = "BLOCKS", conflicts_with_all = &["bodies_pre_merge", "bodies_before"])]
194    pub bodies_distance: Option<u64>,
195    /// Prune storage history before the specified block number. The specified block number is not
196    /// pruned.
197    #[arg(long = "prune.bodies.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["bodies_distance", "bodies_pre_merge"])]
198    pub bodies_before: Option<BlockNumber>,
199
200    /// Minimum pruning distance from the tip. This controls the safety margin for reorgs and
201    /// manual unwinds.
202    #[arg(long = "prune.minimum-distance", value_name = "BLOCKS")]
203    pub minimum_distance: Option<u64>,
204}
205
206impl PruningArgs {
207    /// Returns pruning configuration.
208    ///
209    /// Returns [`None`] if no parameters are specified and default pruning configuration should be
210    /// used.
211    pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
212    where
213        ChainSpec: EthereumHardforks,
214    {
215        // Initialize with a default prune configuration.
216        let mut config = PruneConfig::default();
217
218        // If --full is set, use full node defaults.
219        if self.full {
220            let defaults = DefaultPruningValues::get_global();
221            let mut segments = defaults.full_prune_modes.clone();
222            if defaults.full_bodies_history_use_pre_merge {
223                segments.bodies_history = chain_spec
224                    .ethereum_fork_activation(EthereumHardfork::Paris)
225                    .block_number()
226                    .map(PruneMode::Before);
227            }
228            config = PruneConfig {
229                block_interval: config.block_interval,
230                segments,
231                minimum_pruning_distance: config.minimum_pruning_distance,
232            }
233        }
234
235        // If --minimal is set, use minimal storage mode with aggressive pruning.
236        if self.minimal {
237            config = PruneConfig {
238                block_interval: config.block_interval,
239                segments: DefaultPruningValues::get_global().minimal_prune_modes.clone(),
240                minimum_pruning_distance: config.minimum_pruning_distance,
241            }
242        }
243
244        // Override with any explicitly set prune.* flags.
245        if let Some(block_interval) = self.block_interval {
246            config.block_interval = block_interval as usize;
247        }
248        if let Some(distance) = self.minimum_distance {
249            config.minimum_pruning_distance = distance;
250        }
251        if let Some(mode) = self.sender_recovery_prune_mode() {
252            config.segments.sender_recovery = Some(mode);
253        }
254        if let Some(mode) = self.transaction_lookup_prune_mode() {
255            config.segments.transaction_lookup = Some(mode);
256        }
257        if let Some(mode) = self.receipts_prune_mode(chain_spec) {
258            config.segments.receipts = Some(mode);
259        }
260        if let Some(mode) = self.account_history_prune_mode() {
261            config.segments.account_history = Some(mode);
262        }
263        if let Some(mode) = self.bodies_prune_mode(chain_spec) {
264            config.segments.bodies_history = Some(mode);
265        }
266        if let Some(mode) = self.storage_history_prune_mode() {
267            config.segments.storage_history = Some(mode);
268        }
269        if let Some(receipt_logs) =
270            self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned()
271        {
272            config.segments.receipts_log_filter = receipt_logs;
273            // need to remove the receipts segment filter entirely because that takes precedence
274            // over the logs filter
275            config.segments.receipts.take();
276        }
277
278        config.is_default().not().then_some(config)
279    }
280
281    fn bodies_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
282    where
283        ChainSpec: EthereumHardforks,
284    {
285        if self.bodies_pre_merge {
286            chain_spec
287                .ethereum_fork_activation(EthereumHardfork::Paris)
288                .block_number()
289                .map(PruneMode::Before)
290        } else if let Some(distance) = self.bodies_distance {
291            Some(PruneMode::Distance(distance))
292        } else {
293            self.bodies_before.map(PruneMode::Before)
294        }
295    }
296
297    const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
298        if self.sender_recovery_full {
299            Some(PruneMode::Full)
300        } else if let Some(distance) = self.sender_recovery_distance {
301            Some(PruneMode::Distance(distance))
302        } else if let Some(block_number) = self.sender_recovery_before {
303            Some(PruneMode::Before(block_number))
304        } else {
305            None
306        }
307    }
308
309    const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
310        if self.transaction_lookup_full {
311            Some(PruneMode::Full)
312        } else if let Some(distance) = self.transaction_lookup_distance {
313            Some(PruneMode::Distance(distance))
314        } else if let Some(block_number) = self.transaction_lookup_before {
315            Some(PruneMode::Before(block_number))
316        } else {
317            None
318        }
319    }
320
321    fn receipts_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
322    where
323        ChainSpec: EthereumHardforks,
324    {
325        if self.receipts_pre_merge {
326            chain_spec
327                .ethereum_fork_activation(EthereumHardfork::Paris)
328                .block_number()
329                .map(PruneMode::Before)
330        } else if self.receipts_full {
331            Some(PruneMode::Full)
332        } else if let Some(distance) = self.receipts_distance {
333            Some(PruneMode::Distance(distance))
334        } else {
335            self.receipts_before.map(PruneMode::Before)
336        }
337    }
338
339    const fn account_history_prune_mode(&self) -> Option<PruneMode> {
340        if self.account_history_full {
341            Some(PruneMode::Full)
342        } else if let Some(distance) = self.account_history_distance {
343            Some(PruneMode::Distance(distance))
344        } else if let Some(block_number) = self.account_history_before {
345            Some(PruneMode::Before(block_number))
346        } else {
347            None
348        }
349    }
350
351    const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
352        if self.storage_history_full {
353            Some(PruneMode::Full)
354        } else if let Some(distance) = self.storage_history_distance {
355            Some(PruneMode::Distance(distance))
356        } else if let Some(block_number) = self.storage_history_before {
357            Some(PruneMode::Before(block_number))
358        } else {
359            None
360        }
361    }
362}
363
364/// Parses `,` separated pruning info into [`ReceiptsLogPruneConfig`].
365pub(crate) fn parse_receipts_log_filter(
366    value: &str,
367) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
368    let mut config = BTreeMap::new();
369    // Split out each of the filters.
370    let filters = value.split(',').map(str::trim);
371    for filter in filters {
372        let parts: Vec<&str> = filter.split(':').collect();
373        if parts.len() < 2 {
374            return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
375        }
376        // Parse the address
377        let address = parts[0]
378            .parse::<Address>()
379            .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
380
381        // Parse the prune mode
382        let prune_mode = match parts[1] {
383            "full" => PruneMode::Full,
384            s if s.starts_with("distance") => {
385                if parts.len() < 3 {
386                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
387                }
388                let distance =
389                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
390                PruneMode::Distance(distance)
391            }
392            s if s.starts_with("before") => {
393                if parts.len() < 3 {
394                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
395                }
396                let block_number =
397                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidBlockNumber)?;
398                PruneMode::Before(block_number)
399            }
400            _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
401        };
402        config.insert(address, prune_mode);
403    }
404    Ok(ReceiptsLogPruneConfig(config))
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use alloy_primitives::address;
411    use clap::Parser;
412
413    /// A helper type to parse Args more easily
414    #[derive(Parser)]
415    struct CommandParser<T: Args> {
416        #[command(flatten)]
417        args: T,
418    }
419
420    #[test]
421    fn pruning_args_sanity_check() {
422        let args = CommandParser::<PruningArgs>::parse_from([
423            "reth",
424            "--prune.receiptslogfilter",
425            "0x0000000000000000000000000000000000000003:before:5000000",
426        ])
427        .args;
428        let mut config = ReceiptsLogPruneConfig::default();
429        config.0.insert(
430            address!("0x0000000000000000000000000000000000000003"),
431            PruneMode::Before(5000000),
432        );
433        assert_eq!(args.receipts_log_filter, Some(config));
434    }
435
436    #[test]
437    fn parse_receiptslogfilter() {
438        let default_args = PruningArgs::default();
439        let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
440        assert_eq!(args, default_args);
441    }
442
443    #[test]
444    fn test_parse_receipts_log_filter() {
445        let filter1 = "0x0000000000000000000000000000000000000001:full";
446        let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
447        let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
448        let filters = [filter1, filter2, filter3].join(",");
449
450        // Args can be parsed.
451        let result = parse_receipts_log_filter(&filters);
452        assert!(result.is_ok());
453        let config = result.unwrap();
454        assert_eq!(config.0.len(), 3);
455
456        // Check that the args were parsed correctly.
457        let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
458        let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
459        let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
460
461        assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
462        assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
463        assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
464    }
465
466    #[test]
467    fn test_parse_receipts_log_filter_with_spaces() {
468        // Verify that spaces after commas are handled correctly
469        let filters = "0x0000000000000000000000000000000000000001:full, 0x0000000000000000000000000000000000000002:distance:1000";
470
471        let result = parse_receipts_log_filter(filters);
472        assert!(result.is_ok());
473        let config = result.unwrap();
474        assert_eq!(config.0.len(), 2);
475
476        let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
477        let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
478
479        assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
480        assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
481    }
482
483    #[test]
484    fn test_parse_receipts_log_filter_invalid_filter_format() {
485        let result = parse_receipts_log_filter("invalid_format");
486        assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
487    }
488
489    #[test]
490    fn test_parse_receipts_log_filter_invalid_address() {
491        let result = parse_receipts_log_filter("invalid_address:full");
492        assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
493    }
494
495    #[test]
496    fn test_parse_receipts_log_filter_invalid_prune_mode() {
497        let result =
498            parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
499        assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
500    }
501
502    #[test]
503    fn test_parse_receipts_log_filter_invalid_distance() {
504        let result = parse_receipts_log_filter(
505            "0x0000000000000000000000000000000000000000:distance:invalid_distance",
506        );
507        assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
508    }
509
510    #[test]
511    fn test_parse_receipts_log_filter_invalid_block_number() {
512        let result = parse_receipts_log_filter(
513            "0x0000000000000000000000000000000000000000:before:invalid_block",
514        );
515        assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
516    }
517}