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