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, Copy, PartialEq, Eq)]
96pub enum PruneConfigKind {
97 Archive,
99 Full,
101 Minimal,
103 Custom,
105}
106
107impl PruneConfigKind {
108 pub const fn as_str(self) -> &'static str {
110 match self {
111 Self::Archive => "archive",
112 Self::Full => "full",
113 Self::Minimal => "minimal",
114 Self::Custom => "custom",
115 }
116 }
117
118 pub fn from_config<ChainSpec>(config: &PruneConfig, chain_spec: &ChainSpec) -> Self
120 where
121 ChainSpec: EthereumHardforks,
122 {
123 if config.is_default() {
124 return Self::Archive
125 }
126
127 let full_config = PruningArgs { full: true, ..Default::default() }.prune_config(chain_spec);
128 if full_config.as_ref() == Some(config) {
129 return Self::Full
130 }
131
132 let minimal_config =
133 PruningArgs { minimal: true, ..Default::default() }.prune_config(chain_spec);
134 if minimal_config.as_ref() == Some(config) {
135 return Self::Minimal
136 }
137
138 Self::Custom
139 }
140}
141
142#[derive(Debug, Clone, Args, PartialEq, Eq, Default)]
144#[command(next_help_heading = "Pruning")]
145pub struct PruningArgs {
146 #[arg(long, default_value_t = false, conflicts_with = "minimal")]
149 pub full: bool,
150
151 #[arg(long, default_value_t = false, conflicts_with = "full")]
158 pub minimal: bool,
159
160 #[arg(long = "prune.block-interval", alias = "block-interval", value_parser = RangedU64ValueParser::<u64>::new().range(1..))]
162 pub block_interval: Option<u64>,
163
164 #[arg(long = "prune.sender-recovery.full", alias = "prune.senderrecovery.full", conflicts_with_all = &["sender_recovery_distance", "sender_recovery_before"])]
167 pub sender_recovery_full: bool,
168 #[arg(long = "prune.sender-recovery.distance", alias = "prune.senderrecovery.distance", value_name = "BLOCKS", conflicts_with_all = &["sender_recovery_full", "sender_recovery_before"])]
171 pub sender_recovery_distance: Option<u64>,
172 #[arg(long = "prune.sender-recovery.before", alias = "prune.senderrecovery.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["sender_recovery_full", "sender_recovery_distance"])]
175 pub sender_recovery_before: Option<BlockNumber>,
176
177 #[arg(long = "prune.transaction-lookup.full", alias = "prune.transactionlookup.full", conflicts_with_all = &["transaction_lookup_distance", "transaction_lookup_before"])]
180 pub transaction_lookup_full: bool,
181 #[arg(long = "prune.transaction-lookup.distance", alias = "prune.transactionlookup.distance", value_name = "BLOCKS", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_before"])]
184 pub transaction_lookup_distance: Option<u64>,
185 #[arg(long = "prune.transaction-lookup.before", alias = "prune.transactionlookup.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_distance"])]
188 pub transaction_lookup_before: Option<BlockNumber>,
189
190 #[arg(long = "prune.receipts.full", conflicts_with_all = &["receipts_pre_merge", "receipts_distance", "receipts_before"])]
193 pub receipts_full: bool,
194 #[arg(long = "prune.receipts.pre-merge", conflicts_with_all = &["receipts_full", "receipts_distance", "receipts_before"])]
196 pub receipts_pre_merge: bool,
197 #[arg(long = "prune.receipts.distance", value_name = "BLOCKS", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_before"])]
199 pub receipts_distance: Option<u64>,
200 #[arg(long = "prune.receipts.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_distance"])]
202 pub receipts_before: Option<BlockNumber>,
203 #[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)]
208 pub receipts_log_filter: Option<ReceiptsLogPruneConfig>,
209
210 #[arg(long = "prune.account-history.full", alias = "prune.accounthistory.full", conflicts_with_all = &["account_history_distance", "account_history_before"])]
213 pub account_history_full: bool,
214 #[arg(long = "prune.account-history.distance", alias = "prune.accounthistory.distance", value_name = "BLOCKS", conflicts_with_all = &["account_history_full", "account_history_before"])]
216 pub account_history_distance: Option<u64>,
217 #[arg(long = "prune.account-history.before", alias = "prune.accounthistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["account_history_full", "account_history_distance"])]
220 pub account_history_before: Option<BlockNumber>,
221
222 #[arg(long = "prune.storage-history.full", alias = "prune.storagehistory.full", conflicts_with_all = &["storage_history_distance", "storage_history_before"])]
225 pub storage_history_full: bool,
226 #[arg(long = "prune.storage-history.distance", alias = "prune.storagehistory.distance", value_name = "BLOCKS", conflicts_with_all = &["storage_history_full", "storage_history_before"])]
229 pub storage_history_distance: Option<u64>,
230 #[arg(long = "prune.storage-history.before", alias = "prune.storagehistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["storage_history_full", "storage_history_distance"])]
233 pub storage_history_before: Option<BlockNumber>,
234
235 #[arg(long = "prune.bodies.pre-merge", value_name = "BLOCKS", conflicts_with_all = &["bodies_distance", "bodies_before"])]
238 pub bodies_pre_merge: bool,
239 #[arg(long = "prune.bodies.distance", value_name = "BLOCKS", conflicts_with_all = &["bodies_pre_merge", "bodies_before"])]
242 pub bodies_distance: Option<u64>,
243 #[arg(long = "prune.bodies.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["bodies_distance", "bodies_pre_merge"])]
246 pub bodies_before: Option<BlockNumber>,
247
248 #[arg(long = "prune.minimum-distance", value_name = "BLOCKS")]
251 pub minimum_distance: Option<u64>,
252}
253
254impl PruningArgs {
255 pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
260 where
261 ChainSpec: EthereumHardforks,
262 {
263 let mut config = PruneConfig::default();
265
266 if self.full {
268 let defaults = DefaultPruningValues::get_global();
269 let mut segments = defaults.full_prune_modes.clone();
270 if defaults.full_bodies_history_use_pre_merge {
271 segments.bodies_history = chain_spec
272 .ethereum_fork_activation(EthereumHardfork::Paris)
273 .block_number()
274 .map(PruneMode::Before);
275 }
276 config = PruneConfig {
277 block_interval: config.block_interval,
278 segments,
279 minimum_pruning_distance: config.minimum_pruning_distance,
280 }
281 }
282
283 if self.minimal {
285 config = PruneConfig {
286 block_interval: config.block_interval,
287 segments: DefaultPruningValues::get_global().minimal_prune_modes.clone(),
288 minimum_pruning_distance: config.minimum_pruning_distance,
289 }
290 }
291
292 if let Some(block_interval) = self.block_interval {
294 config.block_interval = block_interval as usize;
295 }
296 if let Some(distance) = self.minimum_distance {
297 config.minimum_pruning_distance = distance;
298 }
299 if let Some(mode) = self.sender_recovery_prune_mode() {
300 config.segments.sender_recovery = Some(mode);
301 }
302 if let Some(mode) = self.transaction_lookup_prune_mode() {
303 config.segments.transaction_lookup = Some(mode);
304 }
305 if let Some(mode) = self.receipts_prune_mode(chain_spec) {
306 config.segments.receipts = Some(mode);
307 }
308 if let Some(mode) = self.account_history_prune_mode() {
309 config.segments.account_history = Some(mode);
310 }
311 if let Some(mode) = self.bodies_prune_mode(chain_spec) {
312 config.segments.bodies_history = Some(mode);
313 }
314 if let Some(mode) = self.storage_history_prune_mode() {
315 config.segments.storage_history = Some(mode);
316 }
317 if let Some(receipt_logs) =
318 self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned()
319 {
320 config.segments.receipts_log_filter = receipt_logs;
321 config.segments.receipts.take();
324 }
325
326 config.is_default().not().then_some(config)
327 }
328
329 fn bodies_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
330 where
331 ChainSpec: EthereumHardforks,
332 {
333 if self.bodies_pre_merge {
334 chain_spec
335 .ethereum_fork_activation(EthereumHardfork::Paris)
336 .block_number()
337 .map(PruneMode::Before)
338 } else if let Some(distance) = self.bodies_distance {
339 Some(PruneMode::Distance(distance))
340 } else {
341 self.bodies_before.map(PruneMode::Before)
342 }
343 }
344
345 const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
346 if self.sender_recovery_full {
347 Some(PruneMode::Full)
348 } else if let Some(distance) = self.sender_recovery_distance {
349 Some(PruneMode::Distance(distance))
350 } else if let Some(block_number) = self.sender_recovery_before {
351 Some(PruneMode::Before(block_number))
352 } else {
353 None
354 }
355 }
356
357 const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
358 if self.transaction_lookup_full {
359 Some(PruneMode::Full)
360 } else if let Some(distance) = self.transaction_lookup_distance {
361 Some(PruneMode::Distance(distance))
362 } else if let Some(block_number) = self.transaction_lookup_before {
363 Some(PruneMode::Before(block_number))
364 } else {
365 None
366 }
367 }
368
369 fn receipts_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
370 where
371 ChainSpec: EthereumHardforks,
372 {
373 if self.receipts_pre_merge {
374 chain_spec
375 .ethereum_fork_activation(EthereumHardfork::Paris)
376 .block_number()
377 .map(PruneMode::Before)
378 } else if self.receipts_full {
379 Some(PruneMode::Full)
380 } else if let Some(distance) = self.receipts_distance {
381 Some(PruneMode::Distance(distance))
382 } else {
383 self.receipts_before.map(PruneMode::Before)
384 }
385 }
386
387 const fn account_history_prune_mode(&self) -> Option<PruneMode> {
388 if self.account_history_full {
389 Some(PruneMode::Full)
390 } else if let Some(distance) = self.account_history_distance {
391 Some(PruneMode::Distance(distance))
392 } else if let Some(block_number) = self.account_history_before {
393 Some(PruneMode::Before(block_number))
394 } else {
395 None
396 }
397 }
398
399 const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
400 if self.storage_history_full {
401 Some(PruneMode::Full)
402 } else if let Some(distance) = self.storage_history_distance {
403 Some(PruneMode::Distance(distance))
404 } else if let Some(block_number) = self.storage_history_before {
405 Some(PruneMode::Before(block_number))
406 } else {
407 None
408 }
409 }
410}
411
412pub(crate) fn parse_receipts_log_filter(
414 value: &str,
415) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
416 let mut config = BTreeMap::new();
417 let filters = value.split(',').map(str::trim);
419 for filter in filters {
420 let parts: Vec<&str> = filter.split(':').collect();
421 if parts.len() < 2 {
422 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
423 }
424 let address = parts[0]
426 .parse::<Address>()
427 .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
428
429 let prune_mode = match parts[1] {
431 "full" => PruneMode::Full,
432 s if s.starts_with("distance") => {
433 if parts.len() < 3 {
434 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
435 }
436 let distance =
437 parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
438 PruneMode::Distance(distance)
439 }
440 s if s.starts_with("before") => {
441 if parts.len() < 3 {
442 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
443 }
444 let block_number =
445 parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidBlockNumber)?;
446 PruneMode::Before(block_number)
447 }
448 _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
449 };
450 config.insert(address, prune_mode);
451 }
452 Ok(ReceiptsLogPruneConfig(config))
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use alloy_primitives::address;
459 use clap::Parser;
460 use reth_chainspec::MAINNET;
461
462 #[derive(Parser)]
464 struct CommandParser<T: Args> {
465 #[command(flatten)]
466 args: T,
467 }
468
469 #[test]
470 fn pruning_args_sanity_check() {
471 let args = CommandParser::<PruningArgs>::parse_from([
472 "reth",
473 "--prune.receiptslogfilter",
474 "0x0000000000000000000000000000000000000003:before:5000000",
475 ])
476 .args;
477 let mut config = ReceiptsLogPruneConfig::default();
478 config.0.insert(
479 address!("0x0000000000000000000000000000000000000003"),
480 PruneMode::Before(5000000),
481 );
482 assert_eq!(args.receipts_log_filter, Some(config));
483 }
484
485 #[test]
486 fn pruning_config_kind_classifies_presets() {
487 let chain_spec = MAINNET.as_ref();
488
489 assert_eq!(
490 PruneConfigKind::from_config(&PruneConfig::default(), chain_spec),
491 PruneConfigKind::Archive
492 );
493
494 let full_config =
495 PruningArgs { full: true, ..Default::default() }.prune_config(chain_spec).unwrap();
496 assert_eq!(PruneConfigKind::from_config(&full_config, chain_spec), PruneConfigKind::Full);
497
498 let minimal_config =
499 PruningArgs { minimal: true, ..Default::default() }.prune_config(chain_spec).unwrap();
500 assert_eq!(
501 PruneConfigKind::from_config(&minimal_config, chain_spec),
502 PruneConfigKind::Minimal
503 );
504
505 let mut custom_config = full_config;
506 custom_config.block_interval += 1;
507 assert_eq!(
508 PruneConfigKind::from_config(&custom_config, chain_spec),
509 PruneConfigKind::Custom
510 );
511 }
512
513 #[test]
514 fn parse_receiptslogfilter() {
515 let default_args = PruningArgs::default();
516 let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
517 assert_eq!(args, default_args);
518 }
519
520 #[test]
521 fn test_parse_receipts_log_filter() {
522 let filter1 = "0x0000000000000000000000000000000000000001:full";
523 let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
524 let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
525 let filters = [filter1, filter2, filter3].join(",");
526
527 let result = parse_receipts_log_filter(&filters);
529 assert!(result.is_ok());
530 let config = result.unwrap();
531 assert_eq!(config.0.len(), 3);
532
533 let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
535 let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
536 let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
537
538 assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
539 assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
540 assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
541 }
542
543 #[test]
544 fn test_parse_receipts_log_filter_with_spaces() {
545 let filters = "0x0000000000000000000000000000000000000001:full, 0x0000000000000000000000000000000000000002:distance:1000";
547
548 let result = parse_receipts_log_filter(filters);
549 assert!(result.is_ok());
550 let config = result.unwrap();
551 assert_eq!(config.0.len(), 2);
552
553 let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
554 let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
555
556 assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
557 assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
558 }
559
560 #[test]
561 fn test_parse_receipts_log_filter_invalid_filter_format() {
562 let result = parse_receipts_log_filter("invalid_format");
563 assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
564 }
565
566 #[test]
567 fn test_parse_receipts_log_filter_invalid_address() {
568 let result = parse_receipts_log_filter("invalid_address:full");
569 assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
570 }
571
572 #[test]
573 fn test_parse_receipts_log_filter_invalid_prune_mode() {
574 let result =
575 parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
576 assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
577 }
578
579 #[test]
580 fn test_parse_receipts_log_filter_invalid_distance() {
581 let result = parse_receipts_log_filter(
582 "0x0000000000000000000000000000000000000000:distance:invalid_distance",
583 );
584 assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
585 }
586
587 #[test]
588 fn test_parse_receipts_log_filter_invalid_block_number() {
589 let result = parse_receipts_log_filter(
590 "0x0000000000000000000000000000000000000000:before:invalid_block",
591 );
592 assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
593 }
594}