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