Skip to main content

reth_prune/segments/
mod.rs

1mod receipts;
2mod set;
3mod user;
4
5use crate::{PruneLimiter, PrunerError};
6use alloy_primitives::{BlockNumber, TxNumber};
7use reth_provider::{
8    errors::provider::ProviderResult, BlockReader, PruneCheckpointWriter, StaticFileProviderFactory,
9};
10use reth_prune_types::{
11    PruneCheckpoint, PruneMode, PruneProgress, PrunePurpose, PruneSegment, SegmentOutput,
12    SegmentOutputCheckpoint,
13};
14use reth_stages_types::StageId;
15use reth_static_file_types::StaticFileSegment;
16pub use set::SegmentSet;
17use std::{fmt::Debug, ops::RangeInclusive};
18use tracing::{debug, error};
19pub use user::{
20    AccountHistory, Bodies, Receipts as UserReceipts, ReceiptsByLogs, SenderRecovery,
21    StorageHistory, TransactionLookup,
22};
23
24/// Prunes data from static files for a given segment.
25///
26/// This is a generic helper function used by both receipts and bodies pruning
27/// when data is stored in static files.
28///
29/// The checkpoint block number is set to the highest block in the actually deleted files,
30/// not `input.to_block`, since `to_block` might refer to a block in the middle of an
31/// undeleted file.
32pub(crate) fn prune_static_files<Provider>(
33    provider: &Provider,
34    input: PruneInput,
35    segment: StaticFileSegment,
36) -> Result<SegmentOutput, PrunerError>
37where
38    Provider: StaticFileProviderFactory,
39{
40    let deleted_headers =
41        provider.static_file_provider().delete_segment_below_block(segment, input.to_block + 1)?;
42
43    if deleted_headers.is_empty() {
44        return Ok(SegmentOutput {
45            progress: PruneProgress::Finished,
46            pruned: 0,
47            checkpoint: input
48                .previous_checkpoint
49                .map(SegmentOutputCheckpoint::from_prune_checkpoint),
50        })
51    }
52
53    let tx_ranges = deleted_headers.iter().filter_map(|header| header.tx_range());
54
55    let pruned = tx_ranges.clone().map(|range| range.len()).sum::<u64>() as usize;
56
57    // The highest block number in the deleted files is the actual checkpoint.
58    let checkpoint_block = deleted_headers
59        .iter()
60        .filter_map(|header| header.block_range())
61        .map(|range| range.end())
62        .max();
63
64    Ok(SegmentOutput {
65        progress: PruneProgress::Finished,
66        pruned,
67        checkpoint: Some(SegmentOutputCheckpoint {
68            block_number: checkpoint_block,
69            tx_number: tx_ranges.map(|range| range.end()).max(),
70        }),
71    })
72}
73
74/// Deletes ALL static file jars for a given segment.
75///
76/// This is used for `PruneMode::Full` where all data should be removed, including the highest jar.
77/// Unlike [`prune_static_files`], this does not preserve the most recent jar.
78pub(crate) fn delete_static_files_segment<Provider>(
79    provider: &Provider,
80    input: PruneInput,
81    segment: StaticFileSegment,
82) -> Result<SegmentOutput, PrunerError>
83where
84    Provider: StaticFileProviderFactory,
85{
86    let deleted_headers = provider.static_file_provider().delete_segment(segment)?;
87
88    if deleted_headers.is_empty() {
89        return Ok(SegmentOutput::done())
90    }
91
92    let tx_ranges = deleted_headers.iter().filter_map(|header| header.tx_range());
93
94    let pruned = tx_ranges.clone().map(|range| range.len()).sum::<u64>() as usize;
95
96    Ok(SegmentOutput {
97        progress: PruneProgress::Finished,
98        pruned,
99        checkpoint: Some(SegmentOutputCheckpoint {
100            block_number: Some(input.to_block),
101            tx_number: tx_ranges.map(|range| range.end()).max(),
102        }),
103    })
104}
105
106/// A segment represents a pruning of some portion of the data.
107///
108/// Segments are called from [`Pruner`](crate::Pruner) with the following lifecycle:
109/// 1. Call [`Segment::prune`] with `delete_limit` of [`PruneInput`].
110/// 2. If [`Segment::prune`] returned a [`Some`] in `checkpoint` of [`SegmentOutput`], call
111///    [`Segment::save_checkpoint`].
112/// 3. Subtract `pruned` of [`SegmentOutput`] from `delete_limit` of next [`PruneInput`].
113pub trait Segment<Provider>: Debug + Send + Sync {
114    /// Segment of data that's pruned.
115    fn segment(&self) -> PruneSegment;
116
117    /// Prune mode with which the segment was initialized.
118    fn mode(&self) -> Option<PruneMode>;
119
120    /// Purpose of the segment.
121    fn purpose(&self) -> PrunePurpose;
122
123    /// Prune data for [`Self::segment`] using the provided input.
124    fn prune(&self, provider: &Provider, input: PruneInput) -> Result<SegmentOutput, PrunerError>;
125
126    /// Save checkpoint for [`Self::segment`] to the database.
127    fn save_checkpoint(
128        &self,
129        provider: &Provider,
130        checkpoint: PruneCheckpoint,
131    ) -> ProviderResult<()>
132    where
133        Provider: PruneCheckpointWriter,
134    {
135        provider.save_prune_checkpoint(self.segment(), checkpoint)
136    }
137
138    /// Returns the stage this segment depends on, if any.
139    ///
140    /// If this returns `Some(stage_id)`, the pruner will skip this segment if the stage
141    /// has not yet caught up with the `Finish` stage checkpoint.
142    fn required_stage(&self) -> Option<StageId> {
143        None
144    }
145}
146
147/// Segment pruning input, see [`Segment::prune`].
148#[derive(Debug)]
149#[cfg_attr(test, derive(Clone))]
150pub struct PruneInput {
151    pub(crate) previous_checkpoint: Option<PruneCheckpoint>,
152    /// Target block up to which the pruning needs to be done, inclusive.
153    pub(crate) to_block: BlockNumber,
154    /// Limits pruning of a segment.
155    pub(crate) limiter: PruneLimiter,
156}
157
158impl PruneInput {
159    /// Get next inclusive tx number range to prune according to the checkpoint and `to_block` block
160    /// number.
161    ///
162    /// To get the range start:
163    /// 1. If checkpoint exists, get next block body and return its first tx number.
164    /// 2. If checkpoint doesn't exist, return 0.
165    ///
166    /// To get the range end: get last tx number for `to_block`.
167    pub(crate) fn get_next_tx_num_range<Provider: BlockReader>(
168        &self,
169        provider: &Provider,
170    ) -> ProviderResult<Option<RangeInclusive<TxNumber>>> {
171        let from_tx_number = self.previous_checkpoint
172            // Checkpoint exists, prune from the next transaction after the highest pruned one
173            .and_then(|checkpoint| match (checkpoint.tx_number, checkpoint.block_number) {
174                (Some(tx_number), _) => Some(tx_number + 1),
175                // Pruned through a block without pruning any transactions, e.g. a snap sync pivot.
176                (None, Some(_)) => {
177                    debug!(target: "pruner", ?checkpoint, "Prune checkpoint has no transaction number, starting from transaction 0");
178                    None
179                }
180                (None, None) => {
181                    error!(target: "pruner", ?checkpoint, "Expected transaction number in prune checkpoint, found None");
182                    None
183                }
184            })
185            // No checkpoint exists, prune from genesis
186            .unwrap_or_default();
187
188        let to_tx_number = match provider.block_body_indices(self.to_block)? {
189            Some(body) => {
190                let last_tx = body.last_tx_num();
191                if last_tx + body.tx_count() == 0 {
192                    // Prevents a scenario where the pruner correctly starts at a finalized block,
193                    // but the first transaction (tx_num = 0) only appears on a non-finalized one.
194                    // Should only happen on a test/hive scenario.
195                    return Ok(None)
196                }
197                last_tx
198            }
199            None => return Ok(None),
200        };
201
202        let range = from_tx_number..=to_tx_number;
203        if range.is_empty() {
204            return Ok(None)
205        }
206
207        Ok(Some(range))
208    }
209
210    /// Get next inclusive block range to prune according to the checkpoint, `to_block` block
211    /// number and `limit`.
212    ///
213    /// To get the range start (`from_block`):
214    /// 1. If checkpoint exists, use next block.
215    /// 2. If checkpoint doesn't exist, use block 0.
216    ///
217    /// To get the range end: use block `to_block`.
218    pub(crate) fn get_next_block_range(&self) -> Option<RangeInclusive<BlockNumber>> {
219        let from_block = self.get_start_next_block_range();
220        let range = from_block..=self.to_block;
221        if range.is_empty() {
222            return None
223        }
224
225        Some(range)
226    }
227
228    /// Returns the start of the next block range.
229    ///
230    /// 1. If checkpoint exists, use next block.
231    /// 2. If checkpoint doesn't exist, use block 0.
232    pub(crate) fn get_start_next_block_range(&self) -> u64 {
233        self.previous_checkpoint
234            .and_then(|checkpoint| checkpoint.block_number)
235            // Checkpoint exists, prune from the next block after the highest pruned one
236            .map(|block_number| block_number + 1)
237            // No checkpoint exists, prune from genesis
238            .unwrap_or(0)
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use alloy_primitives::B256;
246    use reth_provider::{
247        providers::BlockchainProvider,
248        test_utils::{create_test_provider_factory, MockEthProvider},
249        BlockWriter,
250    };
251    use reth_testing_utils::generators::{self, random_block_range, BlockRangeParams};
252
253    #[test]
254    fn test_prune_input_get_next_tx_num_range_no_to_block() {
255        let input = PruneInput {
256            previous_checkpoint: None,
257            to_block: 10,
258            limiter: PruneLimiter::default(),
259        };
260
261        // Default provider with no block corresponding to block 10
262        let provider = MockEthProvider::default();
263
264        // No block body for block 10, expected None
265        let range = input.get_next_tx_num_range(&provider).expect("Expected range");
266        assert!(range.is_none());
267    }
268
269    #[test]
270    fn test_prune_input_get_next_tx_num_range_no_tx() {
271        let input = PruneInput {
272            previous_checkpoint: None,
273            to_block: 10,
274            limiter: PruneLimiter::default(),
275        };
276
277        let mut rng = generators::rng();
278        let factory = create_test_provider_factory();
279
280        // Generate 10 random blocks with no transactions
281        let blocks = random_block_range(
282            &mut rng,
283            0..=10,
284            BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() },
285        );
286
287        // Insert the blocks into the database
288        let provider_rw = factory.provider_rw().expect("failed to get provider_rw");
289        for block in &blocks {
290            provider_rw
291                .insert_block(
292                    &block.clone().try_recover().expect("failed to seal block with senders"),
293                )
294                .expect("failed to insert block");
295        }
296        provider_rw.commit().expect("failed to commit");
297
298        // Create a new provider
299        let provider = BlockchainProvider::new(factory).unwrap();
300
301        // Since there are no transactions, expected None
302        let range = input.get_next_tx_num_range(&provider).expect("Expected range");
303        assert!(range.is_none());
304    }
305
306    #[test]
307    fn test_prune_input_get_next_tx_num_range_valid() {
308        // Create a new prune input
309        let input = PruneInput {
310            previous_checkpoint: None,
311            to_block: 10,
312            limiter: PruneLimiter::default(),
313        };
314
315        let mut rng = generators::rng();
316        let factory = create_test_provider_factory();
317
318        // Generate 10 random blocks with some transactions
319        let blocks = random_block_range(
320            &mut rng,
321            0..=10,
322            BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..5, ..Default::default() },
323        );
324
325        // Insert the blocks into the database
326        let provider_rw = factory.provider_rw().expect("failed to get provider_rw");
327        for block in &blocks {
328            provider_rw
329                .insert_block(
330                    &block.clone().try_recover().expect("failed to seal block with senders"),
331                )
332                .expect("failed to insert block");
333        }
334        provider_rw.commit().expect("failed to commit");
335
336        // Create a new provider
337        let provider = BlockchainProvider::new(factory).unwrap();
338
339        // Get the next tx number range
340        let range = input.get_next_tx_num_range(&provider).expect("Expected range").unwrap();
341
342        // Calculate the total number of transactions
343        let num_txs = blocks.iter().map(|block| block.transaction_count() as u64).sum::<u64>();
344
345        assert_eq!(range, 0..=num_txs - 1);
346    }
347
348    #[test]
349    fn test_prune_input_get_next_tx_checkpoint_without_tx_number() {
350        // Create a prune input with a previous checkpoint without a tx number (unexpected)
351        let input = PruneInput {
352            previous_checkpoint: Some(PruneCheckpoint {
353                block_number: Some(5),
354                tx_number: None,
355                prune_mode: PruneMode::Full,
356            }),
357            to_block: 10,
358            limiter: PruneLimiter::default(),
359        };
360
361        let mut rng = generators::rng();
362        let factory = create_test_provider_factory();
363
364        // Generate 10 random blocks
365        let blocks = random_block_range(
366            &mut rng,
367            0..=10,
368            BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..5, ..Default::default() },
369        );
370
371        // Insert the blocks into the database
372        let provider_rw = factory.provider_rw().expect("failed to get provider_rw");
373        for block in &blocks {
374            provider_rw
375                .insert_block(
376                    &block.clone().try_recover().expect("failed to seal block with senders"),
377                )
378                .expect("failed to insert block");
379        }
380        provider_rw.commit().expect("failed to commit");
381
382        // Create a new provider
383        let provider = BlockchainProvider::new(factory).unwrap();
384
385        // Fetch the range and check if it is correct
386        let range = input.get_next_tx_num_range(&provider).expect("Expected range").unwrap();
387
388        // Calculate the total number of transactions
389        let num_txs = blocks.iter().map(|block| block.transaction_count() as u64).sum::<u64>();
390
391        assert_eq!(range, 0..=num_txs - 1,);
392    }
393
394    #[test]
395    fn test_prune_input_get_next_tx_empty_range() {
396        // Create a new provider via factory
397        let mut rng = generators::rng();
398        let factory = create_test_provider_factory();
399
400        // Generate 10 random blocks
401        let blocks = random_block_range(
402            &mut rng,
403            0..=10,
404            BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..5, ..Default::default() },
405        );
406
407        // Insert the blocks into the database
408        let provider_rw = factory.provider_rw().expect("failed to get provider_rw");
409        for block in &blocks {
410            provider_rw
411                .insert_block(
412                    &block.clone().try_recover().expect("failed to seal block with senders"),
413                )
414                .expect("failed to insert block");
415        }
416        provider_rw.commit().expect("failed to commit");
417
418        // Create a new provider
419        let provider = BlockchainProvider::new(factory).unwrap();
420
421        // Get the last tx number
422        // Calculate the total number of transactions
423        let num_txs = blocks.iter().map(|block| block.transaction_count() as u64).sum::<u64>();
424        let max_range = num_txs - 1;
425
426        // Create a prune input with a previous checkpoint that is the last tx number
427        let input = PruneInput {
428            previous_checkpoint: Some(PruneCheckpoint {
429                block_number: Some(5),
430                tx_number: Some(max_range),
431                prune_mode: PruneMode::Full,
432            }),
433            to_block: 10,
434            limiter: PruneLimiter::default(),
435        };
436
437        // We expect an empty range since the previous checkpoint is the last tx number
438        let range = input.get_next_tx_num_range(&provider).expect("Expected range");
439        assert!(range.is_none());
440    }
441}