1use alloy_primitives::BlockNumber;
2use derive_more::Display;
3use thiserror::Error;
4
5use crate::{PruneCheckpoint, PruneMode, PruneSegment};
6
7pub const MINIMUM_PRUNING_DISTANCE: u64 = 32 * 2 + 10_000;
13
14#[derive(Debug, Error, PartialEq, Eq, Clone)]
16pub enum UnwindTargetPrunedError {
17 #[error("Cannot unwind to block {target_block} as it is beyond the {history_type} limit. Latest block: {latest_block}, History limit: {limit}")]
19 TargetBeyondHistoryLimit {
20 latest_block: BlockNumber,
22 target_block: BlockNumber,
24 history_type: HistoryType,
26 limit: u64,
28 },
29}
30
31#[derive(Debug, Display, Clone, PartialEq, Eq)]
32pub enum HistoryType {
33 AccountHistory,
35 StorageHistory,
37}
38
39const fn default_merkle_changesets_mode() -> PruneMode {
41 PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)
42}
43
44#[derive(Debug, Clone, Eq, PartialEq)]
46#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
47#[cfg_attr(any(test, feature = "serde"), serde(default))]
48pub struct PruneModes {
49 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
51 pub sender_recovery: Option<PruneMode>,
52 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
54 pub transaction_lookup: Option<PruneMode>,
55 #[cfg_attr(
58 any(test, feature = "serde"),
59 serde(
60 skip_serializing_if = "Option::is_none",
61 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_PRUNING_DISTANCE, _>"
62 )
63 )]
64 pub receipts: Option<PruneMode>,
65 #[cfg_attr(
67 any(test, feature = "serde"),
68 serde(
69 skip_serializing_if = "Option::is_none",
70 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_PRUNING_DISTANCE, _>"
71 )
72 )]
73 pub account_history: Option<PruneMode>,
74 #[cfg_attr(
76 any(test, feature = "serde"),
77 serde(
78 skip_serializing_if = "Option::is_none",
79 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_PRUNING_DISTANCE, _>"
80 )
81 )]
82 pub storage_history: Option<PruneMode>,
83 #[cfg_attr(
85 any(test, feature = "serde"),
86 serde(
87 skip_serializing_if = "Option::is_none",
88 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_PRUNING_DISTANCE, _>"
89 )
90 )]
91 pub bodies_history: Option<PruneMode>,
92 #[cfg_attr(
95 any(test, feature = "serde"),
96 serde(
97 default = "default_merkle_changesets_mode",
98 deserialize_with = "deserialize_prune_mode_with_min_blocks::<MINIMUM_PRUNING_DISTANCE, _>"
99 )
100 )]
101 pub merkle_changesets: PruneMode,
102 #[deprecated]
104 #[cfg_attr(any(test, feature = "serde"), serde(skip))]
105 pub receipts_log_filter: (),
106}
107
108impl Default for PruneModes {
109 fn default() -> Self {
110 Self {
111 sender_recovery: None,
112 transaction_lookup: None,
113 receipts: None,
114 account_history: None,
115 storage_history: None,
116 bodies_history: None,
117 merkle_changesets: default_merkle_changesets_mode(),
118 #[expect(deprecated)]
119 receipts_log_filter: (),
120 }
121 }
122}
123
124impl PruneModes {
125 pub const fn all() -> Self {
127 Self {
128 sender_recovery: Some(PruneMode::Full),
129 transaction_lookup: Some(PruneMode::Full),
130 receipts: Some(PruneMode::Full),
131 account_history: Some(PruneMode::Full),
132 storage_history: Some(PruneMode::Full),
133 bodies_history: Some(PruneMode::Full),
134 merkle_changesets: PruneMode::Full,
135 #[expect(deprecated)]
136 receipts_log_filter: (),
137 }
138 }
139
140 pub const fn has_receipts_pruning(&self) -> bool {
142 self.receipts.is_some()
143 }
144
145 pub fn ensure_unwind_target_unpruned(
152 &self,
153 latest_block: u64,
154 target_block: u64,
155 checkpoints: &[(PruneSegment, PruneCheckpoint)],
156 ) -> Result<(), UnwindTargetPrunedError> {
157 let distance = latest_block.saturating_sub(target_block);
158 for (prune_mode, history_type, checkpoint) in &[
159 (
160 self.account_history,
161 HistoryType::AccountHistory,
162 checkpoints.iter().find(|(segment, _)| segment.is_account_history()),
163 ),
164 (
165 self.storage_history,
166 HistoryType::StorageHistory,
167 checkpoints.iter().find(|(segment, _)| segment.is_storage_history()),
168 ),
169 ] {
170 if let Some(PruneMode::Distance(limit)) = prune_mode {
171 if distance > *limit {
173 let pruned_height = checkpoint
176 .and_then(|checkpoint| checkpoint.1.block_number)
177 .unwrap_or(latest_block);
178 if pruned_height >= target_block {
179 return Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
181 latest_block,
182 target_block,
183 history_type: history_type.clone(),
184 limit: *limit,
185 })
186 }
187 }
188 }
189 }
190 Ok(())
191 }
192}
193
194#[cfg(any(test, feature = "serde"))]
203fn deserialize_prune_mode_with_min_blocks<
204 'de,
205 const MIN_BLOCKS: u64,
206 D: serde::Deserializer<'de>,
207>(
208 deserializer: D,
209) -> Result<PruneMode, D::Error> {
210 use serde::Deserialize;
211 let prune_mode = PruneMode::deserialize(deserializer)?;
212 serde_deserialize_validate::<MIN_BLOCKS, D>(&prune_mode)?;
213 Ok(prune_mode)
214}
215
216#[cfg(any(test, feature = "serde"))]
225fn deserialize_opt_prune_mode_with_min_blocks<
226 'de,
227 const MIN_BLOCKS: u64,
228 D: serde::Deserializer<'de>,
229>(
230 deserializer: D,
231) -> Result<Option<PruneMode>, D::Error> {
232 use serde::Deserialize;
233 let prune_mode = Option::<PruneMode>::deserialize(deserializer)?;
234 if let Some(prune_mode) = prune_mode.as_ref() {
235 serde_deserialize_validate::<MIN_BLOCKS, D>(prune_mode)?;
236 }
237 Ok(prune_mode)
238}
239
240#[cfg(any(test, feature = "serde"))]
241fn serde_deserialize_validate<'a, 'de, const MIN_BLOCKS: u64, D: serde::Deserializer<'de>>(
242 prune_mode: &'a PruneMode,
243) -> Result<(), D::Error> {
244 use alloc::format;
245 match prune_mode {
246 PruneMode::Full if MIN_BLOCKS > 0 => {
247 Err(serde::de::Error::invalid_value(
248 serde::de::Unexpected::Str("full"),
249 &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
251 .as_str(),
252 ))
253 }
254 PruneMode::Distance(distance) if *distance < MIN_BLOCKS => {
255 Err(serde::de::Error::invalid_value(
256 serde::de::Unexpected::Unsigned(*distance),
257 &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
259 .as_str(),
260 ))
261 }
262 _ => Ok(()),
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use assert_matches::assert_matches;
270 use serde::Deserialize;
271
272 #[test]
273 fn test_deserialize_opt_prune_mode_with_min_blocks() {
274 #[derive(Debug, Deserialize, PartialEq, Eq)]
275 struct V(
276 #[serde(deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<10, _>")]
277 Option<PruneMode>,
278 );
279
280 assert!(serde_json::from_str::<V>(r#"{"distance": 10}"#).is_ok());
281 assert_matches!(
282 serde_json::from_str::<V>(r#"{"distance": 9}"#),
283 Err(err) if err.to_string() == "invalid value: integer `9`, expected prune mode that leaves at least 10 blocks in the database"
284 );
285
286 assert_matches!(
287 serde_json::from_str::<V>(r#""full""#),
288 Err(err) if err.to_string() == "invalid value: string \"full\", expected prune mode that leaves at least 10 blocks in the database"
289 );
290 }
291
292 #[test]
293 fn test_unwind_target_unpruned() {
294 let prune_modes = PruneModes::default();
296 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 500, &[]).is_ok());
297 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
298
299 let prune_modes = PruneModes {
301 account_history: Some(PruneMode::Distance(100)),
302 storage_history: Some(PruneMode::Distance(100)),
303 ..Default::default()
304 };
305 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 950, &[]).is_ok());
307
308 let prune_modes =
313 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
314 let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &[]);
316 assert_matches!(
317 result,
318 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
319 latest_block: 1000,
320 target_block: 800,
321 history_type: HistoryType::AccountHistory,
322 limit: 100
323 })
324 );
325
326 let prune_modes =
328 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
329 let checkpoints = vec![(
330 PruneSegment::AccountHistory,
331 PruneCheckpoint {
332 block_number: Some(850),
333 tx_number: None,
334 prune_mode: PruneMode::Distance(100),
335 },
336 )];
337 let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints);
339 assert_matches!(
340 result,
341 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
342 latest_block: 1000,
343 target_block: 800,
344 history_type: HistoryType::AccountHistory,
345 limit: 100
346 })
347 );
348
349 let prune_modes =
351 PruneModes { storage_history: Some(PruneMode::Distance(50)), ..Default::default() };
352 let checkpoints = vec![(
353 PruneSegment::StorageHistory,
354 PruneCheckpoint {
355 block_number: Some(960),
356 tx_number: None,
357 prune_mode: PruneMode::Distance(50),
358 },
359 )];
360 let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
362 assert_matches!(
363 result,
364 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
365 latest_block: 1000,
366 target_block: 900,
367 history_type: HistoryType::StorageHistory,
368 limit: 50
369 })
370 );
371
372 let prune_modes =
374 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
375 let checkpoints = vec![(
376 PruneSegment::AccountHistory,
377 PruneCheckpoint {
378 block_number: Some(700),
379 tx_number: None,
380 prune_mode: PruneMode::Distance(100),
381 },
382 )];
383 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints).is_ok());
386
387 let prune_modes = PruneModes {
389 account_history: Some(PruneMode::Distance(200)),
390 storage_history: Some(PruneMode::Distance(50)),
391 ..Default::default()
392 };
393 let checkpoints = vec![
394 (
395 PruneSegment::AccountHistory,
396 PruneCheckpoint {
397 block_number: Some(700),
398 tx_number: None,
399 prune_mode: PruneMode::Distance(200),
400 },
401 ),
402 (
403 PruneSegment::StorageHistory,
404 PruneCheckpoint {
405 block_number: Some(960),
406 tx_number: None,
407 prune_mode: PruneMode::Distance(50),
408 },
409 ),
410 ];
411 let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
414 assert_matches!(
415 result,
416 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
417 latest_block: 1000,
418 target_block: 900,
419 history_type: HistoryType::StorageHistory,
420 limit: 50
421 })
422 );
423
424 let prune_modes =
426 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
427 let checkpoints = vec![(
428 PruneSegment::AccountHistory,
429 PruneCheckpoint {
430 block_number: Some(900),
431 tx_number: None,
432 prune_mode: PruneMode::Distance(100),
433 },
434 )];
435 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints).is_ok());
437
438 let prune_modes = PruneModes {
440 account_history: Some(PruneMode::Full),
441 storage_history: Some(PruneMode::Full),
442 ..Default::default()
443 };
444 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
445
446 let prune_modes =
448 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
449 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 1500, &[]).is_ok());
451 }
452}