1use 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, MINIMUM_DISTANCE, MINIMUM_UNWIND_SAFE_DISTANCE,
10};
11use std::{collections::BTreeMap, ops::Not, sync::OnceLock};
12
13static PRUNING_DEFAULTS: OnceLock<DefaultPruningValues> = OnceLock::new();
15
16#[derive(Debug, Clone)]
20pub struct DefaultPruningValues {
21 pub full_prune_modes: PruneModes,
25 pub full_bodies_history_use_pre_merge: bool,
28 pub minimal_prune_modes: PruneModes,
30}
31
32impl DefaultPruningValues {
33 pub fn try_init(self) -> Result<(), Self> {
37 PRUNING_DEFAULTS.set(self)
38 }
39
40 pub fn get_global() -> &'static Self {
42 PRUNING_DEFAULTS.get_or_init(Self::default)
43 }
44
45 pub fn with_full_prune_modes(mut self, modes: PruneModes) -> Self {
47 self.full_prune_modes = modes;
48 self
49 }
50
51 pub const fn with_full_bodies_history_use_pre_merge(mut self, use_pre_merge: bool) -> Self {
56 self.full_bodies_history_use_pre_merge = use_pre_merge;
57 self
58 }
59
60 pub fn with_minimal_prune_modes(mut self, modes: PruneModes) -> Self {
62 self.minimal_prune_modes = modes;
63 self
64 }
65}
66
67impl Default for DefaultPruningValues {
68 fn default() -> Self {
69 Self {
70 full_prune_modes: PruneModes {
71 sender_recovery: Some(PruneMode::Full),
72 transaction_lookup: None,
73 receipts: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
74 account_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
75 storage_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
76 bodies_history: None,
78 receipts_log_filter: Default::default(),
79 },
80 full_bodies_history_use_pre_merge: true,
81 minimal_prune_modes: PruneModes {
82 sender_recovery: Some(PruneMode::Full),
83 transaction_lookup: Some(PruneMode::Full),
84 receipts: Some(PruneMode::Distance(MINIMUM_DISTANCE)),
85 account_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
86 storage_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
87 bodies_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)),
88 receipts_log_filter: Default::default(),
89 },
90 }
91 }
92}
93
94#[derive(Debug, Clone, Args, PartialEq, Eq, Default)]
96#[command(next_help_heading = "Pruning")]
97pub struct PruningArgs {
98 #[arg(long, default_value_t = false, conflicts_with = "minimal")]
101 pub full: bool,
102
103 #[arg(long, default_value_t = false, conflicts_with = "full")]
110 pub minimal: bool,
111
112 #[arg(long = "prune.block-interval", alias = "block-interval", value_parser = RangedU64ValueParser::<u64>::new().range(1..))]
114 pub block_interval: Option<u64>,
115
116 #[arg(long = "prune.sender-recovery.full", alias = "prune.senderrecovery.full", conflicts_with_all = &["sender_recovery_distance", "sender_recovery_before"])]
119 pub sender_recovery_full: bool,
120 #[arg(long = "prune.sender-recovery.distance", alias = "prune.senderrecovery.distance", value_name = "BLOCKS", conflicts_with_all = &["sender_recovery_full", "sender_recovery_before"])]
123 pub sender_recovery_distance: Option<u64>,
124 #[arg(long = "prune.sender-recovery.before", alias = "prune.senderrecovery.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["sender_recovery_full", "sender_recovery_distance"])]
127 pub sender_recovery_before: Option<BlockNumber>,
128
129 #[arg(long = "prune.transaction-lookup.full", alias = "prune.transactionlookup.full", conflicts_with_all = &["transaction_lookup_distance", "transaction_lookup_before"])]
132 pub transaction_lookup_full: bool,
133 #[arg(long = "prune.transaction-lookup.distance", alias = "prune.transactionlookup.distance", value_name = "BLOCKS", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_before"])]
136 pub transaction_lookup_distance: Option<u64>,
137 #[arg(long = "prune.transaction-lookup.before", alias = "prune.transactionlookup.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_distance"])]
140 pub transaction_lookup_before: Option<BlockNumber>,
141
142 #[arg(long = "prune.receipts.full", conflicts_with_all = &["receipts_pre_merge", "receipts_distance", "receipts_before"])]
145 pub receipts_full: bool,
146 #[arg(long = "prune.receipts.pre-merge", conflicts_with_all = &["receipts_full", "receipts_distance", "receipts_before"])]
148 pub receipts_pre_merge: bool,
149 #[arg(long = "prune.receipts.distance", value_name = "BLOCKS", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_before"])]
151 pub receipts_distance: Option<u64>,
152 #[arg(long = "prune.receipts.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_distance"])]
154 pub receipts_before: Option<BlockNumber>,
155 #[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)]
160 pub receipts_log_filter: Option<ReceiptsLogPruneConfig>,
161
162 #[arg(long = "prune.account-history.full", alias = "prune.accounthistory.full", conflicts_with_all = &["account_history_distance", "account_history_before"])]
165 pub account_history_full: bool,
166 #[arg(long = "prune.account-history.distance", alias = "prune.accounthistory.distance", value_name = "BLOCKS", conflicts_with_all = &["account_history_full", "account_history_before"])]
168 pub account_history_distance: Option<u64>,
169 #[arg(long = "prune.account-history.before", alias = "prune.accounthistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["account_history_full", "account_history_distance"])]
172 pub account_history_before: Option<BlockNumber>,
173
174 #[arg(long = "prune.storage-history.full", alias = "prune.storagehistory.full", conflicts_with_all = &["storage_history_distance", "storage_history_before"])]
177 pub storage_history_full: bool,
178 #[arg(long = "prune.storage-history.distance", alias = "prune.storagehistory.distance", value_name = "BLOCKS", conflicts_with_all = &["storage_history_full", "storage_history_before"])]
181 pub storage_history_distance: Option<u64>,
182 #[arg(long = "prune.storage-history.before", alias = "prune.storagehistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["storage_history_full", "storage_history_distance"])]
185 pub storage_history_before: Option<BlockNumber>,
186
187 #[arg(long = "prune.bodies.pre-merge", value_name = "BLOCKS", conflicts_with_all = &["bodies_distance", "bodies_before"])]
190 pub bodies_pre_merge: bool,
191 #[arg(long = "prune.bodies.distance", value_name = "BLOCKS", conflicts_with_all = &["bodies_pre_merge", "bodies_before"])]
194 pub bodies_distance: Option<u64>,
195 #[arg(long = "prune.bodies.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["bodies_distance", "bodies_pre_merge"])]
198 pub bodies_before: Option<BlockNumber>,
199
200 #[arg(long = "prune.minimum-distance", value_name = "BLOCKS")]
203 pub minimum_distance: Option<u64>,
204}
205
206impl PruningArgs {
207 pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
212 where
213 ChainSpec: EthereumHardforks,
214 {
215 let mut config = PruneConfig::default();
217
218 if self.full {
220 let defaults = DefaultPruningValues::get_global();
221 let mut segments = defaults.full_prune_modes.clone();
222 if defaults.full_bodies_history_use_pre_merge {
223 segments.bodies_history = chain_spec
224 .ethereum_fork_activation(EthereumHardfork::Paris)
225 .block_number()
226 .map(PruneMode::Before);
227 }
228 config = PruneConfig {
229 block_interval: config.block_interval,
230 segments,
231 minimum_pruning_distance: config.minimum_pruning_distance,
232 }
233 }
234
235 if self.minimal {
237 config = PruneConfig {
238 block_interval: config.block_interval,
239 segments: DefaultPruningValues::get_global().minimal_prune_modes.clone(),
240 minimum_pruning_distance: config.minimum_pruning_distance,
241 }
242 }
243
244 if let Some(block_interval) = self.block_interval {
246 config.block_interval = block_interval as usize;
247 }
248 if let Some(distance) = self.minimum_distance {
249 config.minimum_pruning_distance = distance;
250 }
251 if let Some(mode) = self.sender_recovery_prune_mode() {
252 config.segments.sender_recovery = Some(mode);
253 }
254 if let Some(mode) = self.transaction_lookup_prune_mode() {
255 config.segments.transaction_lookup = Some(mode);
256 }
257 if let Some(mode) = self.receipts_prune_mode(chain_spec) {
258 config.segments.receipts = Some(mode);
259 }
260 if let Some(mode) = self.account_history_prune_mode() {
261 config.segments.account_history = Some(mode);
262 }
263 if let Some(mode) = self.bodies_prune_mode(chain_spec) {
264 config.segments.bodies_history = Some(mode);
265 }
266 if let Some(mode) = self.storage_history_prune_mode() {
267 config.segments.storage_history = Some(mode);
268 }
269 if let Some(receipt_logs) =
270 self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned()
271 {
272 config.segments.receipts_log_filter = receipt_logs;
273 config.segments.receipts.take();
276 }
277
278 config.is_default().not().then_some(config)
279 }
280
281 fn bodies_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
282 where
283 ChainSpec: EthereumHardforks,
284 {
285 if self.bodies_pre_merge {
286 chain_spec
287 .ethereum_fork_activation(EthereumHardfork::Paris)
288 .block_number()
289 .map(PruneMode::Before)
290 } else if let Some(distance) = self.bodies_distance {
291 Some(PruneMode::Distance(distance))
292 } else {
293 self.bodies_before.map(PruneMode::Before)
294 }
295 }
296
297 const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
298 if self.sender_recovery_full {
299 Some(PruneMode::Full)
300 } else if let Some(distance) = self.sender_recovery_distance {
301 Some(PruneMode::Distance(distance))
302 } else if let Some(block_number) = self.sender_recovery_before {
303 Some(PruneMode::Before(block_number))
304 } else {
305 None
306 }
307 }
308
309 const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
310 if self.transaction_lookup_full {
311 Some(PruneMode::Full)
312 } else if let Some(distance) = self.transaction_lookup_distance {
313 Some(PruneMode::Distance(distance))
314 } else if let Some(block_number) = self.transaction_lookup_before {
315 Some(PruneMode::Before(block_number))
316 } else {
317 None
318 }
319 }
320
321 fn receipts_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
322 where
323 ChainSpec: EthereumHardforks,
324 {
325 if self.receipts_pre_merge {
326 chain_spec
327 .ethereum_fork_activation(EthereumHardfork::Paris)
328 .block_number()
329 .map(PruneMode::Before)
330 } else if self.receipts_full {
331 Some(PruneMode::Full)
332 } else if let Some(distance) = self.receipts_distance {
333 Some(PruneMode::Distance(distance))
334 } else {
335 self.receipts_before.map(PruneMode::Before)
336 }
337 }
338
339 const fn account_history_prune_mode(&self) -> Option<PruneMode> {
340 if self.account_history_full {
341 Some(PruneMode::Full)
342 } else if let Some(distance) = self.account_history_distance {
343 Some(PruneMode::Distance(distance))
344 } else if let Some(block_number) = self.account_history_before {
345 Some(PruneMode::Before(block_number))
346 } else {
347 None
348 }
349 }
350
351 const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
352 if self.storage_history_full {
353 Some(PruneMode::Full)
354 } else if let Some(distance) = self.storage_history_distance {
355 Some(PruneMode::Distance(distance))
356 } else if let Some(block_number) = self.storage_history_before {
357 Some(PruneMode::Before(block_number))
358 } else {
359 None
360 }
361 }
362}
363
364pub(crate) fn parse_receipts_log_filter(
366 value: &str,
367) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
368 let mut config = BTreeMap::new();
369 let filters = value.split(',').map(str::trim);
371 for filter in filters {
372 let parts: Vec<&str> = filter.split(':').collect();
373 if parts.len() < 2 {
374 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
375 }
376 let address = parts[0]
378 .parse::<Address>()
379 .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
380
381 let prune_mode = match parts[1] {
383 "full" => PruneMode::Full,
384 s if s.starts_with("distance") => {
385 if parts.len() < 3 {
386 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
387 }
388 let distance =
389 parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
390 PruneMode::Distance(distance)
391 }
392 s if s.starts_with("before") => {
393 if parts.len() < 3 {
394 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
395 }
396 let block_number =
397 parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidBlockNumber)?;
398 PruneMode::Before(block_number)
399 }
400 _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
401 };
402 config.insert(address, prune_mode);
403 }
404 Ok(ReceiptsLogPruneConfig(config))
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use alloy_primitives::address;
411 use clap::Parser;
412
413 #[derive(Parser)]
415 struct CommandParser<T: Args> {
416 #[command(flatten)]
417 args: T,
418 }
419
420 #[test]
421 fn pruning_args_sanity_check() {
422 let args = CommandParser::<PruningArgs>::parse_from([
423 "reth",
424 "--prune.receiptslogfilter",
425 "0x0000000000000000000000000000000000000003:before:5000000",
426 ])
427 .args;
428 let mut config = ReceiptsLogPruneConfig::default();
429 config.0.insert(
430 address!("0x0000000000000000000000000000000000000003"),
431 PruneMode::Before(5000000),
432 );
433 assert_eq!(args.receipts_log_filter, Some(config));
434 }
435
436 #[test]
437 fn parse_receiptslogfilter() {
438 let default_args = PruningArgs::default();
439 let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
440 assert_eq!(args, default_args);
441 }
442
443 #[test]
444 fn test_parse_receipts_log_filter() {
445 let filter1 = "0x0000000000000000000000000000000000000001:full";
446 let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
447 let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
448 let filters = [filter1, filter2, filter3].join(",");
449
450 let result = parse_receipts_log_filter(&filters);
452 assert!(result.is_ok());
453 let config = result.unwrap();
454 assert_eq!(config.0.len(), 3);
455
456 let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
458 let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
459 let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
460
461 assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
462 assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
463 assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
464 }
465
466 #[test]
467 fn test_parse_receipts_log_filter_with_spaces() {
468 let filters = "0x0000000000000000000000000000000000000001:full, 0x0000000000000000000000000000000000000002:distance:1000";
470
471 let result = parse_receipts_log_filter(filters);
472 assert!(result.is_ok());
473 let config = result.unwrap();
474 assert_eq!(config.0.len(), 2);
475
476 let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
477 let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
478
479 assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
480 assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
481 }
482
483 #[test]
484 fn test_parse_receipts_log_filter_invalid_filter_format() {
485 let result = parse_receipts_log_filter("invalid_format");
486 assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
487 }
488
489 #[test]
490 fn test_parse_receipts_log_filter_invalid_address() {
491 let result = parse_receipts_log_filter("invalid_address:full");
492 assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
493 }
494
495 #[test]
496 fn test_parse_receipts_log_filter_invalid_prune_mode() {
497 let result =
498 parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
499 assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
500 }
501
502 #[test]
503 fn test_parse_receipts_log_filter_invalid_distance() {
504 let result = parse_receipts_log_filter(
505 "0x0000000000000000000000000000000000000000:distance:invalid_distance",
506 );
507 assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
508 }
509
510 #[test]
511 fn test_parse_receipts_log_filter_invalid_block_number() {
512 let result = parse_receipts_log_filter(
513 "0x0000000000000000000000000000000000000000:before:invalid_block",
514 );
515 assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
516 }
517}