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/// Pruning configuration for every segment of the data that can be pruned.
40#[derive(Debug, Clone, Default, Eq, PartialEq)]
41#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
42#[cfg_attr(any(test, feature = "serde"), serde(default))]
43pub struct PruneModes {
44    /// Sender Recovery pruning configuration.
45    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
46    pub sender_recovery: Option<PruneMode>,
47    /// Transaction Lookup pruning configuration.
48    #[cfg_attr(any(test, feature = "serde"), serde(skip_serializing_if = "Option::is_none"))]
49    pub transaction_lookup: Option<PruneMode>,
50    /// Receipts pruning configuration. This setting overrides `receipts_log_filter`
51    /// and offers improved performance.
52    #[cfg_attr(
53        any(test, feature = "serde"),
54        serde(
55            skip_serializing_if = "Option::is_none",
56            deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<MINIMUM_PRUNING_DISTANCE, _>"
57        )
58    )]
59    pub receipts: Option<PruneMode>,
60    /// Account History pruning configuration.
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 account_history: Option<PruneMode>,
69    /// Storage 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 storage_history: Option<PruneMode>,
78    /// Bodies 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 bodies_history: Option<PruneMode>,
87    /// Receipts pruning configuration by retaining only those receipts that contain logs emitted
88    /// by the specified addresses, discarding others. This setting is overridden by `receipts`.
89    ///
90    /// The [`BlockNumber`](`crate::BlockNumber`) represents the starting block from which point
91    /// onwards the receipts are preserved.
92    pub receipts_log_filter: ReceiptsLogPruneConfig,
93}
94
95impl PruneModes {
96    /// Sets pruning to no target.
97    pub fn none() -> Self {
98        Self::default()
99    }
100
101    /// Sets pruning to all targets.
102    pub fn all() -> Self {
103        Self {
104            sender_recovery: Some(PruneMode::Full),
105            transaction_lookup: Some(PruneMode::Full),
106            receipts: Some(PruneMode::Full),
107            account_history: Some(PruneMode::Full),
108            storage_history: Some(PruneMode::Full),
109            bodies_history: Some(PruneMode::Full),
110            receipts_log_filter: Default::default(),
111        }
112    }
113
114    /// Returns whether there is any kind of receipt pruning configuration.
115    pub fn has_receipts_pruning(&self) -> bool {
116        self.receipts.is_some() || !self.receipts_log_filter.is_empty()
117    }
118
119    /// Returns true if all prune modes are set to [`None`].
120    pub fn is_empty(&self) -> bool {
121        self == &Self::none()
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 have haven't pruned the target yet, if we dont 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 alloc::format;
190    use serde::Deserialize;
191    let prune_mode = Option::<PruneMode>::deserialize(deserializer)?;
192
193    match prune_mode {
194        Some(PruneMode::Full) if MIN_BLOCKS > 0 => {
195            Err(serde::de::Error::invalid_value(
196                serde::de::Unexpected::Str("full"),
197                // This message should have "expected" wording
198                &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
199                    .as_str(),
200            ))
201        }
202        Some(PruneMode::Distance(distance)) if distance < MIN_BLOCKS => {
203            Err(serde::de::Error::invalid_value(
204                serde::de::Unexpected::Unsigned(distance),
205                // This message should have "expected" wording
206                &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
207                    .as_str(),
208            ))
209        }
210        _ => Ok(prune_mode),
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use assert_matches::assert_matches;
218    use serde::Deserialize;
219
220    #[test]
221    fn test_deserialize_opt_prune_mode_with_min_blocks() {
222        #[derive(Debug, Deserialize, PartialEq, Eq)]
223        struct V(
224            #[serde(deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<10, _>")]
225            Option<PruneMode>,
226        );
227
228        assert!(serde_json::from_str::<V>(r#"{"distance": 10}"#).is_ok());
229        assert_matches!(
230            serde_json::from_str::<V>(r#"{"distance": 9}"#),
231            Err(err) if err.to_string() == "invalid value: integer `9`, expected prune mode that leaves at least 10 blocks in the database"
232        );
233
234        assert_matches!(
235            serde_json::from_str::<V>(r#""full""#),
236            Err(err) if err.to_string() == "invalid value: string \"full\", expected prune mode that leaves at least 10 blocks in the database"
237        );
238    }
239
240    #[test]
241    fn test_unwind_target_unpruned() {
242        // Test case 1: No pruning configured - should always succeed
243        let prune_modes = PruneModes::none();
244        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 500, &[]).is_ok());
245        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
246
247        // Test case 2: Distance pruning within limit - should succeed
248        let prune_modes = PruneModes {
249            account_history: Some(PruneMode::Distance(100)),
250            storage_history: Some(PruneMode::Distance(100)),
251            ..Default::default()
252        };
253        // Distance is 50, limit is 100 - OK
254        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 950, &[]).is_ok());
255
256        // Test case 3: Distance exceeds limit with no checkpoint
257        // NOTE: Current implementation assumes pruned_height = latest_block when no checkpoint
258        // exists This means it will fail because it assumes we've pruned up to block 1000 >
259        // target 800
260        let prune_modes =
261            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
262        // Distance is 200 > 100, no checkpoint - current impl treats as pruned up to latest_block
263        let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &[]);
264        assert_matches!(
265            result,
266            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
267                latest_block: 1000,
268                target_block: 800,
269                history_type: HistoryType::AccountHistory,
270                limit: 100
271            })
272        );
273
274        // Test case 4: Distance exceeds limit and target is pruned - should fail
275        let prune_modes =
276            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
277        let checkpoints = vec![(
278            PruneSegment::AccountHistory,
279            PruneCheckpoint {
280                block_number: Some(850),
281                tx_number: None,
282                prune_mode: PruneMode::Distance(100),
283            },
284        )];
285        // Distance is 200 > 100, and checkpoint shows we've pruned up to block 850 > target 800
286        let result = prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints);
287        assert_matches!(
288            result,
289            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
290                latest_block: 1000,
291                target_block: 800,
292                history_type: HistoryType::AccountHistory,
293                limit: 100
294            })
295        );
296
297        // Test case 5: Storage history exceeds limit and is pruned - should fail
298        let prune_modes =
299            PruneModes { storage_history: Some(PruneMode::Distance(50)), ..Default::default() };
300        let checkpoints = vec![(
301            PruneSegment::StorageHistory,
302            PruneCheckpoint {
303                block_number: Some(960),
304                tx_number: None,
305                prune_mode: PruneMode::Distance(50),
306            },
307        )];
308        // Distance is 100 > 50, and checkpoint shows we've pruned up to block 960 > target 900
309        let result = prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints);
310        assert_matches!(
311            result,
312            Err(UnwindTargetPrunedError::TargetBeyondHistoryLimit {
313                latest_block: 1000,
314                target_block: 900,
315                history_type: HistoryType::StorageHistory,
316                limit: 50
317            })
318        );
319
320        // Test case 6: Distance exceeds limit but target block not pruned yet - should succeed
321        let prune_modes =
322            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
323        let checkpoints = vec![(
324            PruneSegment::AccountHistory,
325            PruneCheckpoint {
326                block_number: Some(700),
327                tx_number: None,
328                prune_mode: PruneMode::Distance(100),
329            },
330        )];
331        // Distance is 200 > 100, but checkpoint shows we've only pruned up to block 700 < target
332        // 800
333        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 800, &checkpoints).is_ok());
334
335        // Test case 7: Both account and storage history configured, only one fails
336        let prune_modes = PruneModes {
337            account_history: Some(PruneMode::Distance(200)),
338            storage_history: Some(PruneMode::Distance(50)),
339            ..Default::default()
340        };
341        let checkpoints = vec![
342            (
343                PruneSegment::AccountHistory,
344                PruneCheckpoint {
345                    block_number: Some(700),
346                    tx_number: None,
347                    prune_mode: PruneMode::Distance(200),
348                },
349            ),
350            (
351                PruneSegment::StorageHistory,
352                PruneCheckpoint {
353                    block_number: Some(960),
354                    tx_number: None,
355                    prune_mode: PruneMode::Distance(50),
356                },
357            ),
358        ];
359        // For target 900: account history OK (distance 100 < 200), storage history fails (distance
360        // 100 > 50, pruned at 960)
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 8: Edge case - exact boundary
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(900),
379                tx_number: None,
380                prune_mode: PruneMode::Distance(100),
381            },
382        )];
383        // Distance is exactly 100, checkpoint at exactly the target block
384        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 900, &checkpoints).is_ok());
385
386        // Test case 9: Full pruning mode - should succeed (no distance check)
387        let prune_modes = PruneModes {
388            account_history: Some(PruneMode::Full),
389            storage_history: Some(PruneMode::Full),
390            ..Default::default()
391        };
392        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok());
393
394        // Test case 10: Edge case - saturating subtraction (target > latest)
395        let prune_modes =
396            PruneModes { account_history: Some(PruneMode::Distance(100)), ..Default::default() };
397        // Target block (1500) > latest block (1000) - distance should be 0
398        assert!(prune_modes.ensure_unwind_target_unpruned(1000, 1500, &[]).is_ok());
399    }
400}