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::{PruneMode, PruneModes, ReceiptsLogPruneConfig, MINIMUM_PRUNING_DISTANCE};
9use std::{collections::BTreeMap, ops::Not};
10
11#[derive(Debug, Clone, Args, PartialEq, Eq, Default)]
13#[command(next_help_heading = "Pruning")]
14pub struct PruningArgs {
15 #[arg(long, default_value_t = false)]
17 pub full: bool,
18
19 #[arg(long = "prune.block-interval", alias = "block-interval", value_parser = RangedU64ValueParser::<u64>::new().range(1..))]
21 pub block_interval: Option<u64>,
22
23 #[arg(long = "prune.sender-recovery.full", alias = "prune.senderrecovery.full", conflicts_with_all = &["sender_recovery_distance", "sender_recovery_before"])]
26 pub sender_recovery_full: bool,
27 #[arg(long = "prune.sender-recovery.distance", alias = "prune.senderrecovery.distance", value_name = "BLOCKS", conflicts_with_all = &["sender_recovery_full", "sender_recovery_before"])]
30 pub sender_recovery_distance: Option<u64>,
31 #[arg(long = "prune.sender-recovery.before", alias = "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 #[arg(long = "prune.transaction-lookup.full", alias = "prune.transactionlookup.full", conflicts_with_all = &["transaction_lookup_distance", "transaction_lookup_before"])]
39 pub transaction_lookup_full: bool,
40 #[arg(long = "prune.transaction-lookup.distance", alias = "prune.transactionlookup.distance", value_name = "BLOCKS", conflicts_with_all = &["transaction_lookup_full", "transaction_lookup_before"])]
43 pub transaction_lookup_distance: Option<u64>,
44 #[arg(long = "prune.transaction-lookup.before", alias = "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 #[arg(long = "prune.receipts.full", conflicts_with_all = &["receipts_pre_merge", "receipts_distance", "receipts_before"])]
52 pub receipts_full: bool,
53 #[arg(long = "prune.receipts.pre-merge", conflicts_with_all = &["receipts_full", "receipts_distance", "receipts_before"])]
55 pub receipts_pre_merge: bool,
56 #[arg(long = "prune.receipts.distance", value_name = "BLOCKS", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_before"])]
58 pub receipts_distance: Option<u64>,
59 #[arg(long = "prune.receipts.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_distance"])]
61 pub receipts_before: Option<BlockNumber>,
62 #[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)]
67 pub receipts_log_filter: Option<ReceiptsLogPruneConfig>,
68
69 #[arg(long = "prune.account-history.full", alias = "prune.accounthistory.full", conflicts_with_all = &["account_history_distance", "account_history_before"])]
72 pub account_history_full: bool,
73 #[arg(long = "prune.account-history.distance", alias = "prune.accounthistory.distance", value_name = "BLOCKS", conflicts_with_all = &["account_history_full", "account_history_before"])]
75 pub account_history_distance: Option<u64>,
76 #[arg(long = "prune.account-history.before", alias = "prune.accounthistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["account_history_full", "account_history_distance"])]
79 pub account_history_before: Option<BlockNumber>,
80
81 #[arg(long = "prune.storage-history.full", alias = "prune.storagehistory.full", conflicts_with_all = &["storage_history_distance", "storage_history_before"])]
84 pub storage_history_full: bool,
85 #[arg(long = "prune.storage-history.distance", alias = "prune.storagehistory.distance", value_name = "BLOCKS", conflicts_with_all = &["storage_history_full", "storage_history_before"])]
88 pub storage_history_distance: Option<u64>,
89 #[arg(long = "prune.storage-history.before", alias = "prune.storagehistory.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["storage_history_full", "storage_history_distance"])]
92 pub storage_history_before: Option<BlockNumber>,
93
94 #[arg(long = "prune.bodies.pre-merge", value_name = "BLOCKS", conflicts_with_all = &["bodies_distance", "bodies_before"])]
97 pub bodies_pre_merge: bool,
98 #[arg(long = "prune.bodies.distance", value_name = "BLOCKS", conflicts_with_all = &["bodies_pre_merge", "bodies_before"])]
101 pub bodies_distance: Option<u64>,
102 #[arg(long = "prune.bodies.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["bodies_distance", "bodies_pre_merge"])]
105 pub bodies_before: Option<BlockNumber>,
106}
107
108impl PruningArgs {
109 pub fn prune_config<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneConfig>
114 where
115 ChainSpec: EthereumHardforks,
116 {
117 let mut config = PruneConfig::default();
119
120 if self.full {
122 config = PruneConfig {
123 block_interval: config.block_interval,
124 segments: PruneModes {
125 sender_recovery: Some(PruneMode::Full),
126 transaction_lookup: None,
127 receipts: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
128 account_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
129 storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)),
130 bodies_history: chain_spec
131 .ethereum_fork_activation(EthereumHardfork::Paris)
132 .block_number()
133 .map(PruneMode::Before),
134 merkle_changesets: PruneMode::Distance(MINIMUM_PRUNING_DISTANCE),
135 receipts_log_filter: Default::default(),
136 },
137 }
138 }
139
140 if let Some(block_interval) = self.block_interval {
142 config.block_interval = block_interval as usize;
143 }
144 if let Some(mode) = self.sender_recovery_prune_mode() {
145 config.segments.sender_recovery = Some(mode);
146 }
147 if let Some(mode) = self.transaction_lookup_prune_mode() {
148 config.segments.transaction_lookup = Some(mode);
149 }
150 if let Some(mode) = self.receipts_prune_mode(chain_spec) {
151 config.segments.receipts = Some(mode);
152 }
153 if let Some(mode) = self.account_history_prune_mode() {
154 config.segments.account_history = Some(mode);
155 }
156 if let Some(mode) = self.bodies_prune_mode(chain_spec) {
157 config.segments.bodies_history = Some(mode);
158 }
159 if let Some(mode) = self.storage_history_prune_mode() {
160 config.segments.storage_history = Some(mode);
161 }
162 if let Some(receipt_logs) =
163 self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned()
164 {
165 config.segments.receipts_log_filter = receipt_logs;
166 config.segments.receipts.take();
169 }
170
171 config.is_default().not().then_some(config)
172 }
173
174 fn bodies_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
175 where
176 ChainSpec: EthereumHardforks,
177 {
178 if self.bodies_pre_merge {
179 chain_spec
180 .ethereum_fork_activation(EthereumHardfork::Paris)
181 .block_number()
182 .map(PruneMode::Before)
183 } else if let Some(distance) = self.bodies_distance {
184 Some(PruneMode::Distance(distance))
185 } else {
186 self.bodies_before.map(PruneMode::Before)
187 }
188 }
189
190 const fn sender_recovery_prune_mode(&self) -> Option<PruneMode> {
191 if self.sender_recovery_full {
192 Some(PruneMode::Full)
193 } else if let Some(distance) = self.sender_recovery_distance {
194 Some(PruneMode::Distance(distance))
195 } else if let Some(block_number) = self.sender_recovery_before {
196 Some(PruneMode::Before(block_number))
197 } else {
198 None
199 }
200 }
201
202 const fn transaction_lookup_prune_mode(&self) -> Option<PruneMode> {
203 if self.transaction_lookup_full {
204 Some(PruneMode::Full)
205 } else if let Some(distance) = self.transaction_lookup_distance {
206 Some(PruneMode::Distance(distance))
207 } else if let Some(block_number) = self.transaction_lookup_before {
208 Some(PruneMode::Before(block_number))
209 } else {
210 None
211 }
212 }
213
214 fn receipts_prune_mode<ChainSpec>(&self, chain_spec: &ChainSpec) -> Option<PruneMode>
215 where
216 ChainSpec: EthereumHardforks,
217 {
218 if self.receipts_pre_merge {
219 chain_spec
220 .ethereum_fork_activation(EthereumHardfork::Paris)
221 .block_number()
222 .map(PruneMode::Before)
223 } else if self.receipts_full {
224 Some(PruneMode::Full)
225 } else if let Some(distance) = self.receipts_distance {
226 Some(PruneMode::Distance(distance))
227 } else {
228 self.receipts_before.map(PruneMode::Before)
229 }
230 }
231
232 const fn account_history_prune_mode(&self) -> Option<PruneMode> {
233 if self.account_history_full {
234 Some(PruneMode::Full)
235 } else if let Some(distance) = self.account_history_distance {
236 Some(PruneMode::Distance(distance))
237 } else if let Some(block_number) = self.account_history_before {
238 Some(PruneMode::Before(block_number))
239 } else {
240 None
241 }
242 }
243
244 const fn storage_history_prune_mode(&self) -> Option<PruneMode> {
245 if self.storage_history_full {
246 Some(PruneMode::Full)
247 } else if let Some(distance) = self.storage_history_distance {
248 Some(PruneMode::Distance(distance))
249 } else if let Some(block_number) = self.storage_history_before {
250 Some(PruneMode::Before(block_number))
251 } else {
252 None
253 }
254 }
255}
256
257pub(crate) fn parse_receipts_log_filter(
259 value: &str,
260) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
261 let mut config = BTreeMap::new();
262 let filters = value.split(',');
264 for filter in filters {
265 let parts: Vec<&str> = filter.split(':').collect();
266 if parts.len() < 2 {
267 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
268 }
269 let address = parts[0]
271 .parse::<Address>()
272 .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?;
273
274 let prune_mode = match parts[1] {
276 "full" => PruneMode::Full,
277 s if s.starts_with("distance") => {
278 if parts.len() < 3 {
279 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
280 }
281 let distance =
282 parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidDistance)?;
283 PruneMode::Distance(distance)
284 }
285 s if s.starts_with("before") => {
286 if parts.len() < 3 {
287 return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string()));
288 }
289 let block_number =
290 parts[2].parse::<u64>().map_err(ReceiptsLogError::InvalidBlockNumber)?;
291 PruneMode::Before(block_number)
292 }
293 _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())),
294 };
295 config.insert(address, prune_mode);
296 }
297 Ok(ReceiptsLogPruneConfig(config))
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use alloy_primitives::address;
304 use clap::Parser;
305
306 #[derive(Parser)]
308 struct CommandParser<T: Args> {
309 #[command(flatten)]
310 args: T,
311 }
312
313 #[test]
314 fn pruning_args_sanity_check() {
315 let args = CommandParser::<PruningArgs>::parse_from([
316 "reth",
317 "--prune.receiptslogfilter",
318 "0x0000000000000000000000000000000000000003:before:5000000",
319 ])
320 .args;
321 let mut config = ReceiptsLogPruneConfig::default();
322 config.0.insert(
323 address!("0x0000000000000000000000000000000000000003"),
324 PruneMode::Before(5000000),
325 );
326 assert_eq!(args.receipts_log_filter, Some(config));
327 }
328
329 #[test]
330 fn parse_receiptslogfilter() {
331 let default_args = PruningArgs::default();
332 let args = CommandParser::<PruningArgs>::parse_from(["reth"]).args;
333 assert_eq!(args, default_args);
334 }
335
336 #[test]
337 fn test_parse_receipts_log_filter() {
338 let filter1 = "0x0000000000000000000000000000000000000001:full";
339 let filter2 = "0x0000000000000000000000000000000000000002:distance:1000";
340 let filter3 = "0x0000000000000000000000000000000000000003:before:5000000";
341 let filters = [filter1, filter2, filter3].join(",");
342
343 let result = parse_receipts_log_filter(&filters);
345 assert!(result.is_ok());
346 let config = result.unwrap();
347 assert_eq!(config.0.len(), 3);
348
349 let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
351 let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
352 let addr3: Address = "0x0000000000000000000000000000000000000003".parse().unwrap();
353
354 assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
355 assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
356 assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
357 }
358
359 #[test]
360 fn test_parse_receipts_log_filter_invalid_filter_format() {
361 let result = parse_receipts_log_filter("invalid_format");
362 assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_))));
363 }
364
365 #[test]
366 fn test_parse_receipts_log_filter_invalid_address() {
367 let result = parse_receipts_log_filter("invalid_address:full");
368 assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_))));
369 }
370
371 #[test]
372 fn test_parse_receipts_log_filter_invalid_prune_mode() {
373 let result =
374 parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode");
375 assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_))));
376 }
377
378 #[test]
379 fn test_parse_receipts_log_filter_invalid_distance() {
380 let result = parse_receipts_log_filter(
381 "0x0000000000000000000000000000000000000000:distance:invalid_distance",
382 );
383 assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_))));
384 }
385
386 #[test]
387 fn test_parse_receipts_log_filter_invalid_block_number() {
388 let result = parse_receipts_log_filter(
389 "0x0000000000000000000000000000000000000000:before:invalid_block",
390 );
391 assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_))));
392 }
393}