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