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
201impl PruningArgs {
202 pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
207 where
208 ChainSpec: EthereumHardforks,
209 {
210 let mut config = PruneConfig::default();
212
213 if self.full {
215 let defaults = DefaultPruningValues::get_global();
216 let mut segments = defaults.full_prune_modes.clone();
217 if defaults.full_bodies_history_use_pre_merge {
218 segments.bodies_history = chain_spec
219 .ethereum_fork_activation(EthereumHardfork::Paris)
220 .block_number()
221 .map(PruneMode::Before);
222 }
223 config = PruneConfig { block_interval: config.block_interval, segments }
224 }
225
226 if self.minimal {
228 config = PruneConfig {
229 block_interval: config.block_interval,
230 segments: DefaultPruningValues::get_global().minimal_prune_modes.clone(),
231 }
232 }
233
234 if let Some(block_interval) = self.block_interval {
236 config.block_interval = block_interval as usize;
237 }
238 if let Some(mode) = self.sender_recovery_prune_mode() {
239 config.segments.sender_recovery = Some(mode);
240 }
241 if let Some(mode) = self.transaction_lookup_prune_mode() {
242 config.segments.transaction_lookup = Some(mode);
243 }
244 if let Some(mode) = self.receipts_prune_mode(chain_spec) {
245 config.segments.receipts = Some(mode);
246 }
247 if let Some(mode) = self.account_history_prune_mode() {
248 config.segments.account_history = Some(mode);
249 }
250 if let Some(mode) = self.bodies_prune_mode(chain_spec) {
251 config.segments.bodies_history = Some(mode);
252 }
253 if let Some(mode) = self.storage_history_prune_mode() {
254 config.segments.storage_history = Some(mode);
255 }
256 if let Some(receipt_logs) =
257 self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned()
258 {
259 config.segments.receipts_log_filter = receipt_logs;
260 config.segments.receipts.take();
263 }
264
265 config.is_default().not().then_some(config)
266 }
267
268 fn bodies_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
269 where
270 ChainSpec: EthereumHardforks,
271 {
272 if self.bodies_pre_merge {
273 chain_spec
274 .ethereum_fork_activation(EthereumHardfork::Paris)
275 .block_number()
276 .map(PruneMode::Before)
277 } else if let Some(distance) = self.bodies_distance {
278 Some(PruneMode::Distance(distance))
279 } else {
280 self.bodies_before.map(PruneMode::Before)
281 }
282 }
283
284 const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
285 if self.sender_recovery_full {
286 Some(PruneMode::Full)
287 } else if let Some(distance) = self.sender_recovery_distance {
288 Some(PruneMode::Distance(distance))
289 } else if let Some(block_number) = self.sender_recovery_before {
290 Some(PruneMode::Before(block_number))
291 } else {
292 None
293 }
294 }
295
296 const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
297 if self.transaction_lookup_full {
298 Some(PruneMode::Full)
299 } else if let Some(distance) = self.transaction_lookup_distance {
300 Some(PruneMode::Distance(distance))
301 } else if let Some(block_number) = self.transaction_lookup_before {
302 Some(PruneMode::Before(block_number))
303 } else {
304 None
305 }
306 }
307
308 fn receipts_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
309 where
310 ChainSpec: EthereumHardforks,
311 {
312 if self.receipts_pre_merge {
313 chain_spec
314 .ethereum_fork_activation(EthereumHardfork::Paris)
315 .block_number()
316 .map(PruneMode::Before)
317 } else if self.receipts_full {
318 Some(PruneMode::Full)
319 } else if let Some(distance) = self.receipts_distance {
320 Some(PruneMode::Distance(distance))
321 } else {
322 self.receipts_before.map(PruneMode::Before)
323 }
324 }
325
326 const fn account_history_prune_mode(&self) -> Option<PruneMode> {
327 if self.account_history_full {
328 Some(PruneMode::Full)
329 } else if let Some(distance) = self.account_history_distance {
330 Some(PruneMode::Distance(distance))
331 } else if let Some(block_number) = self.account_history_before {
332 Some(PruneMode::Before(block_number))
333 } else {
334 None
335 }
336 }
337
338 const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
339 if self.storage_history_full {
340 Some(PruneMode::Full)
341 } else if let Some(distance) = self.storage_history_distance {
342 Some(PruneMode::Distance(distance))
343 } else if let Some(block_number) = self.storage_history_before {
344 Some(PruneMode::Before(block_number))
345 } else {
346 None
347 }
348 }
349}
350
351pub(crate) fn parse_receipts_log_filter(
353 value: &str,
354) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
355 let mut config = BTreeMap::new();
356 let filters = value.split(',');
358 for filter in filters {
359 let parts: Vec<&str> = filter.split(':').collect();
360 if parts.len() < 2 {
361 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
362 }
363 let address = parts[0]
365 .parse::<Address>()
366 .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
367
368 let prune_mode = match parts[1] {
370 "full" => PruneMode::Full,
371 s if s.starts_with("distance") => {
372 if parts.len() < 3 {
373 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
374 }
375 let distance =
376 parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
377 PruneMode::Distance(distance)
378 }
379 s if s.starts_with("before") => {
380 if parts.len() < 3 {
381 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
382 }
383 let block_number =
384 parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidBlockNumber)?;
385 PruneMode::Before(block_number)
386 }
387 _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
388 };
389 config.insert(address, prune_mode);
390 }
391 Ok(ReceiptsLogPruneConfig(config))
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use alloy_primitives::address;
398 use clap::Parser;
399
400 #[derive(Parser)]
402 struct CommandParser<T: Args> {
403 #[command(flatten)]
404 args: T,
405 }
406
407 #[test]
408 fn pruning_args_sanity_check() {
409 let args = CommandParser::<PruningArgs>::parse_from([
410 "reth",
411 "--prune.receiptslogfilter",
412 "0x0000000000000000000000000000000000000003:before:5000000",
413 ])
414 .args;
415 let mut config = ReceiptsLogPruneConfig::default();
416 config.0.insert(
417 address!("0x0000000000000000000000000000000000000003"),
418 PruneMode::Before(5000000),
419 );
420 assert_eq!(args.receipts_log_filter, Some(config));
421 }
422
423 #[test]
424 fn parse_receiptslogfilter() {
425 let default_args = PruningArgs::default();
426 let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
427 assert_eq!(args, default_args);
428 }
429
430 #[test]
431 fn test_parse_receipts_log_filter() {
432 let filter1 = "0x0000000000000000000000000000000000000001:full";
433 let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
434 let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
435 let filters = [filter1, filter2, filter3].join(",");
436
437 let result = parse_receipts_log_filter(&filters);
439 assert!(result.is_ok());
440 let config = result.unwrap();
441 assert_eq!(config.0.len(), 3);
442
443 let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
445 let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
446 let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
447
448 assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
449 assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
450 assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
451 }
452
453 #[test]
454 fn test_parse_receipts_log_filter_invalid_filter_format() {
455 let result = parse_receipts_log_filter("invalid_format");
456 assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
457 }
458
459 #[test]
460 fn test_parse_receipts_log_filter_invalid_address() {
461 let result = parse_receipts_log_filter("invalid_address:full");
462 assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
463 }
464
465 #[test]
466 fn test_parse_receipts_log_filter_invalid_prune_mode() {
467 let result =
468 parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
469 assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
470 }
471
472 #[test]
473 fn test_parse_receipts_log_filter_invalid_distance() {
474 let result = parse_receipts_log_filter(
475 "0x0000000000000000000000000000000000000000:distance:invalid_distance",
476 );
477 assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
478 }
479
480 #[test]
481 fn test_parse_receipts_log_filter_invalid_block_number() {
482 let result = parse_receipts_log_filter(
483 "0x0000000000000000000000000000000000000000:before:invalid_block",
484 );
485 assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
486 }
487}