1use alloy_primitives::BlockNumber;
2use derive_more::Display;
3use thiserror::Error;
4
5use crate::{PruneCheckpoint, PruneMode, PruneSegment, ReceiptsLogPruneConfig};
6
7pub const MINIMUM_UNWIND_SAFE_DISTANCE: u64 = 32 * 2 + 10_000;
13
14pub const MINIMUM_DISTANCE: u64 = 64;
18
19#[derive(Debug, Error, PartialEq, Eq, Clone)]
21pub enum UnwindTargetPrunedError {
22 #[error("Cannot unwind to block {target_block} as it is beyond the {history_type} limit. Latest block: {latest_block}, History limit: {limit}")]
24 TargetBeyondHistoryLimit {
25 latest_block: BlockNumber,
27 target_block: BlockNumber,
29 history_type: HistoryType,
31 limit: u64,
33 },
34 #[error("Cannot unwind to block {target_block} as {history_type} is pruned through block {pruned_block}")]
36 TargetBelowPrunedHistory {
37 target_block: BlockNumber,
39 history_type: HistoryType,
41 pruned_block: BlockNumber,
43 },
44}
45
46#[derive(Debug, Display, Clone, PartialEq, Eq)]
47pub enum HistoryType {
48 AccountHistory,
50 StorageHistory,
52}
53
54#[derive(Debug, Clone, Eq, PartialEq, Default)]
56#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
57#[cfg_attr(any(test, feature = "serde"), serde(default))]
58pub struct PruneModes {
59 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
61 pub sender_recovery: Option<PruneMode>,
62 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
64 pub transaction_lookup: Option<PruneMode>,
65 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
68 pub receipts: Option<PruneMode>,
69 #[cfg_attr(
71 any(test, feature = "serde"),
72 serde(
73 skip_serializing_if = "Option::is_none",
74 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_UNWIND_SAFE_DISTANCE, _>"
75 )
76 )]
77 pub account_history: Option<PruneMode>,
78 #[cfg_attr(
80 any(test, feature = "serde"),
81 serde(
82 skip_serializing_if = "Option::is_none",
83 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_UNWIND_SAFE_DISTANCE, _>"
84 )
85 )]
86 pub storage_history: Option<PruneMode>,
87 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
89 pub bodies_history: Option<PruneMode>,
90 #[cfg_attr(
96 any(test, feature = "serde"),
97 serde(skip_serializing_if = "ReceiptsLogPruneConfig::is_empty")
98 )]
99 pub receipts_log_filter: ReceiptsLogPruneConfig,
100}
101
102impl PruneModes {
103 pub fn all() -> Self {
105 Self {
106 sender_recovery: Some(PruneMode::Full),
107 transaction_lookup: Some(PruneMode::Full),
108 receipts: Some(PruneMode::Full),
109 account_history: Some(PruneMode::Full),
110 storage_history: Some(PruneMode::Full),
111 bodies_history: Some(PruneMode::Full),
112 receipts_log_filter: Default::default(),
113 }
114 }
115
116 pub fn has_receipts_pruning(&self) -> bool {
118 self.receipts.is_some() || !self.receipts_log_filter.is_empty()
119 }
120
121 pub const fn migrate(&mut self) -> bool {
125 match &self.receipts {
126 Some(PruneMode::Full | PruneMode::Distance(0..MINIMUM_DISTANCE)) => {
127 self.receipts = Some(PruneMode::Distance(MINIMUM_DISTANCE));
128 true
129 }
130 _ => false,
131 }
132 }
133
134 pub fn ensure_unwind_target_unpruned(
141 &self,
142 latest_block: u64,
143 target_block: u64,
144 checkpoints: &[(PruneSegment, PruneCheckpoint)],
145 ) -> Result<(), UnwindTargetPrunedError> {
146 let distance = latest_block.saturating_sub(target_block);
147 for (prune_mode, history_type, checkpoint) in &[
148 (
149 self.account_history,
150 HistoryType::AccountHistory,
151 checkpoints.iter().find(|(segment, _)| segment.is_account_history()),
152 ),
153 (
154 self.storage_history,
155 HistoryType::StorageHistory,
156 checkpoints.iter().find(|(segment, _)| segment.is_storage_history()),
157 ),
158 ] {
159 if let Some(PruneMode::Distance(limit)) = prune_mode {
160 if distance > *limit {
162 let pruned_height = checkpoint
165 .and_then(|checkpoint| checkpoint.1.block_number)
166 .unwrap_or(latest_block);
167 if pruned_height >= target_block {
168 return Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
170 latest_block,
171 target_block,
172 history_type: history_type.clone(),
173 limit: *limit,
174 })
175 }
176 }
177 }
178
179 if let Some(pruned_block) = checkpoint.and_then(|checkpoint| checkpoint.1.block_number) &&
184 target_block < pruned_block
185 {
186 return Err(UnwindTargetPrunedError::TargetBelowPrunedHistory {
187 target_block,
188 history_type: history_type.clone(),
189 pruned_block,
190 })
191 }
192 }
193 Ok(())
194 }
195}
196
197#[cfg(any(test, feature = "serde"))]
206fn deserialize_opt_prune_mode_with_min_blocks<
207 'de,
208 const MIN_BLOCKS: u64,
209 D: serde::Deserializer<'de>,
210>(
211 deserializer: D,
212) -> Result<Option<PruneMode>, D::Error> {
213 use serde::Deserialize;
214 let prune_mode = Option::<PruneMode>::deserialize(deserializer)?;
215 if let Some(prune_mode) = prune_mode.as_ref() {
216 serde_deserialize_validate::<MIN_BLOCKS, D>(prune_mode)?;
217 }
218 Ok(prune_mode)
219}
220
221#[cfg(any(test, feature = "serde"))]
222fn serde_deserialize_validate<'a, 'de, const MIN_BLOCKS: u64, D: serde::Deserializer<'de>>(
223 prune_mode: &'a PruneMode,
224) -> Result<(), D::Error> {
225 use alloc::format;
226 match prune_mode {
227 PruneMode::Full if MIN_BLOCKS > 0 => {
228 Err(serde::de::Error::invalid_value(
229 serde::de::Unexpected::Str("full"),
230 &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
232 .as_str(),
233 ))
234 }
235 PruneMode::Distance(distance) if *distance < MIN_BLOCKS => {
236 Err(serde::de::Error::invalid_value(
237 serde::de::Unexpected::Unsigned(*distance),
238 &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
240 .as_str(),
241 ))
242 }
243 _ => Ok(()),
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use assert_matches::assert_matches;
251 use serde::Deserialize;
252
253 #[test]
254 fn test_deserialize_opt_prune_mode_with_min_blocks() {
255 #[derive(Debug, Deserialize, PartialEq, Eq)]
256 struct V(
257 #[serde(deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<10, _>")]
258 Option<PruneMode>,
259 );
260
261 assert!(serde_json::from_str::<V>(r#"{"distance": 10}"#).is_ok());
262 assert_matches!(
263 serde_json::from_str::<V>(r#"{"distance": 9}"#),
264 Err(err) if err.to_string() == "invalid value: integer `9`, expected prune mode that leaves at least 10 blocks in the database"
265 );
266
267 assert_matches!(
268 serde_json::from_str::<V>(r#""full""#),
269 Err(err) if err.to_string() == "invalid value: string \"full\", expected prune mode that leaves at least 10 blocks in the database"
270 );
271 }
272
273 #[test]
274 fn unwind_below_recorded_history_checkpoint() {
275 let checkpoints = [(
276 PruneSegment::AccountHistory,
277 PruneCheckpoint {
278 block_number: Some(499),
279 tx_number: None,
280 prune_mode: PruneMode::Before(500),
281 },
282 )];
283 let before =
284 PruneModes { account_history: Some(PruneMode::Before(500)), ..Default::default() };
285 for prune_modes in [before, PruneModes::default()] {
286 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 100, &checkpoints).is_err());
287 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 499, &checkpoints).is_ok());
288 }
289 }
290
291 #[test]
292 fn test_unwind_target_unpruned() {
293 let prune_modes = PruneModes::default();
295 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 500, &[]).is_ok());
296 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
297
298 let prune_modes = PruneModes {
300 account_history: Some(PruneMode::Distance(100)),
301 storage_history: Some(PruneMode::Distance(100)),
302 ..Default::default()
303 };
304 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 950, &[]).is_ok());
306
307 let prune_modes =
312 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
313 let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &[]);
315 assert_matches!(
316 result,
317 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
318 latest_block: 1000,
319 target_block: 800,
320 history_type: HistoryType::AccountHistory,
321 limit: 100
322 })
323 );
324
325 let prune_modes =
327 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
328 let checkpoints = vec![(
329 PruneSegment::AccountHistory,
330 PruneCheckpoint {
331 block_number: Some(850),
332 tx_number: None,
333 prune_mode: PruneMode::Distance(100),
334 },
335 )];
336 let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints);
338 assert_matches!(
339 result,
340 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
341 latest_block: 1000,
342 target_block: 800,
343 history_type: HistoryType::AccountHistory,
344 limit: 100
345 })
346 );
347
348 let prune_modes =
350 PruneModes { storage_history: Some(PruneMode::Distance(50)), ..Default::default() };
351 let checkpoints = vec![(
352 PruneSegment::StorageHistory,
353 PruneCheckpoint {
354 block_number: Some(960),
355 tx_number: None,
356 prune_mode: PruneMode::Distance(50),
357 },
358 )];
359 let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
361 assert_matches!(
362 result,
363 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
364 latest_block: 1000,
365 target_block: 900,
366 history_type: HistoryType::StorageHistory,
367 limit: 50
368 })
369 );
370
371 let prune_modes =
373 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
374 let checkpoints = vec![(
375 PruneSegment::AccountHistory,
376 PruneCheckpoint {
377 block_number: Some(700),
378 tx_number: None,
379 prune_mode: PruneMode::Distance(100),
380 },
381 )];
382 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints).is_ok());
385
386 let prune_modes = PruneModes {
388 account_history: Some(PruneMode::Distance(200)),
389 storage_history: Some(PruneMode::Distance(50)),
390 ..Default::default()
391 };
392 let checkpoints = vec![
393 (
394 PruneSegment::AccountHistory,
395 PruneCheckpoint {
396 block_number: Some(700),
397 tx_number: None,
398 prune_mode: PruneMode::Distance(200),
399 },
400 ),
401 (
402 PruneSegment::StorageHistory,
403 PruneCheckpoint {
404 block_number: Some(960),
405 tx_number: None,
406 prune_mode: PruneMode::Distance(50),
407 },
408 ),
409 ];
410 let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
413 assert_matches!(
414 result,
415 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
416 latest_block: 1000,
417 target_block: 900,
418 history_type: HistoryType::StorageHistory,
419 limit: 50
420 })
421 );
422
423 let prune_modes =
425 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
426 let checkpoints = vec![(
427 PruneSegment::AccountHistory,
428 PruneCheckpoint {
429 block_number: Some(900),
430 tx_number: None,
431 prune_mode: PruneMode::Distance(100),
432 },
433 )];
434 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints).is_ok());
436
437 let prune_modes = PruneModes {
439 account_history: Some(PruneMode::Full),
440 storage_history: Some(PruneMode::Full),
441 ..Default::default()
442 };
443 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
444
445 let prune_modes =
447 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
448 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 1500, &[]).is_ok());
450 }
451}