reth_node_core/args/
pruning.rs

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