Skip to main content

reth_prune_types/
target.rs

1use alloy_primitives::BlockNumber;
2use derive_more::Display;
3use thiserror::Error;
4
5use crate::{PruneCheckpoint, PruneMode, PruneSegment, ReceiptsLogPruneConfig};
6
7/// Minimum distance from the tip necessary for the node to work correctly:
8/// 1. Minimum 2 epochs (32 blocks per epoch) required to handle any reorg according to the
9///    consensus protocol.
10/// 2. Another 10k blocks to have a room for maneuver in case when things go wrong and a manual
11///    unwind is required.
12pub const MINIMUM_UNWIND_SAFE_DISTANCE: u64 = 32 * 2 + 10_000;
13
14/// Minimum blocks to retain for receipts and bodies to ensure reorg safety.
15/// This prevents pruning data that may be needed when handling chain reorganizations,
16/// specifically when `canonical_block_by_hash` needs to reconstruct `ExecutedBlock` from disk.
17pub const MINIMUM_DISTANCE: u64 = 64;
18
19/// Type of history that can be pruned
20#[derive(Debug, Error, PartialEq, Eq, Clone)]
21pub enum UnwindTargetPrunedError {
22    /// The target block is beyond the history limit
23    #[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        /// The latest block number
26        latest_block: BlockNumber,
27        /// The target block number
28        target_block: BlockNumber,
29        /// The type of history that is beyond the limit
30        history_type: HistoryType,
31        /// The limit of the history
32        limit: u64,
33    },
34    /// The target block is below history that was already pruned
35    #[error("Cannot unwind to block {target_block} as {history_type} is pruned through block {pruned_block}")]
36    TargetBelowPrunedHistory {
37        /// The target block number
38        target_block: BlockNumber,
39        /// The type of history that was pruned
40        history_type: HistoryType,
41        /// The highest pruned block
42        pruned_block: BlockNumber,
43    },
44}
45
46#[derive(Debug, Display, Clone, PartialEq, Eq)]
47pub enum HistoryType {
48    /// Account history
49    AccountHistory,
50    /// Storage history
51    StorageHistory,
52}
53
54/// Pruning configuration for every segment of the data that can be pruned.
55#[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    /// Sender Recovery pruning configuration.
60    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
61    pub sender_recovery: Option<PruneMode>,
62    /// Transaction Lookup pruning configuration.
63    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
64    pub transaction_lookup: Option<PruneMode>,
65    /// Receipts pruning configuration. This setting overrides `receipts_log_filter`
66    /// and offers improved performance.
67    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
68    pub receipts: Option<PruneMode>,
69    /// Account History pruning configuration.
70    #[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    /// Storage History pruning configuration.
79    #[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    /// Bodies History pruning configuration.
88    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
89    pub bodies_history: Option<PruneMode>,
90    /// Receipts pruning configuration by retaining only those receipts that contain logs emitted
91    /// by the specified addresses, discarding others. This setting is overridden by `receipts`.
92    ///
93    /// The [`BlockNumber`](`crate::BlockNumber`) represents the starting block from which point
94    /// onwards the receipts are preserved.
95    #[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    /// Sets pruning to all targets.
104    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    /// Returns whether there is any kind of receipt pruning configuration.
117    pub fn has_receipts_pruning(&self) -> bool {
118        self.receipts.is_some() || !self.receipts_log_filter.is_empty()
119    }
120
121    /// Migrates deprecated prune mode values to their new defaults.
122    ///
123    /// Returns `true` if any migration was performed.
124    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    /// Returns an error if we can't unwind to the targeted block because the target block is
135    /// outside the range.
136    ///
137    /// This is only relevant for certain tables that are required by other stages
138    ///
139    /// See also <https://github.com/paradigmxyz/reth/issues/16579>
140    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                // check if distance exceeds the configured limit
161                if distance > *limit {
162                    // but only if we haven't pruned the target yet, if we don't have a checkpoint
163                    // yet, it's fully unpruned yet
164                    let pruned_height = checkpoint
165                        .and_then(|checkpoint| checkpoint.1.block_number)
166                        .unwrap_or(latest_block);
167                    if pruned_height >= target_block {
168                        // we've pruned the target block already and can't unwind past it
169                        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            // History pruned through a block can't be reverted below it, whatever the mode.
180            // Unwinding to the checkpoint itself only needs the changesets above it. The database
181            // pruner can stop inside a block and then records the block before it, so the block
182            // right above the checkpoint may be partly pruned.
183            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/// Deserializes [`Option<PruneMode>`] and validates that the value is not less than the const
198/// generic parameter `MIN_BLOCKS`. This parameter represents the number of blocks that needs to be
199/// left in database after the pruning.
200///
201/// 1. For [`PruneMode::Full`], it fails if `MIN_BLOCKS > 0`.
202/// 2. For [`PruneMode::Distance`], it fails if `distance < MIN_BLOCKS + 1`. `+ 1` is needed because
203///    `PruneMode::Distance(0)` means that we leave zero blocks from the latest, meaning we have one
204///    block in the database.
205#[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                // This message should have "expected" wording
231                &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                // This message should have "expected" wording
239                &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        // Test case 1: No pruning configured - should always succeed
294        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        // Test case 2: Distance pruning within limit - should succeed
299        let prune_modes = PruneModes {
300            account_history: Some(PruneMode::Distance(100)),
301            storage_history: Some(PruneMode::Distance(100)),
302            ..Default::default()
303        };
304        // Distance is 50, limit is 100 - OK
305        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 950, &[]).is_ok());
306
307        // Test case 3: Distance exceeds limit with no checkpoint
308        // NOTE: Current implementation assumes pruned_height = latest_block when no checkpoint
309        // exists This means it will fail because it assumes we've pruned up to block 1000 >
310        // target 800
311        let prune_modes =
312            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
313        // Distance is 200 > 100, no checkpoint - current impl treats as pruned up to latest_block
314        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        // Test case 4: Distance exceeds limit and target is pruned - should fail
326        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        // Distance is 200 > 100, and checkpoint shows we've pruned up to block 850 > target 800
337        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        // Test case 5: Storage history exceeds limit and is pruned - should fail
349        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        // Distance is 100 > 50, and checkpoint shows we've pruned up to block 960 > target 900
360        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        // Test case 6: Distance exceeds limit but target block not pruned yet - should succeed
372        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        // Distance is 200 > 100, but checkpoint shows we've only pruned up to block 700 < target
383        // 800
384        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints).is_ok());
385
386        // Test case 7: Both account and storage history configured, only one fails
387        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        // For target 900: account history OK (distance 100 < 200), storage history fails (distance
411        // 100 > 50, pruned at 960)
412        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        // Test case 8: Edge case - exact boundary
424        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        // Distance is exactly 100, checkpoint at exactly the target block
435        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints).is_ok());
436
437        // Test case 9: Full pruning mode - should succeed (no distance check)
438        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        // Test case 10: Edge case - saturating subtraction (target > latest)
446        let prune_modes =
447            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
448        // Target block (1500) > latest block (1000) - distance should be 0
449        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 1500, &[]).is_ok());
450    }
451}