reth_prune/segments/user/
bodies.rs

1use crate::{
2    segments::{PruneInput, Segment},
3    PrunerError,
4};
5use reth_provider::{BlockReader, StaticFileProviderFactory};
6use reth_prune_types::{
7    PruneMode, PruneProgress, PrunePurpose, PruneSegment, SegmentOutput, SegmentOutputCheckpoint,
8};
9use reth_static_file_types::StaticFileSegment;
10
11/// Segment responsible for pruning transactions in static files.
12///
13/// This segment is controlled by the `bodies_history` configuration.
14#[derive(Debug)]
15pub struct Bodies {
16    mode: PruneMode,
17}
18
19impl Bodies {
20    /// Creates a new [`Bodies`] segment with the given prune mode.
21    pub const fn new(mode: PruneMode) -> Self {
22        Self { mode }
23    }
24}
25
26impl<Provider> Segment<Provider> for Bodies
27where
28    Provider: StaticFileProviderFactory + BlockReader,
29{
30    fn segment(&self) -> PruneSegment {
31        PruneSegment::Bodies
32    }
33
34    fn mode(&self) -> Option<PruneMode> {
35        Some(self.mode)
36    }
37
38    fn purpose(&self) -> PrunePurpose {
39        PrunePurpose::User
40    }
41
42    fn prune(&self, provider: &Provider, input: PruneInput) -> Result<SegmentOutput, PrunerError> {
43        let deleted_headers = provider
44            .static_file_provider()
45            .delete_segment_below_block(StaticFileSegment::Transactions, input.to_block + 1)?;
46
47        if deleted_headers.is_empty() {
48            return Ok(SegmentOutput::done())
49        }
50
51        let tx_ranges = deleted_headers.iter().filter_map(|header| header.tx_range());
52
53        let pruned = tx_ranges.clone().map(|range| range.len()).sum::<u64>() as usize;
54
55        Ok(SegmentOutput {
56            progress: PruneProgress::Finished,
57            pruned,
58            checkpoint: Some(SegmentOutputCheckpoint {
59                block_number: Some(input.to_block),
60                tx_number: tx_ranges.map(|range| range.end()).max(),
61            }),
62        })
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::Pruner;
70    use alloy_primitives::BlockNumber;
71    use reth_exex_types::FinishedExExHeight;
72    use reth_provider::{
73        test_utils::{create_test_provider_factory, MockNodeTypesWithDB},
74        ProviderFactory, StaticFileWriter,
75    };
76    use reth_prune_types::{PruneMode, PruneProgress, PruneSegment};
77    use reth_static_file_types::{
78        SegmentHeader, SegmentRangeInclusive, StaticFileSegment, DEFAULT_BLOCKS_PER_STATIC_FILE,
79    };
80
81    /// Creates empty static file jars at 500k block intervals up to the tip block.
82    ///
83    /// Each jar contains sequential transaction ranges for testing deletion logic.
84    fn setup_static_file_jars<P: StaticFileProviderFactory>(provider: &P, tip_block: u64) {
85        let num_jars = (tip_block + 1) / DEFAULT_BLOCKS_PER_STATIC_FILE;
86        let txs_per_jar = 1000;
87        let static_file_provider = provider.static_file_provider();
88
89        let mut writer =
90            static_file_provider.latest_writer(StaticFileSegment::Transactions).unwrap();
91
92        for jar_idx in 0..num_jars {
93            let block_start = jar_idx * DEFAULT_BLOCKS_PER_STATIC_FILE;
94            let block_end = ((jar_idx + 1) * DEFAULT_BLOCKS_PER_STATIC_FILE - 1).min(tip_block);
95
96            let tx_start = jar_idx * txs_per_jar;
97            let tx_end = tx_start + txs_per_jar - 1;
98
99            *writer.user_header_mut() = SegmentHeader::new(
100                SegmentRangeInclusive::new(block_start, block_end),
101                Some(SegmentRangeInclusive::new(block_start, block_end)),
102                Some(SegmentRangeInclusive::new(tx_start, tx_end)),
103                StaticFileSegment::Transactions,
104            );
105
106            writer.inner().set_dirty();
107            writer.commit().expect("commit empty jar");
108
109            if jar_idx < num_jars - 1 {
110                writer.increment_block(block_end + 1).expect("increment block");
111            }
112        }
113
114        static_file_provider.initialize_index().expect("initialize index");
115    }
116
117    struct PruneTestCase {
118        prune_mode: PruneMode,
119        expected_pruned: usize,
120        expected_lowest_block: Option<BlockNumber>,
121    }
122
123    fn run_prune_test(
124        factory: &ProviderFactory<MockNodeTypesWithDB>,
125        finished_exex_height_rx: &tokio::sync::watch::Receiver<FinishedExExHeight>,
126        test_case: PruneTestCase,
127        tip: BlockNumber,
128    ) {
129        let bodies = Bodies::new(test_case.prune_mode);
130        let segments: Vec<Box<dyn Segment<_>>> = vec![Box::new(bodies)];
131
132        let mut pruner = Pruner::new_with_factory(
133            factory.clone(),
134            segments,
135            5,
136            10000,
137            None,
138            finished_exex_height_rx.clone(),
139        );
140
141        let result = pruner.run(tip).expect("pruner run");
142
143        assert_eq!(result.progress, PruneProgress::Finished);
144        assert_eq!(result.segments.len(), 1);
145
146        let (segment, output) = &result.segments[0];
147        assert_eq!(*segment, PruneSegment::Bodies);
148        assert_eq!(output.pruned, test_case.expected_pruned);
149
150        let static_provider = factory.static_file_provider();
151        assert_eq!(
152            static_provider.get_lowest_static_file_block(StaticFileSegment::Transactions),
153            test_case.expected_lowest_block
154        );
155        assert_eq!(
156            static_provider.get_highest_static_file_block(StaticFileSegment::Transactions),
157            Some(tip)
158        );
159    }
160
161    #[test]
162    fn bodies_prune_through_pruner() {
163        let factory = create_test_provider_factory();
164        let tip = 2_499_999;
165        setup_static_file_jars(&factory, tip);
166
167        let (_, finished_exex_height_rx) = tokio::sync::watch::channel(FinishedExExHeight::NoExExs);
168
169        let test_cases = vec![
170            // Test 1: PruneMode::Before(750_000) → deletes jar 1 (0-499_999)
171            PruneTestCase {
172                prune_mode: PruneMode::Before(750_000),
173                expected_pruned: 1000,
174                expected_lowest_block: Some(999_999),
175            },
176            // Test 2: PruneMode::Before(850_000) → no deletion (jar 2: 500_000-999_999 contains
177            // target)
178            PruneTestCase {
179                prune_mode: PruneMode::Before(850_000),
180                expected_pruned: 0,
181                expected_lowest_block: Some(999_999),
182            },
183            // Test 3: PruneMode::Before(1_599_999) → deletes jar 2 (500_000-999_999) and jar 3
184            // (1_000_000-1_499_999)
185            PruneTestCase {
186                prune_mode: PruneMode::Before(1_599_999),
187                expected_pruned: 2000,
188                expected_lowest_block: Some(1_999_999),
189            },
190            // Test 4: PruneMode::Distance(500_000) with tip=2_499_999 → deletes jar 4
191            // (1_500_000-1_999_999)
192            PruneTestCase {
193                prune_mode: PruneMode::Distance(500_000),
194                expected_pruned: 1000,
195                expected_lowest_block: Some(2_499_999),
196            },
197            // Test 5: PruneMode::Before(2_300_000) → no deletion (jar 5: 2_000_000-2_499_999
198            // contains target)
199            PruneTestCase {
200                prune_mode: PruneMode::Before(2_300_000),
201                expected_pruned: 0,
202                expected_lowest_block: Some(2_499_999),
203            },
204        ];
205
206        for test_case in test_cases {
207            run_prune_test(&factory, &finished_exex_height_rx, test_case, tip);
208        }
209    }
210}