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;
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, 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.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.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.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.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.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.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.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.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.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.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.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.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    pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
111    where
112        ChainSpec: EthereumHardforks,
113    {
114        // Initialise with a default prune configuration.
115        let mut config = PruneConfig::default();
116
117        // If --full is set, use full node defaults.
118        if self.full {
119            config = PruneConfig {
120                block_interval: config.block_interval,
121                segments: PruneModes {
122                    sender_recovery: Some(PruneMode::Full),
123                    transaction_lookup: None,
124                    receipts: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
125                    account_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
126                    storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
127                    // TODO: set default to pre-merge block if available
128                    bodies_history: None,
129                    receipts_log_filter: Default::default(),
130                },
131            }
132        }
133
134        // Override with any explicitly set prune.* flags.
135        if let Some(block_interval) = self.block_interval {
136            config.block_interval = block_interval as usize;
137        }
138        if let Some(mode) = self.sender_recovery_prune_mode() {
139            config.segments.sender_recovery = Some(mode);
140        }
141        if let Some(mode) = self.transaction_lookup_prune_mode() {
142            config.segments.transaction_lookup = Some(mode);
143        }
144        if let Some(mode) = self.receipts_prune_mode(chain_spec) {
145            config.segments.receipts = Some(mode);
146        }
147        if let Some(mode) = self.account_history_prune_mode() {
148            config.segments.account_history = Some(mode);
149        }
150        if let Some(mode) = self.bodies_prune_mode(chain_spec) {
151            config.segments.bodies_history = Some(mode);
152        }
153        if let Some(mode) = self.storage_history_prune_mode() {
154            config.segments.storage_history = Some(mode);
155        }
156        if let Some(receipt_logs) =
157            self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned()
158        {
159            config.segments.receipts_log_filter = receipt_logs;
160            // need to remove the receipts segment filter entirely because that takes precedence
161            // over the logs filter
162            config.segments.receipts.take();
163        }
164
165        Some(config)
166    }
167
168    fn bodies_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
169    where
170        ChainSpec: EthereumHardforks,
171    {
172        if self.bodies_pre_merge {
173            chain_spec
174                .ethereum_fork_activation(EthereumHardfork::Paris)
175                .block_number()
176                .map(PruneMode::Before)
177        } else if let Some(distance) = self.bodies_distance {
178            Some(PruneMode::Distance(distance))
179        } else {
180            self.bodies_before.map(PruneMode::Before)
181        }
182    }
183
184    const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
185        if self.sender_recovery_full {
186            Some(PruneMode::Full)
187        } else if let Some(distance) = self.sender_recovery_distance {
188            Some(PruneMode::Distance(distance))
189        } else if let Some(block_number) = self.sender_recovery_before {
190            Some(PruneMode::Before(block_number))
191        } else {
192            None
193        }
194    }
195
196    const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
197        if self.transaction_lookup_full {
198            Some(PruneMode::Full)
199        } else if let Some(distance) = self.transaction_lookup_distance {
200            Some(PruneMode::Distance(distance))
201        } else if let Some(block_number) = self.transaction_lookup_before {
202            Some(PruneMode::Before(block_number))
203        } else {
204            None
205        }
206    }
207
208    fn receipts_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
209    where
210        ChainSpec: EthereumHardforks,
211    {
212        if self.receipts_pre_merge {
213            chain_spec
214                .ethereum_fork_activation(EthereumHardfork::Paris)
215                .block_number()
216                .map(PruneMode::Before)
217        } else if self.receipts_full {
218            Some(PruneMode::Full)
219        } else if let Some(distance) = self.receipts_distance {
220            Some(PruneMode::Distance(distance))
221        } else {
222            self.receipts_before.map(PruneMode::Before)
223        }
224    }
225
226    const fn account_history_prune_mode(&self) -> Option<PruneMode> {
227        if self.account_history_full {
228            Some(PruneMode::Full)
229        } else if let Some(distance) = self.account_history_distance {
230            Some(PruneMode::Distance(distance))
231        } else if let Some(block_number) = self.account_history_before {
232            Some(PruneMode::Before(block_number))
233        } else {
234            None
235        }
236    }
237
238    const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
239        if self.storage_history_full {
240            Some(PruneMode::Full)
241        } else if let Some(distance) = self.storage_history_distance {
242            Some(PruneMode::Distance(distance))
243        } else if let Some(block_number) = self.storage_history_before {
244            Some(PruneMode::Before(block_number))
245        } else {
246            None
247        }
248    }
249}
250
251/// Parses `,` separated pruning info into [`ReceiptsLogPruneConfig`].
252pub(crate) fn parse_receipts_log_filter(
253    value: &str,
254) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
255    let mut config = BTreeMap::new();
256    // Split out each of the filters.
257    let filters = value.split(',');
258    for filter in filters {
259        let parts: Vec<&str> = filter.split(':').collect();
260        if parts.len() < 2 {
261            return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
262        }
263        // Parse the address
264        let address = parts[0]
265            .parse::<Address>()
266            .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
267
268        // Parse the prune mode
269        let prune_mode = match parts[1] {
270            "full" => PruneMode::Full,
271            s if s.starts_with("distance") => {
272                if parts.len() < 3 {
273                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
274                }
275                let distance =
276                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
277                PruneMode::Distance(distance)
278            }
279            s if s.starts_with("before") => {
280                if parts.len() < 3 {
281                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
282                }
283                let block_number =
284                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidBlockNumber)?;
285                PruneMode::Before(block_number)
286            }
287            _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
288        };
289        config.insert(address, prune_mode);
290    }
291    Ok(ReceiptsLogPruneConfig(config))
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use alloy_primitives::address;
298    use clap::Parser;
299
300    /// A helper type to parse Args more easily
301    #[derive(Parser)]
302    struct CommandParser<T: Args> {
303        #[command(flatten)]
304        args: T,
305    }
306
307    #[test]
308    fn pruning_args_sanity_check() {
309        let args = CommandParser::<PruningArgs>::parse_from([
310            "reth",
311            "--prune.receiptslogfilter",
312            "0x0000000000000000000000000000000000000003:before:5000000",
313        ])
314        .args;
315        let mut config = ReceiptsLogPruneConfig::default();
316        config.0.insert(
317            address!("0x0000000000000000000000000000000000000003"),
318            PruneMode::Before(5000000),
319        );
320        assert_eq!(args.receipts_log_filter, Some(config));
321    }
322
323    #[test]
324    fn parse_receiptslogfilter() {
325        let default_args = PruningArgs::default();
326        let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
327        assert_eq!(args, default_args);
328    }
329
330    #[test]
331    fn test_parse_receipts_log_filter() {
332        let filter1 = "0x0000000000000000000000000000000000000001:full";
333        let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
334        let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
335        let filters = [filter1, filter2, filter3].join(",");
336
337        // Args can be parsed.
338        let result = parse_receipts_log_filter(&filters);
339        assert!(result.is_ok());
340        let config = result.unwrap();
341        assert_eq!(config.0.len(), 3);
342
343        // Check that the args were parsed correctly.
344        let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
345        let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
346        let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
347
348        assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
349        assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
350        assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
351    }
352
353    #[test]
354    fn test_parse_receipts_log_filter_invalid_filter_format() {
355        let result = parse_receipts_log_filter("invalid_format");
356        assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
357    }
358
359    #[test]
360    fn test_parse_receipts_log_filter_invalid_address() {
361        let result = parse_receipts_log_filter("invalid_address:full");
362        assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
363    }
364
365    #[test]
366    fn test_parse_receipts_log_filter_invalid_prune_mode() {
367        let result =
368            parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
369        assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
370    }
371
372    #[test]
373    fn test_parse_receipts_log_filter_invalid_distance() {
374        let result = parse_receipts_log_filter(
375            "0x0000000000000000000000000000000000000000:distance:invalid_distance",
376        );
377        assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
378    }
379
380    #[test]
381    fn test_parse_receipts_log_filter_invalid_block_number() {
382        let result = parse_receipts_log_filter(
383            "0x0000000000000000000000000000000000000000:before:invalid_block",
384        );
385        assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
386    }
387}