Skip to main content

reth_prune/segments/user/
receipts_by_logs.rs

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        // Contract log filtering removes every receipt possible except the ones in the list. So,
53        // for the other receipts it's as if they had a `PruneMode::Distance()` of
54        // `MINIMUM_UNWIND_SAFE_DISTANCE`.
55        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        // Get status checkpoint from latest run
61        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        // Figure out what receipts have already been pruned, so we can have an accurate
74        // `address_filter`
75        let address_filter = self.config.group_by_block(input.to_block, last_pruned_block)?;
76
77        // Splits all transactions in different block ranges. Each block range will have its own
78        // filter address list and will check it while going through the table
79        //
80        // Example:
81        // For an `address_filter` such as:
82        // { block9: [a1, a2], block20: [a3, a4, a5] }
83        //
84        // The following structures will be created in the exact order as showed:
85        // `block_ranges`: [
86        //    (block0, block8, 0 addresses),
87        //    (block9, block19, 2 addresses),
88        //    (block20, to_block, 5 addresses)
89        //  ]
90        // `filtered_addresses`: [a1, a2, a3, a4, a5]
91        //
92        // The first range will delete all receipts between block0 - block8
93        // The second range will delete all receipts between block9 - 19, except the ones with
94        //     emitter logs from these addresses: [a1, a2].
95        // The third range will delete all receipts between block20 - to_block, except the ones with
96        //     emitter logs from these addresses: [a1, a2, a3, a4, a5]
97        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            // This will clear all receipts before the first  appearance of a contract log or since
105            // the block after the last pruned one.
106            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            // Addresses in lower block ranges, are still included in the inclusion list for future
117            // ranges.
118            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            // Calculate the transaction range from this block range
137            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                // Empty blocks advance the block checkpoint without consuming a transaction.
151                last_pruned_block = Some(end_block);
152                continue
153            }
154
155            // Delete receipts, except the ones in the inclusion list
156            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            // For accurate checkpoints we need to know that we have checked every transaction.
182            // Example: we reached the end of the range, and the last receipt is supposed to skip
183            // its deletion.
184            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                    // If there's more receipts to prune, set the checkpoint block number to
192                    // previous, so we could finish pruning its receipts on the
193                    // next run.
194                    .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        // If there are contracts using `PruneMode::Distance(_)` there will be receipts before
206        // `to_block` that become eligible to be pruned in future runs. Therefore, our checkpoint is
207        // not actually `to_block`, but the `lowest_block_with_distance` from any contract.
208        // This ensures that in future pruner runs we can prune all these receipts between the
209        // previous `lowest_block_with_distance` and the new one using
210        // `get_next_tx_num_range_from_checkpoint`.
211        //
212        // Only applies if we were able to prune everything intended for this run, otherwise the
213        // checkpoint is the `last_pruned_block`.
214        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            // All receipts are in the end of the block
443            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            // Either we only find our contract, or the receipt is part of the unprunable receipts
463            // set by tip - 128
464            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}