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}
35
36#[derive(Debug, Display, Clone, PartialEq, Eq)]
37pub enum HistoryType {
38    /// Account history
39    AccountHistory,
40    /// Storage history
41    StorageHistory,
42}
43
44/// Pruning configuration for every segment of the data that can be pruned.
45#[derive(Debug, Clone, Eq, PartialEq, Default)]
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(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
58    pub receipts: Option<PruneMode>,
59    /// Account History pruning configuration.
60    #[cfg_attr(
61        any(test, feature = "serde"),
62        serde(
63            skip_serializing_if = "Option::is_none",
64            deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_UNWIND_SAFE_DISTANCE, _>"
65        )
66    )]
67    pub account_history: Option<PruneMode>,
68    /// Storage History pruning configuration.
69    #[cfg_attr(
70        any(test, feature = "serde"),
71        serde(
72            skip_serializing_if = "Option::is_none",
73            deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_UNWIND_SAFE_DISTANCE, _>"
74        )
75    )]
76    pub storage_history: Option<PruneMode>,
77    /// Bodies History pruning configuration.
78    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
79    pub bodies_history: Option<PruneMode>,
80    /// Receipts pruning configuration by retaining only those receipts that contain logs emitted
81    /// by the specified addresses, discarding others. This setting is overridden by `receipts`.
82    ///
83    /// The [`BlockNumber`](`crate::BlockNumber`) represents the starting block from which point
84    /// onwards the receipts are preserved.
85    #[cfg_attr(
86        any(test, feature = "serde"),
87        serde(skip_serializing_if = "ReceiptsLogPruneConfig::is_empty")
88    )]
89    pub receipts_log_filter: ReceiptsLogPruneConfig,
90}
91
92impl PruneModes {
93    /// Sets pruning to all targets.
94    pub fn all() -> Self {
95        Self {
96            sender_recovery: Some(PruneMode::Full),
97            transaction_lookup: Some(PruneMode::Full),
98            receipts: Some(PruneMode::Full),
99            account_history: Some(PruneMode::Full),
100            storage_history: Some(PruneMode::Full),
101            bodies_history: Some(PruneMode::Full),
102            receipts_log_filter: Default::default(),
103        }
104    }
105
106    /// Returns whether there is any kind of receipt pruning configuration.
107    pub fn has_receipts_pruning(&self) -> bool {
108        self.receipts.is_some() || !self.receipts_log_filter.is_empty()
109    }
110
111    /// Migrates deprecated prune mode values to their new defaults.
112    ///
113    /// Returns `true` if any migration was performed.
114    pub const fn migrate(&mut self) -> bool {
115        match &self.receipts {
116            Some(PruneMode::Full | PruneMode::Distance(0..MINIMUM_DISTANCE)) => {
117                self.receipts = Some(PruneMode::Distance(MINIMUM_DISTANCE));
118                true
119            }
120            _ => false,
121        }
122    }
123
124    /// Returns an error if we can't unwind to the targeted block because the target block is
125    /// outside the range.
126    ///
127    /// This is only relevant for certain tables that are required by other stages
128    ///
129    /// See also <https://github.com/paradigmxyz/reth/issues/16579>
130    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                // check if distance exceeds the configured limit
151                if distance > *limit {
152                    // but only if we haven't pruned the target yet, if we don't have a checkpoint
153                    // yet, it's fully unpruned yet
154                    let pruned_height = checkpoint
155                        .and_then(|checkpoint| checkpoint.1.block_number)
156                        .unwrap_or(latest_block);
157                    if pruned_height >= target_block {
158                        // we've pruned the target block already and can't unwind past it
159                        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/// Deserializes [`Option<PruneMode>`] and validates that the value is not less than the const
174/// generic parameter `MIN_BLOCKS`. This parameter represents the number of blocks that needs to be
175/// left in database after the pruning.
176///
177/// 1. For [`PruneMode::Full`], it fails if `MIN_BLOCKS > 0`.
178/// 2. For [`PruneMode::Distance`], it fails if `distance < MIN_BLOCKS + 1`. `+ 1` is needed because
179///    `PruneMode::Distance(0)` means that we leave zero blocks from the latest, meaning we have one
180///    block in the database.
181#[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 serde::Deserialize;
190    let prune_mode = Option::<PruneMode>::deserialize(deserializer)?;
191    if let Some(prune_mode) = prune_mode.as_ref() {
192        serde_deserialize_validate::<MIN_BLOCKS, D>(prune_mode)?;
193    }
194    Ok(prune_mode)
195}
196
197#[cfg(any(test, feature = "serde"))]
198fn serde_deserialize_validate<'a, 'de, const MIN_BLOCKS: u64, D: serde::Deserializer<'de>>(
199    prune_mode: &'a PruneMode,
200) -> Result<(), D::Error> {
201    use alloc::format;
202    match prune_mode {
203        PruneMode::Full if MIN_BLOCKS > 0 => {
204            Err(serde::de::Error::invalid_value(
205                serde::de::Unexpected::Str("full"),
206                // This message should have "expected" wording
207                &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
208                    .as_str(),
209            ))
210        }
211        PruneMode::Distance(distance) if *distance < MIN_BLOCKS => {
212            Err(serde::de::Error::invalid_value(
213                serde::de::Unexpected::Unsigned(*distance),
214                // This message should have "expected" wording
215                &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
216                    .as_str(),
217            ))
218        }
219        _ => Ok(()),
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use assert_matches::assert_matches;
227    use serde::Deserialize;
228
229    #[test]
230    fn test_deserialize_opt_prune_mode_with_min_blocks() {
231        #[derive(Debug, Deserialize, PartialEq, Eq)]
232        struct V(
233            #[serde(deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<10, _>")]
234            Option<PruneMode>,
235        );
236
237        assert!(serde_json::from_str::<V>(r#"{"distance": 10}"#).is_ok());
238        assert_matches!(
239            serde_json::from_str::<V>(r#"{"distance": 9}"#),
240            Err(err) if err.to_string() == "invalid value: integer `9`, expected prune mode that leaves at least 10 blocks in the database"
241        );
242
243        assert_matches!(
244            serde_json::from_str::<V>(r#""full""#),
245            Err(err) if err.to_string() == "invalid value: string \"full\", expected prune mode that leaves at least 10 blocks in the database"
246        );
247    }
248
249    #[test]
250    fn test_unwind_target_unpruned() {
251        // Test case 1: No pruning configured - should always succeed
252        let prune_modes = PruneModes::default();
253        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 500, &[]).is_ok());
254        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
255
256        // Test case 2: Distance pruning within limit - should succeed
257        let prune_modes = PruneModes {
258            account_history: Some(PruneMode::Distance(100)),
259            storage_history: Some(PruneMode::Distance(100)),
260            ..Default::default()
261        };
262        // Distance is 50, limit is 100 - OK
263        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 950, &[]).is_ok());
264
265        // Test case 3: Distance exceeds limit with no checkpoint
266        // NOTE: Current implementation assumes pruned_height = latest_block when no checkpoint
267        // exists This means it will fail because it assumes we've pruned up to block 1000 >
268        // target 800
269        let prune_modes =
270            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
271        // Distance is 200 > 100, no checkpoint - current impl treats as pruned up to latest_block
272        let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &[]);
273        assert_matches!(
274            result,
275            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
276                latest_block: 1000,
277                target_block: 800,
278                history_type: HistoryType::AccountHistory,
279                limit: 100
280            })
281        );
282
283        // Test case 4: Distance exceeds limit and target is pruned - should fail
284        let prune_modes =
285            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
286        let checkpoints = vec![(
287            PruneSegment::AccountHistory,
288            PruneCheckpoint {
289                block_number: Some(850),
290                tx_number: None,
291                prune_mode: PruneMode::Distance(100),
292            },
293        )];
294        // Distance is 200 > 100, and checkpoint shows we've pruned up to block 850 > target 800
295        let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints);
296        assert_matches!(
297            result,
298            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
299                latest_block: 1000,
300                target_block: 800,
301                history_type: HistoryType::AccountHistory,
302                limit: 100
303            })
304        );
305
306        // Test case 5: Storage history exceeds limit and is pruned - should fail
307        let prune_modes =
308            PruneModes { storage_history: Some(PruneMode::Distance(50)), ..Default::default() };
309        let checkpoints = vec![(
310            PruneSegment::StorageHistory,
311            PruneCheckpoint {
312                block_number: Some(960),
313                tx_number: None,
314                prune_mode: PruneMode::Distance(50),
315            },
316        )];
317        // Distance is 100 > 50, and checkpoint shows we've pruned up to block 960 > target 900
318        let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
319        assert_matches!(
320            result,
321            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
322                latest_block: 1000,
323                target_block: 900,
324                history_type: HistoryType::StorageHistory,
325                limit: 50
326            })
327        );
328
329        // Test case 6: Distance exceeds limit but target block not pruned yet - should succeed
330        let prune_modes =
331            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
332        let checkpoints = vec![(
333            PruneSegment::AccountHistory,
334            PruneCheckpoint {
335                block_number: Some(700),
336                tx_number: None,
337                prune_mode: PruneMode::Distance(100),
338            },
339        )];
340        // Distance is 200 > 100, but checkpoint shows we've only pruned up to block 700 < target
341        // 800
342        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints).is_ok());
343
344        // Test case 7: Both account and storage history configured, only one fails
345        let prune_modes = PruneModes {
346            account_history: Some(PruneMode::Distance(200)),
347            storage_history: Some(PruneMode::Distance(50)),
348            ..Default::default()
349        };
350        let checkpoints = vec![
351            (
352                PruneSegment::AccountHistory,
353                PruneCheckpoint {
354                    block_number: Some(700),
355                    tx_number: None,
356                    prune_mode: PruneMode::Distance(200),
357                },
358            ),
359            (
360                PruneSegment::StorageHistory,
361                PruneCheckpoint {
362                    block_number: Some(960),
363                    tx_number: None,
364                    prune_mode: PruneMode::Distance(50),
365                },
366            ),
367        ];
368        // For target 900: account history OK (distance 100 < 200), storage history fails (distance
369        // 100 > 50, pruned at 960)
370        let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
371        assert_matches!(
372            result,
373            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
374                latest_block: 1000,
375                target_block: 900,
376                history_type: HistoryType::StorageHistory,
377                limit: 50
378            })
379        );
380
381        // Test case 8: Edge case - exact boundary
382        let prune_modes =
383            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
384        let checkpoints = vec![(
385            PruneSegment::AccountHistory,
386            PruneCheckpoint {
387                block_number: Some(900),
388                tx_number: None,
389                prune_mode: PruneMode::Distance(100),
390            },
391        )];
392        // Distance is exactly 100, checkpoint at exactly the target block
393        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints).is_ok());
394
395        // Test case 9: Full pruning mode - should succeed (no distance check)
396        let prune_modes = PruneModes {
397            account_history: Some(PruneMode::Full),
398            storage_history: Some(PruneMode::Full),
399            ..Default::default()
400        };
401        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
402
403        // Test case 10: Edge case - saturating subtraction (target > latest)
404        let prune_modes =
405            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
406        // Target block (1500) > latest block (1000) - distance should be 0
407        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 1500, &[]).is_ok());
408    }
409}