Skip to main content

reth_prune/segments/
receipts.rs

1//! Common receipts pruning logic.
2//!
3//! - [`crate::segments::user::Receipts`] is responsible for pruning receipts according to the
4//!   user-configured settings (for example, on a full node or with a custom prune config)
5
6use 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    // Original database implementation for when receipts are not on static files (old nodes)
39    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        // If there's more receipts to prune, set the checkpoint block number to previous,
65        // so we could finish pruning its receipts on the next run.
66        .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    // `PruneSegment::Receipts` overrides `PruneSegment::ContractLogs`, so we can preemptively
87    // limit their pruning start point.
88    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        // Configure the factory to use database for receipts by enabling receipt pruning.
117        // This ensures EitherWriter::receipts_destination returns Database instead of StaticFile.
118        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}