reth_prune_types/
target.rs

1use alloy_primitives::BlockNumber;
2use derive_more::Display;
3use thiserror::Error;
4
5use crate::{PruneMode, 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 true if target block is within history limit
125    pub fn ensure_unwind_target_unpruned(
126        &self,
127        latest_block: u64,
128        target_block: u64,
129    ) -> Result<(), UnwindTargetPrunedError> {
130        let distance = latest_block.saturating_sub(target_block);
131        [
132            (self.account_history, HistoryType::AccountHistory),
133            (self.storage_history, HistoryType::StorageHistory),
134        ]
135        .iter()
136        .find_map(|(prune_mode, history_type)| {
137            if let Some(PruneMode::Distance(limit)) = prune_mode {
138                (distance > *limit).then_some(Err(
139                    UnwindTargetPrunedError::TargetBeyondHistoryLimit {
140                        latest_block,
141                        target_block,
142                        history_type: history_type.clone(),
143                        limit: *limit,
144                    },
145                ))
146            } else {
147                None
148            }
149        })
150        .unwrap_or(Ok(()))
151    }
152}
153
154/// Deserializes [`Option<PruneMode>`] and validates that the value is not less than the const
155/// generic parameter `MIN_BLOCKS`. This parameter represents the number of blocks that needs to be
156/// left in database after the pruning.
157///
158/// 1. For [`PruneMode::Full`], it fails if `MIN_BLOCKS > 0`.
159/// 2. For [`PruneMode::Distance`], it fails if `distance < MIN_BLOCKS + 1`. `+ 1` is needed because
160///    `PruneMode::Distance(0)` means that we leave zero blocks from the latest, meaning we have one
161///    block in the database.
162#[cfg(any(test, feature = "serde"))]
163fn deserialize_opt_prune_mode_with_min_blocks<
164    'de,
165    const MIN_BLOCKS: u64,
166    D: serde::Deserializer<'de>,
167>(
168    deserializer: D,
169) -> Result<Option<PruneMode>, D::Error> {
170    use alloc::format;
171    use serde::Deserialize;
172    let prune_mode = Option::<PruneMode>::deserialize(deserializer)?;
173
174    match prune_mode {
175        Some(PruneMode::Full) if MIN_BLOCKS > 0 => {
176            Err(serde::de::Error::invalid_value(
177                serde::de::Unexpected::Str("full"),
178                // This message should have "expected" wording
179                &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
180                    .as_str(),
181            ))
182        }
183        Some(PruneMode::Distance(distance)) if distance < MIN_BLOCKS => {
184            Err(serde::de::Error::invalid_value(
185                serde::de::Unexpected::Unsigned(distance),
186                // This message should have "expected" wording
187                &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database")
188                    .as_str(),
189            ))
190        }
191        _ => Ok(prune_mode),
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use assert_matches::assert_matches;
199    use serde::Deserialize;
200
201    #[test]
202    fn test_deserialize_opt_prune_mode_with_min_blocks() {
203        #[derive(Debug, Deserialize, PartialEq, Eq)]
204        struct V(
205            #[serde(deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::<10, _>")]
206            Option<PruneMode>,
207        );
208
209        assert!(serde_json::from_str::<V>(r#"{"distance": 10}"#).is_ok());
210        assert_matches!(
211            serde_json::from_str::<V>(r#"{"distance": 9}"#),
212            Err(err) if err.to_string() == "invalid value: integer `9`, expected prune mode that leaves at least 10 blocks in the database"
213        );
214
215        assert_matches!(
216            serde_json::from_str::<V>(r#""full""#),
217            Err(err) if err.to_string() == "invalid value: string \"full\", expected prune mode that leaves at least 10 blocks in the database"
218        );
219    }
220}