Skip to main content

reth_node_core/args/
pruning.rs

1//! Pruning and full node arguments
2
3use 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
13/// Global static pruning defaults
14static PRUNING_DEFAULTS: OnceLock<DefaultPruningValues> = OnceLock::new();
15
16/// Default values for `--full` and `--minimal` pruning modes that can be customized.
17///
18/// Global defaults can be set via [`DefaultPruningValues::try_init`].
19#[derive(Debug, Clone)]
20pub struct DefaultPruningValues {
21    /// Prune modes for `--full` flag.
22    ///
23    /// Note: `bodies_history` is ignored when `full_bodies_history_use_pre_merge` is `true`.
24    pub full_prune_modes: PruneModes,
25    /// If `true`, `--full` will set `bodies_history` to prune everything before the merge block
26    /// (Paris hardfork). If `false`, uses `full_prune_modes.bodies_history` directly.
27    pub full_bodies_history_use_pre_merge: bool,
28    /// Prune modes for `--minimal` flag.
29    pub minimal_prune_modes: PruneModes,
30}
31
32impl DefaultPruningValues {
33    /// Initialize the global pruning defaults with this configuration.
34    ///
35    /// Returns `Err(self)` if already initialized.
36    pub fn try_init(self) -> Result<(), Self> {
37        PRUNING_DEFAULTS.set(self)
38    }
39
40    /// Get a reference to the global pruning defaults.
41    pub fn get_global() -> &'static Self {
42        PRUNING_DEFAULTS.get_or_init(Self::default)
43    }
44
45    /// Set the prune modes for `--full` flag.
46    pub fn with_full_prune_modes(mut self, modes: PruneModes) -> Self {
47        self.full_prune_modes = modes;
48        self
49    }
50
51    /// Set whether `--full` should use pre-merge pruning for bodies history.
52    ///
53    /// When `true` (default), bodies are pruned before the Paris hardfork block.
54    /// When `false`, uses `full_prune_modes.bodies_history` directly.
55    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    /// Set the prune modes for `--minimal` flag.
61    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                // This field is ignored when full_bodies_history_use_pre_merge is true
77                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/// High-level pruning configuration profile.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum PruneConfigKind {
97    /// Archive node with default pruning configuration.
98    Archive,
99    /// Full node pruning preset.
100    Full,
101    /// Minimal storage pruning preset.
102    Minimal,
103    /// Custom pruning configuration.
104    Custom,
105}
106
107impl PruneConfigKind {
108    /// Returns the string representation of the pruning profile.
109    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    /// Classifies an effective pruning configuration.
119    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/// Parameters for pruning and full node
143#[derive(Debug, Clone, Args, PartialEq, Eq, Default)]
144#[command(next_help_heading = "Pruning")]
145pub struct PruningArgs {
146    /// Run full node. Only the most recent [`MINIMUM_UNWIND_SAFE_DISTANCE`] block states are
147    /// stored.
148    #[arg(long, default_value_t = false, conflicts_with = "minimal")]
149    pub full: bool,
150
151    /// Run minimal storage mode with maximum pruning and smaller static files.
152    ///
153    /// This mode configures the node to use minimal disk space by:
154    /// - Fully pruning sender recovery, transaction lookup, receipts
155    /// - Leaving 10,064 blocks for account, storage history and block bodies
156    /// - Using 10,000 blocks per static file segment
157    #[arg(long, default_value_t = false, conflicts_with = "full")]
158    pub minimal: bool,
159
160    /// Minimum pruning interval measured in blocks.
161    #[arg(long = "prune.block-interval", alias = "block-interval", value_parser = RangedU64ValueParser::<u64>::new().range(1..))]
162    pub block_interval: Option<u64>,
163
164    // Sender Recovery
165    /// Prunes all sender recovery data.
166    #[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    /// Prune sender recovery data before the `head-N` block number. In other words, keep last N +
169    /// 1 blocks.
170    #[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    /// Prune sender recovery data before the specified block number. The specified block number is
173    /// not pruned.
174    #[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    // Transaction Lookup
178    /// Prunes all transaction lookup data.
179    #[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    /// Prune transaction lookup data before the `head-N` block number. In other words, keep last N
182    /// + 1 blocks.
183    #[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    /// Prune transaction lookup data before the specified block number. The specified block number
186    /// is not pruned.
187    #[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    // Receipts
191    /// Prunes all receipt data.
192    #[arg(long = "prune.receipts.full", conflicts_with_all = &["receipts_pre_merge", "receipts_distance", "receipts_before"])]
193    pub receipts_full: bool,
194    /// Prune receipts before the merge block.
195    #[arg(long = "prune.receipts.pre-merge", conflicts_with_all = &["receipts_full", "receipts_distance", "receipts_before"])]
196    pub receipts_pre_merge: bool,
197    /// Prune receipts before the `head-N` block number. In other words, keep last N + 1 blocks.
198    #[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    /// Prune receipts before the specified block number. The specified block number is not pruned.
201    #[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    // Receipts Log Filter
204    /// Configure receipts log filter. Format:
205    /// <`address`>:<`prune_mode`>... where <`prune_mode`> can be 'full', 'distance:<`blocks`>', or
206    /// 'before:<`block_number`>'
207    #[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    // Account History
211    /// Prunes all account history.
212    #[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    /// Prune account before the `head-N` block number. In other words, keep last N + 1 blocks.
215    #[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    /// Prune account history before the specified block number. The specified block number is not
218    /// pruned.
219    #[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    // Storage History
223    /// Prunes all storage history data.
224    #[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    /// Prune storage history before the `head-N` block number. In other words, keep last N + 1
227    /// blocks.
228    #[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    /// Prune storage history before the specified block number. The specified block number is not
231    /// pruned.
232    #[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    // Bodies
236    /// Prune bodies before the merge block.
237    #[arg(long = "prune.bodies.pre-merge", value_name = "BLOCKS", conflicts_with_all = &["bodies_distance", "bodies_before"])]
238    pub bodies_pre_merge: bool,
239    /// Prune bodies before the `head-N` block number. In other words, keep last N + 1
240    /// blocks.
241    #[arg(long = "prune.bodies.distance", value_name = "BLOCKS", conflicts_with_all = &["bodies_pre_merge", "bodies_before"])]
242    pub bodies_distance: Option<u64>,
243    /// Prune storage history before the specified block number. The specified block number is not
244    /// pruned.
245    #[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    /// Minimum pruning distance from the tip. This controls the safety margin for reorgs and
249    /// manual unwinds.
250    #[arg(long = "prune.minimum-distance", value_name = "BLOCKS")]
251    pub minimum_distance: Option<u64>,
252}
253
254impl PruningArgs {
255    /// Returns pruning configuration.
256    ///
257    /// Returns [`None`] if no parameters are specified and default pruning configuration should be
258    /// used.
259    pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
260    where
261        ChainSpec: EthereumHardforks,
262    {
263        // Initialize with a default prune configuration.
264        let mut config = PruneConfig::default();
265
266        // If --full is set, use full node defaults.
267        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 --minimal is set, use minimal storage mode with aggressive pruning.
284        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        // Override with any explicitly set prune.* flags.
293        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            // need to remove the receipts segment filter entirely because that takes precedence
322            // over the logs filter
323            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
412/// Parses `,` separated pruning info into [`ReceiptsLogPruneConfig`].
413pub(crate) fn parse_receipts_log_filter(
414    value: &str,
415) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
416    let mut config = BTreeMap::new();
417    // Split out each of the filters.
418    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        // Parse the address
425        let address = parts[0]
426            .parse::<Address>()
427            .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
428
429        // Parse the prune mode
430        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    /// A helper type to parse Args more easily
463    #[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        // Args can be parsed.
528        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        // Check that the args were parsed correctly.
534        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        // Verify that spaces after commas are handled correctly
546        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}