reth_prune_types/
target.rs

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