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