reth_prune/segments/user/
bodies.rs1use 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#[derive(Debug)]
15pub struct Bodies {
16 mode: PruneMode,
17}
18
19impl Bodies {
20 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 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 PruneTestCase {
172 prune_mode: PruneMode::Before(750_000),
173 expected_pruned: 1000,
174 expected_lowest_block: Some(999_999),
175 },
176 PruneTestCase {
179 prune_mode: PruneMode::Before(850_000),
180 expected_pruned: 0,
181 expected_lowest_block: Some(999_999),
182 },
183 PruneTestCase {
186 prune_mode: PruneMode::Before(1_599_999),
187 expected_pruned: 2000,
188 expected_lowest_block: Some(1_999_999),
189 },
190 PruneTestCase {
193 prune_mode: PruneMode::Distance(500_000),
194 expected_pruned: 1000,
195 expected_lowest_block: Some(2_499_999),
196 },
197 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}