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::{PruneMode, PruneModes, ReceiptsLogPruneConfig, MINIMUM_PRUNING_DISTANCE};
9use std::{collections::BTreeMap, ops::Not};
10
11/// Parameters for pruning and full node
12#[derive(Debug, Clone, Args, PartialEq, Eq, Default)]
13#[command(next_help_heading = "Pruning")]
14pub struct PruningArgs {
15    /// Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored.
16    #[arg(long, default_value_t = false)]
17    pub full: bool,
18
19    /// Minimum pruning interval measured in blocks.
20    #[arg(long = "prune.block-interval", alias = "block-interval", value_parser = RangedU64ValueParser::<u64>::new().range(1..))]
21    pub block_interval: Option<u64>,
22
23    // Sender Recovery
24    /// Prunes all sender recovery data.
25    #[arg(long = "prune.sender-recovery.full", alias = "prune.senderrecovery.full", conflicts_with_all = &["sender_recovery_distance", "sender_recovery_before"])]
26    pub sender_recovery_full: bool,
27    /// Prune sender recovery data before the `head-N` block number. In other words, keep last N +
28    /// 1 blocks.
29    #[arg(long = "prune.sender-recovery.distance", alias = "prune.senderrecovery.distance", value_name = "BLOCKS", conflicts_with_all = &["sender_recovery_full", "sender_recovery_before"])]
30    pub sender_recovery_distance: Option<u64>,
31    /// Prune sender recovery data before the specified block number. The specified block number is
32    /// not pruned.
33    #[arg(long = "prune.sender-recovery.before", alias = "prune.senderrecovery.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["sender_recovery_full", "sender_recovery_distance"])]
34    pub sender_recovery_before: Option<BlockNumber>,
35
36    // Transaction Lookup
37    /// Prunes all transaction lookup data.
38    #[arg(long = "prune.transaction-lookup.full", alias = "prune.transactionlookup.full", conflicts_with_all = &["transaction_lookup_distance", "transaction_lookup_before"])]
39    pub transaction_lookup_full: bool,
40    /// Prune transaction lookup data before the `head-N` block number. In other words, keep last N
41    /// + 1 blocks.
42    #[arg(long = "prune.transaction-lookup.distance", alias = "prune.transactionlookup.distance", value_name = "BLOCKS", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_before"])]
43    pub transaction_lookup_distance: Option<u64>,
44    /// Prune transaction lookup data before the specified block number. The specified block number
45    /// is not pruned.
46    #[arg(long = "prune.transaction-lookup.before", alias = "prune.transactionlookup.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_distance"])]
47    pub transaction_lookup_before: Option<BlockNumber>,
48
49    // Receipts
50    /// Prunes all receipt data.
51    #[arg(long = "prune.receipts.full", conflicts_with_all = &["receipts_pre_merge", "receipts_distance", "receipts_before"])]
52    pub receipts_full: bool,
53    /// Prune receipts before the merge block.
54    #[arg(long = "prune.receipts.pre-merge", conflicts_with_all = &["receipts_full", "receipts_distance", "receipts_before"])]
55    pub receipts_pre_merge: bool,
56    /// Prune receipts before the `head-N` block number. In other words, keep last N + 1 blocks.
57    #[arg(long = "prune.receipts.distance", value_name = "BLOCKS", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_before"])]
58    pub receipts_distance: Option<u64>,
59    /// Prune receipts before the specified block number. The specified block number is not pruned.
60    #[arg(long = "prune.receipts.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_distance"])]
61    pub receipts_before: Option<BlockNumber>,
62    // Receipts Log Filter
63    /// Configure receipts log filter. Format:
64    /// <`address`>:<`prune_mode`>... where <`prune_mode`> can be 'full', 'distance:<`blocks`>', or
65    /// 'before:<`block_number`>'
66    #[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)]
67    pub receipts_log_filter: Option<ReceiptsLogPruneConfig>,
68
69    // Account History
70    /// Prunes all account history.
71    #[arg(long = "prune.account-history.full", alias = "prune.accounthistory.full", conflicts_with_all = &["account_history_distance", "account_history_before"])]
72    pub account_history_full: bool,
73    /// Prune account before the `head-N` block number. In other words, keep last N + 1 blocks.
74    #[arg(long = "prune.account-history.distance", alias = "prune.accounthistory.distance", value_name = "BLOCKS", conflicts_with_all = &["account_history_full", "account_history_before"])]
75    pub account_history_distance: Option<u64>,
76    /// Prune account history before the specified block number. The specified block number is not
77    /// pruned.
78    #[arg(long = "prune.account-history.before", alias = "prune.accounthistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["account_history_full", "account_history_distance"])]
79    pub account_history_before: Option<BlockNumber>,
80
81    // Storage History
82    /// Prunes all storage history data.
83    #[arg(long = "prune.storage-history.full", alias = "prune.storagehistory.full", conflicts_with_all = &["storage_history_distance", "storage_history_before"])]
84    pub storage_history_full: bool,
85    /// Prune storage history before the `head-N` block number. In other words, keep last N + 1
86    /// blocks.
87    #[arg(long = "prune.storage-history.distance", alias = "prune.storagehistory.distance", value_name = "BLOCKS", conflicts_with_all = &["storage_history_full", "storage_history_before"])]
88    pub storage_history_distance: Option<u64>,
89    /// Prune storage history before the specified block number. The specified block number is not
90    /// pruned.
91    #[arg(long = "prune.storage-history.before", alias = "prune.storagehistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["storage_history_full", "storage_history_distance"])]
92    pub storage_history_before: Option<BlockNumber>,
93
94    // Bodies
95    /// Prune bodies before the merge block.
96    #[arg(long = "prune.bodies.pre-merge", value_name = "BLOCKS", conflicts_with_all = &["bodies_distance", "bodies_before"])]
97    pub bodies_pre_merge: bool,
98    /// Prune bodies before the `head-N` block number. In other words, keep last N + 1
99    /// blocks.
100    #[arg(long = "prune.bodies.distance", value_name = "BLOCKS", conflicts_with_all = &["bodies_pre_merge", "bodies_before"])]
101    pub bodies_distance: Option<u64>,
102    /// Prune storage history before the specified block number. The specified block number is not
103    /// pruned.
104    #[arg(long = "prune.bodies.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["bodies_distance", "bodies_pre_merge"])]
105    pub bodies_before: Option<BlockNumber>,
106}
107
108impl PruningArgs {
109    /// Returns pruning configuration.
110    ///
111    /// Returns [`None`] if no parameters are specified and default pruning configuration should be
112    /// used.
113    pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
114    where
115        ChainSpec: EthereumHardforks,
116    {
117        // Initialize with a default prune configuration.
118        let mut config = PruneConfig::default();
119
120        // If --full is set, use full node defaults.
121        if self.full {
122            config = PruneConfig {
123                block_interval: config.block_interval,
124                segments: PruneModes {
125                    sender_recovery: Some(PruneMode::Full),
126                    transaction_lookup: None,
127                    receipts: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
128                    account_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
129                    storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
130                    bodies_history: chain_spec
131                        .ethereum_fork_activation(EthereumHardfork::Paris)
132                        .block_number()
133                        .map(PruneMode::Before),
134                    merkle_changesets: PruneMode::Distance(MINIMUM_PRUNING_DISTANCE),
135                    receipts_log_filter: Default::default(),
136                },
137            }
138        }
139
140        // Override with any explicitly set prune.* flags.
141        if let Some(block_interval) = self.block_interval {
142            config.block_interval = block_interval as usize;
143        }
144        if let Some(mode) = self.sender_recovery_prune_mode() {
145            config.segments.sender_recovery = Some(mode);
146        }
147        if let Some(mode) = self.transaction_lookup_prune_mode() {
148            config.segments.transaction_lookup = Some(mode);
149        }
150        if let Some(mode) = self.receipts_prune_mode(chain_spec) {
151            config.segments.receipts = Some(mode);
152        }
153        if let Some(mode) = self.account_history_prune_mode() {
154            config.segments.account_history = Some(mode);
155        }
156        if let Some(mode) = self.bodies_prune_mode(chain_spec) {
157            config.segments.bodies_history = Some(mode);
158        }
159        if let Some(mode) = self.storage_history_prune_mode() {
160            config.segments.storage_history = Some(mode);
161        }
162        if let Some(receipt_logs) =
163            self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned()
164        {
165            config.segments.receipts_log_filter = receipt_logs;
166            // need to remove the receipts segment filter entirely because that takes precedence
167            // over the logs filter
168            config.segments.receipts.take();
169        }
170
171        config.is_default().not().then_some(config)
172    }
173
174    fn bodies_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
175    where
176        ChainSpec: EthereumHardforks,
177    {
178        if self.bodies_pre_merge {
179            chain_spec
180                .ethereum_fork_activation(EthereumHardfork::Paris)
181                .block_number()
182                .map(PruneMode::Before)
183        } else if let Some(distance) = self.bodies_distance {
184            Some(PruneMode::Distance(distance))
185        } else {
186            self.bodies_before.map(PruneMode::Before)
187        }
188    }
189
190    const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
191        if self.sender_recovery_full {
192            Some(PruneMode::Full)
193        } else if let Some(distance) = self.sender_recovery_distance {
194            Some(PruneMode::Distance(distance))
195        } else if let Some(block_number) = self.sender_recovery_before {
196            Some(PruneMode::Before(block_number))
197        } else {
198            None
199        }
200    }
201
202    const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
203        if self.transaction_lookup_full {
204            Some(PruneMode::Full)
205        } else if let Some(distance) = self.transaction_lookup_distance {
206            Some(PruneMode::Distance(distance))
207        } else if let Some(block_number) = self.transaction_lookup_before {
208            Some(PruneMode::Before(block_number))
209        } else {
210            None
211        }
212    }
213
214    fn receipts_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
215    where
216        ChainSpec: EthereumHardforks,
217    {
218        if self.receipts_pre_merge {
219            chain_spec
220                .ethereum_fork_activation(EthereumHardfork::Paris)
221                .block_number()
222                .map(PruneMode::Before)
223        } else if self.receipts_full {
224            Some(PruneMode::Full)
225        } else if let Some(distance) = self.receipts_distance {
226            Some(PruneMode::Distance(distance))
227        } else {
228            self.receipts_before.map(PruneMode::Before)
229        }
230    }
231
232    const fn account_history_prune_mode(&self) -> Option<PruneMode> {
233        if self.account_history_full {
234            Some(PruneMode::Full)
235        } else if let Some(distance) = self.account_history_distance {
236            Some(PruneMode::Distance(distance))
237        } else if let Some(block_number) = self.account_history_before {
238            Some(PruneMode::Before(block_number))
239        } else {
240            None
241        }
242    }
243
244    const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
245        if self.storage_history_full {
246            Some(PruneMode::Full)
247        } else if let Some(distance) = self.storage_history_distance {
248            Some(PruneMode::Distance(distance))
249        } else if let Some(block_number) = self.storage_history_before {
250            Some(PruneMode::Before(block_number))
251        } else {
252            None
253        }
254    }
255}
256
257/// Parses `,` separated pruning info into [`ReceiptsLogPruneConfig`].
258pub(crate) fn parse_receipts_log_filter(
259    value: &str,
260) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
261    let mut config = BTreeMap::new();
262    // Split out each of the filters.
263    let filters = value.split(',');
264    for filter in filters {
265        let parts: Vec<&str> = filter.split(':').collect();
266        if parts.len() < 2 {
267            return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
268        }
269        // Parse the address
270        let address = parts[0]
271            .parse::<Address>()
272            .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
273
274        // Parse the prune mode
275        let prune_mode = match parts[1] {
276            "full" => PruneMode::Full,
277            s if s.starts_with("distance") => {
278                if parts.len() < 3 {
279                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
280                }
281                let distance =
282                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
283                PruneMode::Distance(distance)
284            }
285            s if s.starts_with("before") => {
286                if parts.len() < 3 {
287                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
288                }
289                let block_number =
290                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidBlockNumber)?;
291                PruneMode::Before(block_number)
292            }
293            _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
294        };
295        config.insert(address, prune_mode);
296    }
297    Ok(ReceiptsLogPruneConfig(config))
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use alloy_primitives::address;
304    use clap::Parser;
305
306    /// A helper type to parse Args more easily
307    #[derive(Parser)]
308    struct CommandParser<T: Args> {
309        #[command(flatten)]
310        args: T,
311    }
312
313    #[test]
314    fn pruning_args_sanity_check() {
315        let args = CommandParser::<PruningArgs>::parse_from([
316            "reth",
317            "--prune.receiptslogfilter",
318            "0x0000000000000000000000000000000000000003:before:5000000",
319        ])
320        .args;
321        let mut config = ReceiptsLogPruneConfig::default();
322        config.0.insert(
323            address!("0x0000000000000000000000000000000000000003"),
324            PruneMode::Before(5000000),
325        );
326        assert_eq!(args.receipts_log_filter, Some(config));
327    }
328
329    #[test]
330    fn parse_receiptslogfilter() {
331        let default_args = PruningArgs::default();
332        let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
333        assert_eq!(args, default_args);
334    }
335
336    #[test]
337    fn test_parse_receipts_log_filter() {
338        let filter1 = "0x0000000000000000000000000000000000000001:full";
339        let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
340        let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
341        let filters = [filter1, filter2, filter3].join(",");
342
343        // Args can be parsed.
344        let result = parse_receipts_log_filter(&filters);
345        assert!(result.is_ok());
346        let config = result.unwrap();
347        assert_eq!(config.0.len(), 3);
348
349        // Check that the args were parsed correctly.
350        let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
351        let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
352        let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
353
354        assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
355        assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
356        assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
357    }
358
359    #[test]
360    fn test_parse_receipts_log_filter_invalid_filter_format() {
361        let result = parse_receipts_log_filter("invalid_format");
362        assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
363    }
364
365    #[test]
366    fn test_parse_receipts_log_filter_invalid_address() {
367        let result = parse_receipts_log_filter("invalid_address:full");
368        assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
369    }
370
371    #[test]
372    fn test_parse_receipts_log_filter_invalid_prune_mode() {
373        let result =
374            parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
375        assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
376    }
377
378    #[test]
379    fn test_parse_receipts_log_filter_invalid_distance() {
380        let result = parse_receipts_log_filter(
381            "0x0000000000000000000000000000000000000000:distance:invalid_distance",
382        );
383        assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
384    }
385
386    #[test]
387    fn test_parse_receipts_log_filter_invalid_block_number() {
388        let result = parse_receipts_log_filter(
389            "0x0000000000000000000000000000000000000000:before:invalid_block",
390        );
391        assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
392    }
393}