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}
35
36#[derive(Debug, Display, Clone, PartialEq, Eq)]
37pub enum HistoryType {
38 AccountHistory,
40 StorageHistory,
42}
43
44#[derive(Debug, Clone, Eq, PartialEq, Default)]
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(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
58 pub receipts: Option<PruneMode>,
59 #[cfg_attr(
61 any(test, feature = "serde"),
62 serde(
63 skip_serializing_if = "Option::is_none",
64 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_UNWIND_SAFE_DISTANCE, _>"
65 )
66 )]
67 pub account_history: Option<PruneMode>,
68 #[cfg_attr(
70 any(test, feature = "serde"),
71 serde(
72 skip_serializing_if = "Option::is_none",
73 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_UNWIND_SAFE_DISTANCE, _>"
74 )
75 )]
76 pub storage_history: Option<PruneMode>,
77 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
79 pub bodies_history: Option<PruneMode>,
80 #[cfg_attr(
86 any(test, feature = "serde"),
87 serde(skip_serializing_if = "ReceiptsLogPruneConfig::is_empty")
88 )]
89 pub receipts_log_filter: ReceiptsLogPruneConfig,
90}
91
92impl PruneModes {
93 pub fn all() -> Self {
95 Self {
96 sender_recovery: Some(PruneMode::Full),
97 transaction_lookup: Some(PruneMode::Full),
98 receipts: Some(PruneMode::Full),
99 account_history: Some(PruneMode::Full),
100 storage_history: Some(PruneMode::Full),
101 bodies_history: Some(PruneMode::Full),
102 receipts_log_filter: Default::default(),
103 }
104 }
105
106 pub fn has_receipts_pruning(&self) -> bool {
108 self.receipts.is_some() || !self.receipts_log_filter.is_empty()
109 }
110
111 pub const fn migrate(&mut self) -> bool {
115 match &self.receipts {
116 Some(PruneMode::Full | PruneMode::Distance(0..MINIMUM_DISTANCE)) => {
117 self.receipts = Some(PruneMode::Distance(MINIMUM_DISTANCE));
118 true
119 }
120 _ => false,
121 }
122 }
123
124 pub fn ensure_unwind_target_unpruned(
131 &self,
132 latest_block: u64,
133 target_block: u64,
134 checkpoints: &[(PruneSegment, PruneCheckpoint)],
135 ) -> Result<(), UnwindTargetPrunedError> {
136 let distance = latest_block.saturating_sub(target_block);
137 for (prune_mode, history_type, checkpoint) in &[
138 (
139 self.account_history,
140 HistoryType::AccountHistory,
141 checkpoints.iter().find(|(segment, _)| segment.is_account_history()),
142 ),
143 (
144 self.storage_history,
145 HistoryType::StorageHistory,
146 checkpoints.iter().find(|(segment, _)| segment.is_storage_history()),
147 ),
148 ] {
149 if let Some(PruneMode::Distance(limit)) = prune_mode {
150 if distance > *limit {
152 let pruned_height = checkpoint
155 .and_then(|checkpoint| checkpoint.1.block_number)
156 .unwrap_or(latest_block);
157 if pruned_height >= target_block {
158 return Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
160 latest_block,
161 target_block,
162 history_type: history_type.clone(),
163 limit: *limit,
164 })
165 }
166 }
167 }
168 }
169 Ok(())
170 }
171}
172
173#[cfg(any(test, feature = "serde"))]
182fn deserialize_opt_prune_mode_with_min_blocks<
183 'de,
184 const MIN_BLOCKS: u64,
185 D: serde::Deserializer<'de>,
186>(
187 deserializer: D,
188) -> Result<Option<PruneMode>, D::Error> {
189 use serde::Deserialize;
190 let prune_mode = Option::<PruneMode>::deserialize(deserializer)?;
191 if let Some(prune_mode) = prune_mode.as_ref() {
192 serde_deserialize_validate::<MIN_BLOCKS, D>(prune_mode)?;
193 }
194 Ok(prune_mode)
195}
196
197#[cfg(any(test, feature = "serde"))]
198fn serde_deserialize_validate<'a, 'de, const MIN_BLOCKS: u64, D: serde::Deserializer<'de>>(
199 prune_mode: &'a PruneMode,
200) -> Result<(), D::Error> {
201 use alloc::format;
202 match prune_mode {
203 PruneMode::Full if MIN_BLOCKS > 0 => {
204 Err(serde::de::Error::invalid_value(
205 serde::de::Unexpected::Str("full"),
206 &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
208 .as_str(),
209 ))
210 }
211 PruneMode::Distance(distance) if *distance < MIN_BLOCKS => {
212 Err(serde::de::Error::invalid_value(
213 serde::de::Unexpected::Unsigned(*distance),
214 &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
216 .as_str(),
217 ))
218 }
219 _ => Ok(()),
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use assert_matches::assert_matches;
227 use serde::Deserialize;
228
229 #[test]
230 fn test_deserialize_opt_prune_mode_with_min_blocks() {
231 #[derive(Debug, Deserialize, PartialEq, Eq)]
232 struct V(
233 #[serde(deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<10, _>")]
234 Option<PruneMode>,
235 );
236
237 assert!(serde_json::from_str::<V>(r#"{"distance": 10}"#).is_ok());
238 assert_matches!(
239 serde_json::from_str::<V>(r#"{"distance": 9}"#),
240 Err(err) if err.to_string() == "invalid value: integer `9`, expected prune mode that leaves at least 10 blocks in the database"
241 );
242
243 assert_matches!(
244 serde_json::from_str::<V>(r#""full""#),
245 Err(err) if err.to_string() == "invalid value: string \"full\", expected prune mode that leaves at least 10 blocks in the database"
246 );
247 }
248
249 #[test]
250 fn test_unwind_target_unpruned() {
251 let prune_modes = PruneModes::default();
253 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 500, &[]).is_ok());
254 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
255
256 let prune_modes = PruneModes {
258 account_history: Some(PruneMode::Distance(100)),
259 storage_history: Some(PruneMode::Distance(100)),
260 ..Default::default()
261 };
262 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 950, &[]).is_ok());
264
265 let prune_modes =
270 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
271 let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &[]);
273 assert_matches!(
274 result,
275 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
276 latest_block: 1000,
277 target_block: 800,
278 history_type: HistoryType::AccountHistory,
279 limit: 100
280 })
281 );
282
283 let prune_modes =
285 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
286 let checkpoints = vec![(
287 PruneSegment::AccountHistory,
288 PruneCheckpoint {
289 block_number: Some(850),
290 tx_number: None,
291 prune_mode: PruneMode::Distance(100),
292 },
293 )];
294 let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints);
296 assert_matches!(
297 result,
298 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
299 latest_block: 1000,
300 target_block: 800,
301 history_type: HistoryType::AccountHistory,
302 limit: 100
303 })
304 );
305
306 let prune_modes =
308 PruneModes { storage_history: Some(PruneMode::Distance(50)), ..Default::default() };
309 let checkpoints = vec![(
310 PruneSegment::StorageHistory,
311 PruneCheckpoint {
312 block_number: Some(960),
313 tx_number: None,
314 prune_mode: PruneMode::Distance(50),
315 },
316 )];
317 let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
319 assert_matches!(
320 result,
321 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
322 latest_block: 1000,
323 target_block: 900,
324 history_type: HistoryType::StorageHistory,
325 limit: 50
326 })
327 );
328
329 let prune_modes =
331 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
332 let checkpoints = vec![(
333 PruneSegment::AccountHistory,
334 PruneCheckpoint {
335 block_number: Some(700),
336 tx_number: None,
337 prune_mode: PruneMode::Distance(100),
338 },
339 )];
340 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints).is_ok());
343
344 let prune_modes = PruneModes {
346 account_history: Some(PruneMode::Distance(200)),
347 storage_history: Some(PruneMode::Distance(50)),
348 ..Default::default()
349 };
350 let checkpoints = vec![
351 (
352 PruneSegment::AccountHistory,
353 PruneCheckpoint {
354 block_number: Some(700),
355 tx_number: None,
356 prune_mode: PruneMode::Distance(200),
357 },
358 ),
359 (
360 PruneSegment::StorageHistory,
361 PruneCheckpoint {
362 block_number: Some(960),
363 tx_number: None,
364 prune_mode: PruneMode::Distance(50),
365 },
366 ),
367 ];
368 let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
371 assert_matches!(
372 result,
373 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
374 latest_block: 1000,
375 target_block: 900,
376 history_type: HistoryType::StorageHistory,
377 limit: 50
378 })
379 );
380
381 let prune_modes =
383 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
384 let checkpoints = vec![(
385 PruneSegment::AccountHistory,
386 PruneCheckpoint {
387 block_number: Some(900),
388 tx_number: None,
389 prune_mode: PruneMode::Distance(100),
390 },
391 )];
392 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints).is_ok());
394
395 let prune_modes = PruneModes {
397 account_history: Some(PruneMode::Full),
398 storage_history: Some(PruneMode::Full),
399 ..Default::default()
400 };
401 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
402
403 let prune_modes =
405 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
406 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 1500, &[]).is_ok());
408 }
409}