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_PRUNING_DISTANCE: u64 = 32 * 2 + 10_000;
13
14/// Type of history that can be pruned
15#[derive(Debug, Error, PartialEq, Eq, Clone)]
16pub enum UnwindTargetPrunedError {
17    /// The target block is beyond the history limit
18    #[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        /// The latest block number
21        latest_block: BlockNumber,
22        /// The target block number
23        target_block: BlockNumber,
24        /// The type of history that is beyond the limit
25        history_type: HistoryType,
26        /// The limit of the history
27        limit: u64,
28    },
29}
30
31#[derive(Debug, Display, Clone, PartialEq, Eq)]
32pub enum HistoryType {
33    /// Account history
34    AccountHistory,
35    /// Storage history
36    StorageHistory,
37}
38
39/// Default pruning mode for merkle changesets
40const fn default_merkle_changesets_mode() -> PruneMode {
41    PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)
42}
43
44/// Pruning configuration for every segment of the data that can be pruned.
45#[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    /// Sender Recovery pruning configuration.
50    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
51    pub sender_recovery: Option<PruneMode>,
52    /// Transaction Lookup pruning configuration.
53    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
54    pub transaction_lookup: Option<PruneMode>,
55    /// Receipts pruning configuration. This setting overrides `receipts_log_filter`
56    /// and offers improved performance.
57    #[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    /// Account History pruning configuration.
66    #[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    /// Storage History pruning configuration.
75    #[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    /// Bodies History pruning configuration.
84    #[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    /// Merkle Changesets pruning configuration for `AccountsTrieChangeSets` and
93    /// `StoragesTrieChangeSets`.
94    #[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    /// Receipts pruning configuration by retaining only those receipts that contain logs emitted
103    /// by the specified addresses, discarding others. This setting is overridden by `receipts`.
104    ///
105    /// The [`BlockNumber`](`crate::BlockNumber`) represents the starting block from which point
106    /// onwards the receipts are preserved.
107    #[cfg_attr(
108        any(test, feature = "serde"),
109        serde(skip_serializing_if = "ReceiptsLogPruneConfig::is_empty")
110    )]
111    pub receipts_log_filter: ReceiptsLogPruneConfig,
112}
113
114impl Default for PruneModes {
115    fn default() -> Self {
116        Self {
117            sender_recovery: None,
118            transaction_lookup: None,
119            receipts: None,
120            account_history: None,
121            storage_history: None,
122            bodies_history: None,
123            merkle_changesets: default_merkle_changesets_mode(),
124            receipts_log_filter: ReceiptsLogPruneConfig::default(),
125        }
126    }
127}
128
129impl PruneModes {
130    /// Sets pruning to all targets.
131    pub fn all() -> Self {
132        Self {
133            sender_recovery: Some(PruneMode::Full),
134            transaction_lookup: Some(PruneMode::Full),
135            receipts: Some(PruneMode::Full),
136            account_history: Some(PruneMode::Full),
137            storage_history: Some(PruneMode::Full),
138            bodies_history: Some(PruneMode::Full),
139            merkle_changesets: PruneMode::Full,
140            receipts_log_filter: Default::default(),
141        }
142    }
143
144    /// Returns whether there is any kind of receipt pruning configuration.
145    pub fn has_receipts_pruning(&self) -> bool {
146        self.receipts.is_some() || !self.receipts_log_filter.is_empty()
147    }
148
149    /// Returns an error if we can't unwind to the targeted block because the target block is
150    /// outside the range.
151    ///
152    /// This is only relevant for certain tables that are required by other stages
153    ///
154    /// See also <https://github.com/paradigmxyz/reth/issues/16579>
155    pub fn ensure_unwind_target_unpruned(
156        &self,
157        latest_block: u64,
158        target_block: u64,
159        checkpoints: &[(PruneSegment, PruneCheckpoint)],
160    ) -> Result<(), UnwindTargetPrunedError> {
161        let distance = latest_block.saturating_sub(target_block);
162        for (prune_mode, history_type, checkpoint) in &[
163            (
164                self.account_history,
165                HistoryType::AccountHistory,
166                checkpoints.iter().find(|(segment, _)| segment.is_account_history()),
167            ),
168            (
169                self.storage_history,
170                HistoryType::StorageHistory,
171                checkpoints.iter().find(|(segment, _)| segment.is_storage_history()),
172            ),
173        ] {
174            if let Some(PruneMode::Distance(limit)) = prune_mode {
175                // check if distance exceeds the configured limit
176                if distance > *limit {
177                    // but only if have haven't pruned the target yet, if we dont have a checkpoint
178                    // yet, it's fully unpruned yet
179                    let pruned_height = checkpoint
180                        .and_then(|checkpoint| checkpoint.1.block_number)
181                        .unwrap_or(latest_block);
182                    if pruned_height >= target_block {
183                        // we've pruned the target block already and can't unwind past it
184                        return Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
185                            latest_block,
186                            target_block,
187                            history_type: history_type.clone(),
188                            limit: *limit,
189                        })
190                    }
191                }
192            }
193        }
194        Ok(())
195    }
196}
197
198/// Deserializes [`PruneMode`] and validates that the value is not less than the const
199/// generic parameter `MIN_BLOCKS`. This parameter represents the number of blocks that needs to be
200/// left in database after the pruning.
201///
202/// 1. For [`PruneMode::Full`], it fails if `MIN_BLOCKS > 0`.
203/// 2. For [`PruneMode::Distance`], it fails if `distance < MIN_BLOCKS + 1`. `+ 1` is needed because
204///    `PruneMode::Distance(0)` means that we leave zero blocks from the latest, meaning we have one
205///    block in the database.
206#[cfg(any(test, feature = "serde"))]
207fn deserialize_prune_mode_with_min_blocks<
208    'de,
209    const MIN_BLOCKS: u64,
210    D: serde::Deserializer<'de>,
211>(
212    deserializer: D,
213) -> Result<PruneMode, D::Error> {
214    use serde::Deserialize;
215    let prune_mode = PruneMode::deserialize(deserializer)?;
216    serde_deserialize_validate::<MIN_BLOCKS, D>(&prune_mode)?;
217    Ok(prune_mode)
218}
219
220/// Deserializes [`Option<PruneMode>`] and validates that the value is not less than the const
221/// generic parameter `MIN_BLOCKS`. This parameter represents the number of blocks that needs to be
222/// left in database after the pruning.
223///
224/// 1. For [`PruneMode::Full`], it fails if `MIN_BLOCKS > 0`.
225/// 2. For [`PruneMode::Distance`], it fails if `distance < MIN_BLOCKS + 1`. `+ 1` is needed because
226///    `PruneMode::Distance(0)` means that we leave zero blocks from the latest, meaning we have one
227///    block in the database.
228#[cfg(any(test, feature = "serde"))]
229fn deserialize_opt_prune_mode_with_min_blocks<
230    'de,
231    const MIN_BLOCKS: u64,
232    D: serde::Deserializer<'de>,
233>(
234    deserializer: D,
235) -> Result<Option<PruneMode>, D::Error> {
236    use serde::Deserialize;
237    let prune_mode = Option::<PruneMode>::deserialize(deserializer)?;
238    if let Some(prune_mode) = prune_mode.as_ref() {
239        serde_deserialize_validate::<MIN_BLOCKS, D>(prune_mode)?;
240    }
241    Ok(prune_mode)
242}
243
244#[cfg(any(test, feature = "serde"))]
245fn serde_deserialize_validate<'a, 'de, const MIN_BLOCKS: u64, D: serde::Deserializer<'de>>(
246    prune_mode: &'a PruneMode,
247) -> Result<(), D::Error> {
248    use alloc::format;
249    match prune_mode {
250        PruneMode::Full if MIN_BLOCKS > 0 => {
251            Err(serde::de::Error::invalid_value(
252                serde::de::Unexpected::Str("full"),
253                // This message should have "expected" wording
254                &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
255                    .as_str(),
256            ))
257        }
258        PruneMode::Distance(distance) if *distance < MIN_BLOCKS => {
259            Err(serde::de::Error::invalid_value(
260                serde::de::Unexpected::Unsigned(*distance),
261                // This message should have "expected" wording
262                &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
263                    .as_str(),
264            ))
265        }
266        _ => Ok(()),
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use assert_matches::assert_matches;
274    use serde::Deserialize;
275
276    #[test]
277    fn test_deserialize_opt_prune_mode_with_min_blocks() {
278        #[derive(Debug, Deserialize, PartialEq, Eq)]
279        struct V(
280            #[serde(deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<10, _>")]
281            Option<PruneMode>,
282        );
283
284        assert!(serde_json::from_str::<V>(r#"{"distance": 10}"#).is_ok());
285        assert_matches!(
286            serde_json::from_str::<V>(r#"{"distance": 9}"#),
287            Err(err) if err.to_string() == "invalid value: integer `9`, expected prune mode that leaves at least 10 blocks in the database"
288        );
289
290        assert_matches!(
291            serde_json::from_str::<V>(r#""full""#),
292            Err(err) if err.to_string() == "invalid value: string \"full\", expected prune mode that leaves at least 10 blocks in the database"
293        );
294    }
295
296    #[test]
297    fn test_unwind_target_unpruned() {
298        // Test case 1: No pruning configured - should always succeed
299        let prune_modes = PruneModes::default();
300        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 500, &[]).is_ok());
301        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
302
303        // Test case 2: Distance pruning within limit - should succeed
304        let prune_modes = PruneModes {
305            account_history: Some(PruneMode::Distance(100)),
306            storage_history: Some(PruneMode::Distance(100)),
307            ..Default::default()
308        };
309        // Distance is 50, limit is 100 - OK
310        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 950, &[]).is_ok());
311
312        // Test case 3: Distance exceeds limit with no checkpoint
313        // NOTE: Current implementation assumes pruned_height = latest_block when no checkpoint
314        // exists This means it will fail because it assumes we've pruned up to block 1000 >
315        // target 800
316        let prune_modes =
317            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
318        // Distance is 200 > 100, no checkpoint - current impl treats as pruned up to latest_block
319        let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &[]);
320        assert_matches!(
321            result,
322            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
323                latest_block: 1000,
324                target_block: 800,
325                history_type: HistoryType::AccountHistory,
326                limit: 100
327            })
328        );
329
330        // Test case 4: Distance exceeds limit and target is pruned - should fail
331        let prune_modes =
332            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
333        let checkpoints = vec![(
334            PruneSegment::AccountHistory,
335            PruneCheckpoint {
336                block_number: Some(850),
337                tx_number: None,
338                prune_mode: PruneMode::Distance(100),
339            },
340        )];
341        // Distance is 200 > 100, and checkpoint shows we've pruned up to block 850 > target 800
342        let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints);
343        assert_matches!(
344            result,
345            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
346                latest_block: 1000,
347                target_block: 800,
348                history_type: HistoryType::AccountHistory,
349                limit: 100
350            })
351        );
352
353        // Test case 5: Storage history exceeds limit and is pruned - should fail
354        let prune_modes =
355            PruneModes { storage_history: Some(PruneMode::Distance(50)), ..Default::default() };
356        let checkpoints = vec![(
357            PruneSegment::StorageHistory,
358            PruneCheckpoint {
359                block_number: Some(960),
360                tx_number: None,
361                prune_mode: PruneMode::Distance(50),
362            },
363        )];
364        // Distance is 100 > 50, and checkpoint shows we've pruned up to block 960 > target 900
365        let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
366        assert_matches!(
367            result,
368            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
369                latest_block: 1000,
370                target_block: 900,
371                history_type: HistoryType::StorageHistory,
372                limit: 50
373            })
374        );
375
376        // Test case 6: Distance exceeds limit but target block not pruned yet - should succeed
377        let prune_modes =
378            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
379        let checkpoints = vec![(
380            PruneSegment::AccountHistory,
381            PruneCheckpoint {
382                block_number: Some(700),
383                tx_number: None,
384                prune_mode: PruneMode::Distance(100),
385            },
386        )];
387        // Distance is 200 > 100, but checkpoint shows we've only pruned up to block 700 < target
388        // 800
389        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints).is_ok());
390
391        // Test case 7: Both account and storage history configured, only one fails
392        let prune_modes = PruneModes {
393            account_history: Some(PruneMode::Distance(200)),
394            storage_history: Some(PruneMode::Distance(50)),
395            ..Default::default()
396        };
397        let checkpoints = vec![
398            (
399                PruneSegment::AccountHistory,
400                PruneCheckpoint {
401                    block_number: Some(700),
402                    tx_number: None,
403                    prune_mode: PruneMode::Distance(200),
404                },
405            ),
406            (
407                PruneSegment::StorageHistory,
408                PruneCheckpoint {
409                    block_number: Some(960),
410                    tx_number: None,
411                    prune_mode: PruneMode::Distance(50),
412                },
413            ),
414        ];
415        // For target 900: account history OK (distance 100 < 200), storage history fails (distance
416        // 100 > 50, pruned at 960)
417        let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
418        assert_matches!(
419            result,
420            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
421                latest_block: 1000,
422                target_block: 900,
423                history_type: HistoryType::StorageHistory,
424                limit: 50
425            })
426        );
427
428        // Test case 8: Edge case - exact boundary
429        let prune_modes =
430            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
431        let checkpoints = vec![(
432            PruneSegment::AccountHistory,
433            PruneCheckpoint {
434                block_number: Some(900),
435                tx_number: None,
436                prune_mode: PruneMode::Distance(100),
437            },
438        )];
439        // Distance is exactly 100, checkpoint at exactly the target block
440        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints).is_ok());
441
442        // Test case 9: Full pruning mode - should succeed (no distance check)
443        let prune_modes = PruneModes {
444            account_history: Some(PruneMode::Full),
445            storage_history: Some(PruneMode::Full),
446            ..Default::default()
447        };
448        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
449
450        // Test case 10: Edge case - saturating subtraction (target > latest)
451        let prune_modes =
452            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
453        // Target block (1500) > latest block (1000) - distance should be 0
454        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 1500, &[]).is_ok());
455    }
456}