1use crate::{
2 db_ext::DbTxPruneExt,
3 segments::{PruneInput, Segment},
4 PrunerError,
5};
6use alloy_consensus::TxReceipt;
7use reth_db_api::{table::Value, tables, transaction::DbTxMut};
8use reth_primitives_traits::NodePrimitives;
9use reth_provider::{BlockReader, DBProvider, NodePrimitivesProvider, PruneCheckpointWriter};
10use reth_prune_types::{
11 PruneCheckpoint, PruneMode, PrunePurpose, PruneSegment, ReceiptsLogPruneConfig, SegmentOutput,
12 MINIMUM_UNWIND_SAFE_DISTANCE,
13};
14use tracing::{instrument, trace};
15#[derive(Debug)]
16pub struct ReceiptsByLogs {
17 config: ReceiptsLogPruneConfig,
18}
19
20impl ReceiptsByLogs {
21 pub const fn new(config: ReceiptsLogPruneConfig) -> Self {
22 Self { config }
23 }
24}
25
26impl<Provider> Segment<Provider> for ReceiptsByLogs
27where
28 Provider: DBProvider<Tx: DbTxMut>
29 + PruneCheckpointWriter
30 + BlockReader
31 + NodePrimitivesProvider<Primitives: NodePrimitives<Receipt: Value>>,
32{
33 fn segment(&self) -> PruneSegment {
34 PruneSegment::ContractLogs
35 }
36
37 fn mode(&self) -> Option<PruneMode> {
38 None
39 }
40
41 fn purpose(&self) -> PrunePurpose {
42 PrunePurpose::User
43 }
44
45 #[instrument(
46 name = "ReceiptsByLogs::prune",
47 target = "pruner",
48 skip(self, provider),
49 ret(level = "trace")
50 )]
51 fn prune(&self, provider: &Provider, input: PruneInput) -> Result<SegmentOutput, PrunerError> {
52 let to_block = PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)
56 .prune_target_block(input.to_block, PruneSegment::ContractLogs, PrunePurpose::User)?
57 .map(|(bn, _)| bn)
58 .unwrap_or_default();
59
60 let mut last_pruned_block =
62 input.previous_checkpoint.and_then(|checkpoint| checkpoint.block_number);
63
64 let initial_last_pruned_block = last_pruned_block;
65
66 let mut from_tx_number = match initial_last_pruned_block {
67 Some(block) => {
68 provider.block_body_indices(block)?.map(|block| block.next_tx_num()).unwrap_or(0)
69 }
70 None => 0,
71 };
72
73 let address_filter = self.config.group_by_block(input.to_block, last_pruned_block)?;
76
77 let mut block_ranges = vec![];
98 let mut blocks_iter = address_filter.iter().peekable();
99 let mut filtered_addresses = vec![];
100
101 while let Some((start_block, addresses)) = blocks_iter.next() {
102 filtered_addresses.extend_from_slice(addresses);
103
104 if block_ranges.is_empty() {
107 let init = last_pruned_block.map(|b| b + 1).unwrap_or_default();
108 if init < *start_block {
109 block_ranges.push((init, *start_block - 1, 0));
110 }
111 }
112
113 let end_block =
114 blocks_iter.peek().map(|(next_block, _)| *next_block - 1).unwrap_or(to_block);
115
116 block_ranges.push((*start_block, end_block, filtered_addresses.len()));
119 }
120
121 trace!(
122 target: "pruner",
123 ?block_ranges,
124 ?filtered_addresses,
125 "Calculated block ranges and filtered addresses",
126 );
127
128 let mut limiter = input.limiter;
129
130 let mut done = true;
131 let mut pruned = 0;
132 let mut last_pruned_transaction = None;
133 for (start_block, end_block, num_addresses) in block_ranges {
134 let block_range = start_block..=end_block;
135
136 let tx_range_end = match provider.block_body_indices(end_block)? {
138 Some(body) => body.next_tx_num(),
139 None => {
140 trace!(
141 target: "pruner",
142 ?block_range,
143 "No receipts to prune."
144 );
145 continue
146 }
147 };
148 let tx_range = from_tx_number..tx_range_end;
149 if tx_range.is_empty() {
150 last_pruned_block = Some(end_block);
152 continue
153 }
154
155 let mut last_skipped_transaction = 0;
157 let deleted;
158 (deleted, done) = provider.tx_ref().prune_table_with_range::<tables::Receipts<
159 <Provider::Primitives as NodePrimitives>::Receipt,
160 >>(
161 tx_range,
162 &mut limiter,
163 |(tx_num, receipt)| {
164 let skip = num_addresses > 0 &&
165 receipt.logs().iter().any(|log| {
166 filtered_addresses[..num_addresses].contains(&&log.address)
167 });
168
169 if skip {
170 last_skipped_transaction = *tx_num;
171 }
172 skip
173 },
174 |row| last_pruned_transaction = Some(row.0),
175 )?;
176
177 trace!(target: "pruner", %deleted, %done, ?block_range, "Pruned receipts");
178
179 pruned += deleted;
180
181 let last_pruned_transaction = *last_pruned_transaction
185 .insert(last_pruned_transaction.unwrap_or_default().max(last_skipped_transaction));
186
187 last_pruned_block = Some(
188 provider
189 .block_by_transaction_id(last_pruned_transaction)?
190 .ok_or(PrunerError::InconsistentData("Block for transaction is not found"))?
191 .saturating_sub(if done { 0 } else { 1 }),
195 );
196
197 if limiter.is_limit_reached() {
198 done &= end_block == to_block;
199 break
200 }
201
202 from_tx_number = last_pruned_transaction + 1;
203 }
204
205 let prune_mode_block = self
215 .config
216 .lowest_block_with_distance(input.to_block, initial_last_pruned_block)?
217 .unwrap_or(to_block);
218
219 provider.save_prune_checkpoint(
220 PruneSegment::ContractLogs,
221 PruneCheckpoint {
222 block_number: Some(prune_mode_block.min(last_pruned_block.unwrap_or(u64::MAX))),
223 tx_number: last_pruned_transaction,
224 prune_mode: PruneMode::Before(prune_mode_block),
225 },
226 )?;
227
228 let progress = limiter.progress(done);
229
230 Ok(SegmentOutput { progress, pruned, checkpoint: None })
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use crate::segments::{user::ReceiptsByLogs, PruneInput, PruneLimiter, Segment};
237 use alloy_primitives::B256;
238 use assert_matches::assert_matches;
239 use reth_db_api::{cursor::DbCursorRO, tables, transaction::DbTx};
240 use reth_primitives_traits::InMemorySize;
241 use reth_provider::{BlockReader, DBProvider, DatabaseProviderFactory, PruneCheckpointReader};
242 use reth_prune_types::{
243 PruneCheckpoint, PruneMode, PruneSegment, ReceiptsLogPruneConfig,
244 MINIMUM_UNWIND_SAFE_DISTANCE,
245 };
246 use reth_stages::test_utils::{StorageKind, TestStageDB};
247 use reth_testing_utils::generators::{
248 self, random_block_range, random_eoa_account, random_log, random_receipt, BlockRangeParams,
249 };
250 use std::collections::BTreeMap;
251
252 #[test]
253 fn prune_receipts_by_logs_empty_prefix() {
254 for previous_block in [None, Some(1)] {
255 for retain_first in [false, true] {
256 let db = TestStageDB::default();
257 let mut rng = generators::rng();
258 let blocks = [
259 random_block_range(
260 &mut rng,
261 0..=1,
262 BlockRangeParams { tx_count: 0..1, ..Default::default() },
263 ),
264 random_block_range(
265 &mut rng,
266 2..=2,
267 BlockRangeParams { tx_count: 2..3, ..Default::default() },
268 ),
269 ]
270 .concat();
271 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).unwrap();
272
273 let (address, _) = random_eoa_account(&mut rng);
274 let receipts = blocks[2]
275 .body()
276 .transactions
277 .iter()
278 .enumerate()
279 .map(|(id, tx)| {
280 let mut receipt = random_receipt(&mut rng, tx, Some(0), None);
281 if id == 0 && retain_first {
282 receipt.logs.push(random_log(&mut rng, Some(address), Some(1)));
283 }
284 (id as u64, receipt)
285 })
286 .collect::<Vec<_>>();
287 db.insert_receipts(receipts.clone()).unwrap();
288
289 let provider = db.factory.database_provider_rw().unwrap();
290 let result = ReceiptsByLogs::new(ReceiptsLogPruneConfig(BTreeMap::from([(
291 address,
292 PruneMode::Before(2),
293 )])))
294 .prune(
295 &provider,
296 PruneInput {
297 previous_checkpoint: previous_block.map(|block| PruneCheckpoint {
298 block_number: Some(block),
299 tx_number: None,
300 prune_mode: PruneMode::Before(2),
301 }),
302 to_block: MINIMUM_UNWIND_SAFE_DISTANCE + 2,
303 limiter: PruneLimiter::default(),
304 },
305 )
306 .unwrap();
307 assert!(result.progress.is_finished());
308 assert_eq!(result.pruned, if retain_first { 1 } else { 2 });
309 provider.commit().unwrap();
310 let expected = if retain_first { vec![receipts[0].clone()] } else { vec![] };
311 assert_eq!(db.table::<tables::Receipts>().unwrap(), expected);
312 }
313 }
314 }
315
316 #[test]
317 fn prune_receipts_by_logs_empty_chain() {
318 let db = TestStageDB::default();
319 let blocks = random_block_range(
320 &mut generators::rng(),
321 0..=2,
322 BlockRangeParams { tx_count: 0..1, ..Default::default() },
323 );
324 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).unwrap();
325 let provider = db.factory.database_provider_rw().unwrap();
326 let result = ReceiptsByLogs::new(ReceiptsLogPruneConfig(BTreeMap::from([(
327 alloy_primitives::Address::ZERO,
328 PruneMode::Before(2),
329 )])))
330 .prune(
331 &provider,
332 PruneInput {
333 previous_checkpoint: None,
334 to_block: MINIMUM_UNWIND_SAFE_DISTANCE + 2,
335 limiter: PruneLimiter::default(),
336 },
337 )
338 .unwrap();
339 assert!(result.progress.is_finished());
340 assert_eq!(result.pruned, 0);
341 assert_eq!(
342 provider.get_prune_checkpoint(PruneSegment::ContractLogs).unwrap(),
343 Some(PruneCheckpoint {
344 block_number: Some(2),
345 tx_number: None,
346 prune_mode: PruneMode::Before(2),
347 })
348 );
349 }
350
351 #[test]
352 fn prune_receipts_by_logs() {
353 reth_tracing::init_test_tracing();
354
355 let db = TestStageDB::default();
356 let mut rng = generators::rng();
357
358 let tip = 20000;
359 let blocks = [
360 random_block_range(
361 &mut rng,
362 0..=100,
363 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 1..5, ..Default::default() },
364 ),
365 random_block_range(
366 &mut rng,
367 (100 + 1)..=(tip - 100),
368 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() },
369 ),
370 random_block_range(
371 &mut rng,
372 (tip - 100 + 1)..=tip,
373 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 1..5, ..Default::default() },
374 ),
375 ]
376 .concat();
377 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks");
378
379 let mut receipts = Vec::new();
380
381 let (deposit_contract_addr, _) = random_eoa_account(&mut rng);
382 for block in &blocks {
383 receipts.reserve_exact(block.body().size());
384 for (txi, transaction) in block.body().transactions.iter().enumerate() {
385 let mut receipt = random_receipt(&mut rng, transaction, Some(1), None);
386 receipt.logs.push(random_log(
387 &mut rng,
388 (txi == (block.transaction_count() - 1)).then_some(deposit_contract_addr),
389 Some(1),
390 ));
391 receipts.push((receipts.len() as u64, receipt));
392 }
393 }
394 db.insert_receipts(receipts).expect("insert receipts");
395
396 assert_eq!(
397 db.table::<tables::Transactions>().unwrap().len(),
398 blocks.iter().map(|block| block.transaction_count()).sum::<usize>()
399 );
400 assert_eq!(
401 db.table::<tables::Transactions>().unwrap().len(),
402 db.table::<tables::Receipts>().unwrap().len()
403 );
404
405 let run_prune = || {
406 let provider = db.factory.database_provider_rw().unwrap();
407
408 let prune_before_block: usize = 20;
409 let prune_mode = PruneMode::Before(prune_before_block as u64);
410 let receipts_log_filter =
411 ReceiptsLogPruneConfig(BTreeMap::from([(deposit_contract_addr, prune_mode)]));
412
413 let limiter = PruneLimiter::default().set_deleted_entries_limit(10);
414
415 let result = ReceiptsByLogs::new(receipts_log_filter).prune(
416 &provider,
417 PruneInput {
418 previous_checkpoint: db
419 .factory
420 .provider()
421 .unwrap()
422 .get_prune_checkpoint(PruneSegment::ContractLogs)
423 .unwrap(),
424 to_block: tip,
425 limiter,
426 },
427 );
428 provider.commit().expect("commit");
429
430 assert_matches!(result, Ok(_));
431 let output = result.unwrap();
432
433 let (pruned_block, pruned_tx) = db
434 .factory
435 .provider()
436 .unwrap()
437 .get_prune_checkpoint(PruneSegment::ContractLogs)
438 .unwrap()
439 .map(|checkpoint| (checkpoint.block_number.unwrap(), checkpoint.tx_number.unwrap()))
440 .unwrap_or_default();
441
442 let unprunable = pruned_block.saturating_sub(prune_before_block as u64 - 1);
444
445 assert_eq!(
446 db.table::<tables::Receipts>().unwrap().len(),
447 blocks.iter().map(|block| block.transaction_count()).sum::<usize>() -
448 ((pruned_tx + 1) - unprunable) as usize
449 );
450
451 output.progress.is_finished()
452 };
453
454 while !run_prune() {}
455
456 let provider = db.factory.provider().unwrap();
457 let mut cursor = provider.tx_ref().cursor_read::<tables::Receipts>().unwrap();
458 let walker = cursor.walk(None).unwrap();
459 for receipt in walker {
460 let (tx_num, receipt) = receipt.unwrap();
461
462 assert!(
465 receipt.logs.iter().any(|l| l.address == deposit_contract_addr) ||
466 provider.block_by_transaction_id(tx_num).unwrap().unwrap() > tip - 128,
467 );
468 }
469 }
470}