1use crate::{
7 db_ext::DbTxPruneExt,
8 segments::{self, PruneInput},
9 PrunerError,
10};
11use reth_db_api::{table::Value, tables, transaction::DbTxMut};
12use reth_primitives_traits::NodePrimitives;
13use reth_provider::{
14 errors::provider::ProviderResult, BlockReader, DBProvider, EitherWriter,
15 NodePrimitivesProvider, PruneCheckpointWriter, StaticFileProviderFactory, StorageSettingsCache,
16};
17use reth_prune_types::{PruneCheckpoint, PruneSegment, SegmentOutput, SegmentOutputCheckpoint};
18use reth_static_file_types::StaticFileSegment;
19use tracing::{debug, trace};
20
21pub(crate) fn prune<Provider>(
22 provider: &Provider,
23 input: PruneInput,
24) -> Result<SegmentOutput, PrunerError>
25where
26 Provider: DBProvider<Tx: DbTxMut>
27 + BlockReader
28 + StorageSettingsCache
29 + StaticFileProviderFactory
30 + NodePrimitivesProvider<Primitives: NodePrimitives<Receipt: Value>>,
31{
32 if EitherWriter::receipts_destination(provider).is_static_file() {
33 debug!(target: "pruner", "Pruning receipts from static files.");
34 return segments::prune_static_files(provider, input, StaticFileSegment::Receipts)
35 }
36 debug!(target: "pruner", "Pruning receipts from database.");
37
38 let tx_range = match input.get_next_tx_num_range(provider)? {
40 Some(range) => range,
41 None => {
42 trace!(target: "pruner", "No receipts to prune");
43 return Ok(SegmentOutput::done())
44 }
45 };
46 let tx_range_end = *tx_range.end();
47
48 let mut limiter = input.limiter;
49
50 let mut last_pruned_transaction = tx_range_end;
51 let (pruned, done) = provider.tx_ref().prune_table_with_range::<tables::Receipts<
52 <Provider::Primitives as NodePrimitives>::Receipt,
53 >>(
54 tx_range,
55 &mut limiter,
56 |_| false,
57 |row| last_pruned_transaction = row.0,
58 )?;
59 trace!(target: "pruner", %pruned, %done, "Pruned receipts");
60
61 let last_pruned_block = provider
62 .block_by_transaction_id(last_pruned_transaction)?
63 .ok_or(PrunerError::InconsistentData("Block for transaction is not found"))?
64 .checked_sub(if done { 0 } else { 1 });
67
68 let progress = limiter.progress(done);
69
70 Ok(SegmentOutput {
71 progress,
72 pruned,
73 checkpoint: Some(SegmentOutputCheckpoint {
74 block_number: last_pruned_block,
75 tx_number: Some(last_pruned_transaction),
76 }),
77 })
78}
79
80pub(crate) fn save_checkpoint(
81 provider: impl PruneCheckpointWriter,
82 checkpoint: PruneCheckpoint,
83) -> ProviderResult<()> {
84 provider.save_prune_checkpoint(PruneSegment::Receipts, checkpoint)?;
85
86 provider.save_prune_checkpoint(PruneSegment::ContractLogs, checkpoint)?;
89
90 Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95 use crate::segments::{PruneInput, PruneLimiter, SegmentOutput};
96 use alloy_primitives::{BlockNumber, TxNumber, B256};
97 use assert_matches::assert_matches;
98 use itertools::{
99 FoldWhile::{Continue, Done},
100 Itertools,
101 };
102 use reth_db_api::tables;
103 use reth_provider::{DBProvider, DatabaseProviderFactory, PruneCheckpointReader};
104 use reth_prune_types::{
105 PruneCheckpoint, PruneInterruptReason, PruneMode, PruneProgress, PruneSegment,
106 };
107 use reth_stages::test_utils::{StorageKind, TestStageDB};
108 use reth_testing_utils::generators::{
109 self, random_block_range, random_receipt, BlockRangeParams,
110 };
111 use std::ops::Sub;
112
113 #[test]
114 fn prune_legacy() {
115 let mut db = TestStageDB::default();
116 db.factory = db.factory.with_prune_modes(reth_prune_types::PruneModes {
119 receipts: Some(PruneMode::Full),
120 ..Default::default()
121 });
122 let mut rng = generators::rng();
123
124 let blocks = random_block_range(
125 &mut rng,
126 1..=10,
127 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
128 );
129 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks");
130
131 let mut receipts = Vec::new();
132 for block in &blocks {
133 receipts.reserve_exact(block.transaction_count());
134 for transaction in &block.body().transactions {
135 receipts.push((
136 receipts.len() as u64,
137 random_receipt(&mut rng, transaction, Some(0), None),
138 ));
139 }
140 }
141 let receipts_len = receipts.len();
142 db.insert_receipts(receipts).expect("insert receipts");
143
144 assert_eq!(
145 db.table::<tables::Transactions>().unwrap().len(),
146 blocks.iter().map(|block| block.transaction_count()).sum::<usize>()
147 );
148 assert_eq!(
149 db.table::<tables::Transactions>().unwrap().len(),
150 db.table::<tables::Receipts>().unwrap().len()
151 );
152
153 let test_prune = |to_block: BlockNumber, expected_result: (PruneProgress, usize)| {
154 let prune_mode = PruneMode::Before(to_block);
155 let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
156 let input = PruneInput {
157 previous_checkpoint: db
158 .factory
159 .provider()
160 .unwrap()
161 .get_prune_checkpoint(PruneSegment::Receipts)
162 .unwrap(),
163 to_block,
164 limiter: limiter.clone(),
165 };
166
167 let next_tx_number_to_prune = db
168 .factory
169 .provider()
170 .unwrap()
171 .get_prune_checkpoint(PruneSegment::Receipts)
172 .unwrap()
173 .and_then(|checkpoint| checkpoint.tx_number)
174 .map(|tx_number| tx_number + 1)
175 .unwrap_or_default();
176
177 let last_pruned_tx_number = blocks
178 .iter()
179 .take(to_block as usize)
180 .map(|block| block.transaction_count())
181 .sum::<usize>()
182 .min(
183 next_tx_number_to_prune as usize +
184 input.limiter.deleted_entries_limit().unwrap(),
185 )
186 .sub(1);
187
188 let provider = db.factory.database_provider_rw().unwrap();
189 let result = super::prune(&provider, input).unwrap();
190 limiter.increment_deleted_entries_count_by(result.pruned);
191
192 assert_matches!(
193 result,
194 SegmentOutput {progress, pruned, checkpoint: Some(_)}
195 if (progress, pruned) == expected_result
196 );
197
198 super::save_checkpoint(
199 &provider,
200 result.checkpoint.unwrap().as_prune_checkpoint(prune_mode),
201 )
202 .unwrap();
203 provider.commit().expect("commit");
204
205 let last_pruned_block_number = blocks
206 .iter()
207 .fold_while((0, 0), |(_, mut tx_count), block| {
208 tx_count += block.transaction_count();
209
210 if tx_count > last_pruned_tx_number {
211 Done((block.number, tx_count))
212 } else {
213 Continue((block.number, tx_count))
214 }
215 })
216 .into_inner()
217 .0
218 .checked_sub(if result.progress.is_finished() { 0 } else { 1 });
219
220 assert_eq!(
221 db.table::<tables::Receipts>().unwrap().len(),
222 receipts_len - (last_pruned_tx_number + 1)
223 );
224 assert_eq!(
225 db.factory
226 .provider()
227 .unwrap()
228 .get_prune_checkpoint(PruneSegment::Receipts)
229 .unwrap(),
230 Some(PruneCheckpoint {
231 block_number: last_pruned_block_number,
232 tx_number: Some(last_pruned_tx_number as TxNumber),
233 prune_mode
234 })
235 );
236 };
237
238 test_prune(
239 6,
240 (PruneProgress::HasMoreData(PruneInterruptReason::DeletedEntriesLimitReached), 10),
241 );
242 test_prune(6, (PruneProgress::Finished, 2));
243 test_prune(10, (PruneProgress::Finished, 8));
244 }
245}