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
201impl PruningArgs {
202    /// Returns pruning configuration.
203    ///
204    /// Returns [`None`] if no parameters are specified and default pruning configuration should be
205    /// used.
206    pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
207    where
208        ChainSpec: EthereumHardforks,
209    {
210        // Initialize with a default prune configuration.
211        let mut config = PruneConfig::default();
212
213        // If --full is set, use full node defaults.
214        if self.full {
215            let defaults = DefaultPruningValues::get_global();
216            let mut segments = defaults.full_prune_modes.clone();
217            if defaults.full_bodies_history_use_pre_merge {
218                segments.bodies_history = chain_spec
219                    .ethereum_fork_activation(EthereumHardfork::Paris)
220                    .block_number()
221                    .map(PruneMode::Before);
222            }
223            config = PruneConfig { block_interval: config.block_interval, segments }
224        }
225
226        // If --minimal is set, use minimal storage mode with aggressive pruning.
227        if self.minimal {
228            config = PruneConfig {
229                block_interval: config.block_interval,
230                segments: DefaultPruningValues::get_global().minimal_prune_modes.clone(),
231            }
232        }
233
234        // Override with any explicitly set prune.* flags.
235        if let Some(block_interval) = self.block_interval {
236            config.block_interval = block_interval as usize;
237        }
238        if let Some(mode) = self.sender_recovery_prune_mode() {
239            config.segments.sender_recovery = Some(mode);
240        }
241        if let Some(mode) = self.transaction_lookup_prune_mode() {
242            config.segments.transaction_lookup = Some(mode);
243        }
244        if let Some(mode) = self.receipts_prune_mode(chain_spec) {
245            config.segments.receipts = Some(mode);
246        }
247        if let Some(mode) = self.account_history_prune_mode() {
248            config.segments.account_history = Some(mode);
249        }
250        if let Some(mode) = self.bodies_prune_mode(chain_spec) {
251            config.segments.bodies_history = Some(mode);
252        }
253        if let Some(mode) = self.storage_history_prune_mode() {
254            config.segments.storage_history = Some(mode);
255        }
256        if let Some(receipt_logs) =
257            self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned()
258        {
259            config.segments.receipts_log_filter = receipt_logs;
260            // need to remove the receipts segment filter entirely because that takes precedence
261            // over the logs filter
262            config.segments.receipts.take();
263        }
264
265        config.is_default().not().then_some(config)
266    }
267
268    fn bodies_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
269    where
270        ChainSpec: EthereumHardforks,
271    {
272        if self.bodies_pre_merge {
273            chain_spec
274                .ethereum_fork_activation(EthereumHardfork::Paris)
275                .block_number()
276                .map(PruneMode::Before)
277        } else if let Some(distance) = self.bodies_distance {
278            Some(PruneMode::Distance(distance))
279        } else {
280            self.bodies_before.map(PruneMode::Before)
281        }
282    }
283
284    const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
285        if self.sender_recovery_full {
286            Some(PruneMode::Full)
287        } else if let Some(distance) = self.sender_recovery_distance {
288            Some(PruneMode::Distance(distance))
289        } else if let Some(block_number) = self.sender_recovery_before {
290            Some(PruneMode::Before(block_number))
291        } else {
292            None
293        }
294    }
295
296    const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
297        if self.transaction_lookup_full {
298            Some(PruneMode::Full)
299        } else if let Some(distance) = self.transaction_lookup_distance {
300            Some(PruneMode::Distance(distance))
301        } else if let Some(block_number) = self.transaction_lookup_before {
302            Some(PruneMode::Before(block_number))
303        } else {
304            None
305        }
306    }
307
308    fn receipts_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
309    where
310        ChainSpec: EthereumHardforks,
311    {
312        if self.receipts_pre_merge {
313            chain_spec
314                .ethereum_fork_activation(EthereumHardfork::Paris)
315                .block_number()
316                .map(PruneMode::Before)
317        } else if self.receipts_full {
318            Some(PruneMode::Full)
319        } else if let Some(distance) = self.receipts_distance {
320            Some(PruneMode::Distance(distance))
321        } else {
322            self.receipts_before.map(PruneMode::Before)
323        }
324    }
325
326    const fn account_history_prune_mode(&self) -> Option<PruneMode> {
327        if self.account_history_full {
328            Some(PruneMode::Full)
329        } else if let Some(distance) = self.account_history_distance {
330            Some(PruneMode::Distance(distance))
331        } else if let Some(block_number) = self.account_history_before {
332            Some(PruneMode::Before(block_number))
333        } else {
334            None
335        }
336    }
337
338    const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
339        if self.storage_history_full {
340            Some(PruneMode::Full)
341        } else if let Some(distance) = self.storage_history_distance {
342            Some(PruneMode::Distance(distance))
343        } else if let Some(block_number) = self.storage_history_before {
344            Some(PruneMode::Before(block_number))
345        } else {
346            None
347        }
348    }
349}
350
351/// Parses `,` separated pruning info into [`ReceiptsLogPruneConfig`].
352pub(crate) fn parse_receipts_log_filter(
353    value: &str,
354) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
355    let mut config = BTreeMap::new();
356    // Split out each of the filters.
357    let filters = value.split(',');
358    for filter in filters {
359        let parts: Vec<&str> = filter.split(':').collect();
360        if parts.len() < 2 {
361            return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
362        }
363        // Parse the address
364        let address = parts[0]
365            .parse::<Address>()
366            .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
367
368        // Parse the prune mode
369        let prune_mode = match parts[1] {
370            "full" => PruneMode::Full,
371            s if s.starts_with("distance") => {
372                if parts.len() < 3 {
373                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
374                }
375                let distance =
376                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
377                PruneMode::Distance(distance)
378            }
379            s if s.starts_with("before") => {
380                if parts.len() < 3 {
381                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
382                }
383                let block_number =
384                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidBlockNumber)?;
385                PruneMode::Before(block_number)
386            }
387            _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
388        };
389        config.insert(address, prune_mode);
390    }
391    Ok(ReceiptsLogPruneConfig(config))
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use alloy_primitives::address;
398    use clap::Parser;
399
400    /// A helper type to parse Args more easily
401    #[derive(Parser)]
402    struct CommandParser<T: Args> {
403        #[command(flatten)]
404        args: T,
405    }
406
407    #[test]
408    fn pruning_args_sanity_check() {
409        let args = CommandParser::<PruningArgs>::parse_from([
410            "reth",
411            "--prune.receiptslogfilter",
412            "0x0000000000000000000000000000000000000003:before:5000000",
413        ])
414        .args;
415        let mut config = ReceiptsLogPruneConfig::default();
416        config.0.insert(
417            address!("0x0000000000000000000000000000000000000003"),
418            PruneMode::Before(5000000),
419        );
420        assert_eq!(args.receipts_log_filter, Some(config));
421    }
422
423    #[test]
424    fn parse_receiptslogfilter() {
425        let default_args = PruningArgs::default();
426        let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
427        assert_eq!(args, default_args);
428    }
429
430    #[test]
431    fn test_parse_receipts_log_filter() {
432        let filter1 = "0x0000000000000000000000000000000000000001:full";
433        let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
434        let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
435        let filters = [filter1, filter2, filter3].join(",");
436
437        // Args can be parsed.
438        let result = parse_receipts_log_filter(&filters);
439        assert!(result.is_ok());
440        let config = result.unwrap();
441        assert_eq!(config.0.len(), 3);
442
443        // Check that the args were parsed correctly.
444        let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
445        let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
446        let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
447
448        assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
449        assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
450        assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
451    }
452
453    #[test]
454    fn test_parse_receipts_log_filter_invalid_filter_format() {
455        let result = parse_receipts_log_filter("invalid_format");
456        assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
457    }
458
459    #[test]
460    fn test_parse_receipts_log_filter_invalid_address() {
461        let result = parse_receipts_log_filter("invalid_address:full");
462        assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
463    }
464
465    #[test]
466    fn test_parse_receipts_log_filter_invalid_prune_mode() {
467        let result =
468            parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
469        assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
470    }
471
472    #[test]
473    fn test_parse_receipts_log_filter_invalid_distance() {
474        let result = parse_receipts_log_filter(
475            "0x0000000000000000000000000000000000000000:distance:invalid_distance",
476        );
477        assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
478    }
479
480    #[test]
481    fn test_parse_receipts_log_filter_invalid_block_number() {
482        let result = parse_receipts_log_filter(
483            "0x0000000000000000000000000000000000000000:before:invalid_block",
484        );
485        assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
486    }
487}