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_chainspec::EthChainSpec;
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_distance", "receipts_before"])]
52    pub receipts_full: bool,
53    /// Prune receipts before the `head-N` block number. In other words, keep last N + 1 blocks.
54    #[arg(long = "prune.receipts.distance", value_name = "BLOCKS", conflicts_with_all = &["receipts_full", "receipts_before"])]
55    pub receipts_distance: Option<u64>,
56    /// Prune receipts before the specified block number. The specified block number is not pruned.
57    #[arg(long = "prune.receipts.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["receipts_full", "receipts_distance"])]
58    pub receipts_before: Option<BlockNumber>,
59
60    // Account History
61    /// Prunes all account history.
62    #[arg(long = "prune.accounthistory.full", conflicts_with_all = &["account_history_distance", "account_history_before"])]
63    pub account_history_full: bool,
64    /// Prune account before the `head-N` block number. In other words, keep last N + 1 blocks.
65    #[arg(long = "prune.accounthistory.distance", value_name = "BLOCKS", conflicts_with_all = &["account_history_full", "account_history_before"])]
66    pub account_history_distance: Option<u64>,
67    /// Prune account history before the specified block number. The specified block number is not
68    /// pruned.
69    #[arg(long = "prune.accounthistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["account_history_full", "account_history_distance"])]
70    pub account_history_before: Option<BlockNumber>,
71
72    // Storage History
73    /// Prunes all storage history data.
74    #[arg(long = "prune.storagehistory.full", conflicts_with_all = &["storage_history_distance", "storage_history_before"])]
75    pub storage_history_full: bool,
76    /// Prune storage history before the `head-N` block number. In other words, keep last N + 1
77    /// blocks.
78    #[arg(long = "prune.storagehistory.distance", value_name = "BLOCKS", conflicts_with_all = &["storage_history_full", "storage_history_before"])]
79    pub storage_history_distance: Option<u64>,
80    /// Prune storage history before the specified block number. The specified block number is not
81    /// pruned.
82    #[arg(long = "prune.storagehistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["storage_history_full", "storage_history_distance"])]
83    pub storage_history_before: Option<BlockNumber>,
84
85    // Receipts Log Filter
86    /// Configure receipts log filter. Format:
87    /// <`address`>:<`prune_mode`>[,<`address`>:<`prune_mode`>...] Where <`prune_mode`> can be
88    /// 'full', 'distance:<`blocks`>', or 'before:<`block_number`>'
89    #[arg(long = "prune.receiptslogfilter", value_name = "FILTER_CONFIG", value_delimiter = ',', value_parser = parse_receipts_log_filter)]
90    pub receipts_log_filter: Vec<String>,
91}
92
93impl PruningArgs {
94    /// Returns pruning configuration.
95    pub fn prune_config(&self, chain_spec: &impl EthChainSpec) -> Option<PruneConfig> {
96        // Initialise with a default prune configuration.
97        let mut config = PruneConfig::default();
98
99        // If --full is set, use full node defaults.
100        if self.full {
101            config = PruneConfig {
102                block_interval: config.block_interval,
103                segments: PruneModes {
104                    sender_recovery: Some(PruneMode::Full),
105                    transaction_lookup: None,
106                    // prune all receipts if chain doesn't have deposit contract specified in chain
107                    // spec
108                    receipts: chain_spec
109                        .deposit_contract()
110                        .map(|contract| PruneMode::Before(contract.block))
111                        .or(Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE))),
112                    account_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
113                    storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
114                    receipts_log_filter: ReceiptsLogPruneConfig(
115                        chain_spec
116                            .deposit_contract()
117                            .map(|contract| (contract.address, PruneMode::Before(contract.block)))
118                            .into_iter()
119                            .collect(),
120                    ),
121                },
122            }
123        }
124
125        // Override with any explicitly set prune.* flags.
126        if let Some(block_interval) = self.block_interval {
127            config.block_interval = block_interval as usize;
128        }
129        if let Some(mode) = self.sender_recovery_prune_mode() {
130            config.segments.sender_recovery = Some(mode);
131        }
132        if let Some(mode) = self.transaction_lookup_prune_mode() {
133            config.segments.transaction_lookup = Some(mode);
134        }
135        if let Some(mode) = self.receipts_prune_mode() {
136            config.segments.receipts = Some(mode);
137        }
138        if let Some(mode) = self.account_history_prune_mode() {
139            config.segments.account_history = Some(mode);
140        }
141        if let Some(mode) = self.storage_history_prune_mode() {
142            config.segments.storage_history = Some(mode);
143        }
144
145        Some(config)
146    }
147    const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
148        if self.sender_recovery_full {
149            Some(PruneMode::Full)
150        } else if let Some(distance) = self.sender_recovery_distance {
151            Some(PruneMode::Distance(distance))
152        } else if let Some(block_number) = self.sender_recovery_before {
153            Some(PruneMode::Before(block_number))
154        } else {
155            None
156        }
157    }
158
159    const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
160        if self.transaction_lookup_full {
161            Some(PruneMode::Full)
162        } else if let Some(distance) = self.transaction_lookup_distance {
163            Some(PruneMode::Distance(distance))
164        } else if let Some(block_number) = self.transaction_lookup_before {
165            Some(PruneMode::Before(block_number))
166        } else {
167            None
168        }
169    }
170
171    const fn receipts_prune_mode(&self) -> Option<PruneMode> {
172        if self.receipts_full {
173            Some(PruneMode::Full)
174        } else if let Some(distance) = self.receipts_distance {
175            Some(PruneMode::Distance(distance))
176        } else if let Some(block_number) = self.receipts_before {
177            Some(PruneMode::Before(block_number))
178        } else {
179            None
180        }
181    }
182
183    const fn account_history_prune_mode(&self) -> Option<PruneMode> {
184        if self.account_history_full {
185            Some(PruneMode::Full)
186        } else if let Some(distance) = self.account_history_distance {
187            Some(PruneMode::Distance(distance))
188        } else if let Some(block_number) = self.account_history_before {
189            Some(PruneMode::Before(block_number))
190        } else {
191            None
192        }
193    }
194
195    const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
196        if self.storage_history_full {
197            Some(PruneMode::Full)
198        } else if let Some(distance) = self.storage_history_distance {
199            Some(PruneMode::Distance(distance))
200        } else if let Some(block_number) = self.storage_history_before {
201            Some(PruneMode::Before(block_number))
202        } else {
203            None
204        }
205    }
206}
207
208pub(crate) fn parse_receipts_log_filter(
209    value: &str,
210) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
211    let mut config = BTreeMap::new();
212    // Split out each of the filters.
213    let filters = value.split(',');
214    for filter in filters {
215        let parts: Vec<&str> = filter.split(':').collect();
216        if parts.len() < 2 {
217            return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
218        }
219        // Parse the address
220        let address = parts[0]
221            .parse::<Address>()
222            .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
223
224        // Parse the prune mode
225        let prune_mode = match parts[1] {
226            "full" => PruneMode::Full,
227            s if s.starts_with("distance") => {
228                if parts.len() < 3 {
229                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
230                }
231                let distance =
232                    parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
233                PruneMode::Distance(distance)
234            }
235            s if s.starts_with("before") => {
236                if parts.len() < 3 {
237                    return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
238                }
239                let block_number = parts[2]
240                    .parse::<BlockNumber>()
241                    .map_err(ReceiptsLogError::InvalidBlockNumber)?;
242                PruneMode::Before(block_number)
243            }
244            _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
245        };
246        config.insert(address, prune_mode);
247    }
248    Ok(ReceiptsLogPruneConfig(config))
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use clap::Parser;
255
256    /// A helper type to parse Args more easily
257    #[derive(Parser)]
258    struct CommandParser<T: Args> {
259        #[command(flatten)]
260        args: T,
261    }
262
263    #[test]
264    fn pruning_args_sanity_check() {
265        let default_args = PruningArgs::default();
266        let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
267        assert_eq!(args, default_args);
268    }
269
270    #[test]
271    fn test_parse_receipts_log_filter() {
272        let filter1 = "0x0000000000000000000000000000000000000001:full";
273        let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
274        let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
275        let filters = [filter1, filter2, filter3].join(",");
276
277        // Args can be parsed.
278        let result = parse_receipts_log_filter(&filters);
279        assert!(result.is_ok());
280        let config = result.unwrap();
281        assert_eq!(config.0.len(), 3);
282
283        // Check that the args were parsed correctly.
284        let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
285        let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
286        let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
287
288        assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
289        assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
290        assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
291    }
292
293    #[test]
294    fn test_parse_receipts_log_filter_invalid_filter_format() {
295        let result = parse_receipts_log_filter("invalid_format");
296        assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
297    }
298
299    #[test]
300    fn test_parse_receipts_log_filter_invalid_address() {
301        let result = parse_receipts_log_filter("invalid_address:full");
302        assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
303    }
304
305    #[test]
306    fn test_parse_receipts_log_filter_invalid_prune_mode() {
307        let result =
308            parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
309        assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
310    }
311
312    #[test]
313    fn test_parse_receipts_log_filter_invalid_distance() {
314        let result = parse_receipts_log_filter(
315            "0x0000000000000000000000000000000000000000:distance:invalid_distance",
316        );
317        assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
318    }
319
320    #[test]
321    fn test_parse_receipts_log_filter_invalid_block_number() {
322        let result = parse_receipts_log_filter(
323            "0x0000000000000000000000000000000000000000:before:invalid_block",
324        );
325        assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
326    }
327}