1use alloy_primitives::BlockNumber;
2use derive_more::Display;
3use thiserror::Error;
4
5use crate::{PruneCheckpoint, PruneMode, PruneSegment, ReceiptsLogPruneConfig};
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
39#[derive(Debug, Clone, Default, Eq, PartialEq)]
41#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
42#[cfg_attr(any(test, feature = "serde"), serde(default))]
43pub struct PruneModes {
44 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
46 pub sender_recovery: Option<PruneMode>,
47 #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
49 pub transaction_lookup: Option<PruneMode>,
50 #[cfg_attr(
53 any(test, feature = "serde"),
54 serde(
55 skip_serializing_if = "Option::is_none",
56 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_PRUNING_DISTANCE, _>"
57 )
58 )]
59 pub receipts: Option<PruneMode>,
60 #[cfg_attr(
62 any(test, feature = "serde"),
63 serde(
64 skip_serializing_if = "Option::is_none",
65 deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_PRUNING_DISTANCE, _>"
66 )
67 )]
68 pub account_history: 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_PRUNING_DISTANCE, _>"
75 )
76 )]
77 pub storage_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_PRUNING_DISTANCE, _>"
84 )
85 )]
86 pub bodies_history: Option<PruneMode>,
87 pub receipts_log_filter: ReceiptsLogPruneConfig,
93}
94
95impl PruneModes {
96 pub fn none() -> Self {
98 Self::default()
99 }
100
101 pub fn all() -> Self {
103 Self {
104 sender_recovery: Some(PruneMode::Full),
105 transaction_lookup: Some(PruneMode::Full),
106 receipts: Some(PruneMode::Full),
107 account_history: Some(PruneMode::Full),
108 storage_history: Some(PruneMode::Full),
109 bodies_history: Some(PruneMode::Full),
110 receipts_log_filter: Default::default(),
111 }
112 }
113
114 pub fn has_receipts_pruning(&self) -> bool {
116 self.receipts.is_some() || !self.receipts_log_filter.is_empty()
117 }
118
119 pub fn is_empty(&self) -> bool {
121 self == &Self::none()
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 alloc::format;
190 use serde::Deserialize;
191 let prune_mode = Option::<PruneMode>::deserialize(deserializer)?;
192
193 match prune_mode {
194 Some(PruneMode::Full) if MIN_BLOCKS > 0 => {
195 Err(serde::de::Error::invalid_value(
196 serde::de::Unexpected::Str("full"),
197 &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
199 .as_str(),
200 ))
201 }
202 Some(PruneMode::Distance(distance)) if distance < MIN_BLOCKS => {
203 Err(serde::de::Error::invalid_value(
204 serde::de::Unexpected::Unsigned(distance),
205 &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
207 .as_str(),
208 ))
209 }
210 _ => Ok(prune_mode),
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use assert_matches::assert_matches;
218 use serde::Deserialize;
219
220 #[test]
221 fn test_deserialize_opt_prune_mode_with_min_blocks() {
222 #[derive(Debug, Deserialize, PartialEq, Eq)]
223 struct V(
224 #[serde(deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<10, _>")]
225 Option<PruneMode>,
226 );
227
228 assert!(serde_json::from_str::<V>(r#"{"distance": 10}"#).is_ok());
229 assert_matches!(
230 serde_json::from_str::<V>(r#"{"distance": 9}"#),
231 Err(err) if err.to_string() == "invalid value: integer `9`, expected prune mode that leaves at least 10 blocks in the database"
232 );
233
234 assert_matches!(
235 serde_json::from_str::<V>(r#""full""#),
236 Err(err) if err.to_string() == "invalid value: string \"full\", expected prune mode that leaves at least 10 blocks in the database"
237 );
238 }
239
240 #[test]
241 fn test_unwind_target_unpruned() {
242 let prune_modes = PruneModes::none();
244 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 500, &[]).is_ok());
245 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
246
247 let prune_modes = PruneModes {
249 account_history: Some(PruneMode::Distance(100)),
250 storage_history: Some(PruneMode::Distance(100)),
251 ..Default::default()
252 };
253 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 950, &[]).is_ok());
255
256 let prune_modes =
261 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
262 let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &[]);
264 assert_matches!(
265 result,
266 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
267 latest_block: 1000,
268 target_block: 800,
269 history_type: HistoryType::AccountHistory,
270 limit: 100
271 })
272 );
273
274 let prune_modes =
276 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
277 let checkpoints = vec![(
278 PruneSegment::AccountHistory,
279 PruneCheckpoint {
280 block_number: Some(850),
281 tx_number: None,
282 prune_mode: PruneMode::Distance(100),
283 },
284 )];
285 let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints);
287 assert_matches!(
288 result,
289 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
290 latest_block: 1000,
291 target_block: 800,
292 history_type: HistoryType::AccountHistory,
293 limit: 100
294 })
295 );
296
297 let prune_modes =
299 PruneModes { storage_history: Some(PruneMode::Distance(50)), ..Default::default() };
300 let checkpoints = vec![(
301 PruneSegment::StorageHistory,
302 PruneCheckpoint {
303 block_number: Some(960),
304 tx_number: None,
305 prune_mode: PruneMode::Distance(50),
306 },
307 )];
308 let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
310 assert_matches!(
311 result,
312 Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
313 latest_block: 1000,
314 target_block: 900,
315 history_type: HistoryType::StorageHistory,
316 limit: 50
317 })
318 );
319
320 let prune_modes =
322 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
323 let checkpoints = vec![(
324 PruneSegment::AccountHistory,
325 PruneCheckpoint {
326 block_number: Some(700),
327 tx_number: None,
328 prune_mode: PruneMode::Distance(100),
329 },
330 )];
331 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints).is_ok());
334
335 let prune_modes = PruneModes {
337 account_history: Some(PruneMode::Distance(200)),
338 storage_history: Some(PruneMode::Distance(50)),
339 ..Default::default()
340 };
341 let checkpoints = vec![
342 (
343 PruneSegment::AccountHistory,
344 PruneCheckpoint {
345 block_number: Some(700),
346 tx_number: None,
347 prune_mode: PruneMode::Distance(200),
348 },
349 ),
350 (
351 PruneSegment::StorageHistory,
352 PruneCheckpoint {
353 block_number: Some(960),
354 tx_number: None,
355 prune_mode: PruneMode::Distance(50),
356 },
357 ),
358 ];
359 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(900),
379 tx_number: None,
380 prune_mode: PruneMode::Distance(100),
381 },
382 )];
383 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints).is_ok());
385
386 let prune_modes = PruneModes {
388 account_history: Some(PruneMode::Full),
389 storage_history: Some(PruneMode::Full),
390 ..Default::default()
391 };
392 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
393
394 let prune_modes =
396 PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
397 assert!(prune_modes.ensure_unwind_target_unpruned(1000, 1500, &[]).is_ok());
399 }
400}