Skip to main content

reth_prune/segments/user/
sender_recovery.rs

1use crate::{
2    db_ext::DbTxPruneExt,
3    segments::{self, PruneInput, Segment},
4    PrunerError,
5};
6use reth_db_api::{tables, transaction::DbTxMut};
7use reth_provider::{
8    BlockReader, DBProvider, EitherWriterDestination, StaticFileProviderFactory,
9    StorageSettingsCache,
10};
11use reth_prune_types::{
12    PruneMode, PruneProgress, PrunePurpose, PruneSegment, SegmentOutput, SegmentOutputCheckpoint,
13};
14use reth_static_file_types::StaticFileSegment;
15use tracing::{debug, instrument, trace};
16
17#[derive(Debug)]
18pub struct SenderRecovery {
19    mode: PruneMode,
20}
21
22impl SenderRecovery {
23    pub const fn new(mode: PruneMode) -> Self {
24        Self { mode }
25    }
26}
27
28impl<Provider> Segment<Provider> for SenderRecovery
29where
30    Provider:
31        DBProvider<Tx: DbTxMut> + BlockReader + StorageSettingsCache + StaticFileProviderFactory,
32{
33    fn segment(&self) -> PruneSegment {
34        PruneSegment::SenderRecovery
35    }
36
37    fn mode(&self) -> Option<PruneMode> {
38        Some(self.mode)
39    }
40
41    fn purpose(&self) -> PrunePurpose {
42        PrunePurpose::User
43    }
44
45    #[instrument(
46        name = "SenderRecovery::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        if EitherWriterDestination::senders(provider).is_static_file() {
53            debug!(target: "pruner", "Pruning transaction senders from static files.");
54
55            if self.mode.is_full() {
56                debug!(target: "pruner", "PruneMode::Full: deleting all transaction senders static files.");
57                return segments::delete_static_files_segment(
58                    provider,
59                    input,
60                    StaticFileSegment::TransactionSenders,
61                )
62            }
63
64            return segments::prune_static_files(
65                provider,
66                input,
67                StaticFileSegment::TransactionSenders,
68            )
69        }
70        debug!(target: "pruner", "Pruning transaction senders from database.");
71
72        let tx_range = match input.get_next_tx_num_range(provider)? {
73            Some(range) => range,
74            None => {
75                trace!(target: "pruner", "No transaction senders to prune");
76                return Ok(SegmentOutput::done())
77            }
78        };
79        let tx_range_end = *tx_range.end();
80
81        // For PruneMode::Full, clear the entire table in one operation
82        if self.mode.is_full() {
83            let pruned = provider.tx_ref().clear_table::<tables::TransactionSenders>()?;
84            trace!(target: "pruner", %pruned, "Cleared transaction senders table");
85
86            let last_pruned_block = provider
87                .block_by_transaction_id(tx_range_end)?
88                .ok_or(PrunerError::InconsistentData("Block for transaction is not found"))?;
89
90            return Ok(SegmentOutput {
91                progress: PruneProgress::Finished,
92                pruned,
93                checkpoint: Some(SegmentOutputCheckpoint {
94                    block_number: Some(last_pruned_block),
95                    tx_number: Some(tx_range_end),
96                }),
97            });
98        }
99
100        let mut limiter = input.limiter;
101
102        let mut last_pruned_transaction = tx_range_end;
103        let (pruned, done) =
104            provider.tx_ref().prune_table_with_range::<tables::TransactionSenders>(
105                tx_range,
106                &mut limiter,
107                |_| false,
108                |row| last_pruned_transaction = row.0,
109            )?;
110        trace!(target: "pruner", %pruned, %done, "Pruned transaction senders");
111
112        let last_pruned_block = provider
113            .block_by_transaction_id(last_pruned_transaction)?
114            .ok_or(PrunerError::InconsistentData("Block for transaction is not found"))?
115            // If there's more transaction senders to prune, set the checkpoint block number to
116            // previous, so we could finish pruning its transaction senders on the next run.
117            .checked_sub(if done { 0 } else { 1 });
118
119        let progress = limiter.progress(done);
120
121        Ok(SegmentOutput {
122            progress,
123            pruned,
124            checkpoint: Some(SegmentOutputCheckpoint {
125                block_number: last_pruned_block,
126                tx_number: Some(last_pruned_transaction),
127            }),
128        })
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use crate::segments::{PruneInput, PruneLimiter, Segment, SegmentOutput, SenderRecovery};
135    use alloy_primitives::{BlockNumber, TxNumber, B256};
136    use assert_matches::assert_matches;
137    use itertools::{
138        FoldWhile::{Continue, Done},
139        Itertools,
140    };
141    use reth_db_api::tables;
142    use reth_primitives_traits::SignerRecoverable;
143    use reth_provider::{DBProvider, DatabaseProviderFactory, PruneCheckpointReader};
144    use reth_prune_types::{PruneCheckpoint, PruneMode, PruneProgress, PruneSegment};
145    use reth_stages::test_utils::{StorageKind, TestStageDB};
146    use reth_testing_utils::generators::{self, random_block_range, BlockRangeParams};
147    use std::ops::Sub;
148
149    #[test]
150    fn prune() {
151        let db = TestStageDB::default();
152        let mut rng = generators::rng();
153
154        let blocks = random_block_range(
155            &mut rng,
156            1..=10,
157            BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
158        );
159        db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks");
160
161        let mut transaction_senders = Vec::new();
162        for block in &blocks {
163            transaction_senders.reserve_exact(block.transaction_count());
164            for transaction in &block.body().transactions {
165                transaction_senders.push((
166                    transaction_senders.len() as u64,
167                    transaction.recover_signer().expect("recover signer"),
168                ));
169            }
170        }
171        let transaction_senders_len = transaction_senders.len();
172        db.insert_transaction_senders(transaction_senders).expect("insert transaction senders");
173
174        assert_eq!(
175            db.table::<tables::Transactions>().unwrap().len(),
176            blocks.iter().map(|block| block.transaction_count()).sum::<usize>()
177        );
178        assert_eq!(
179            db.table::<tables::Transactions>().unwrap().len(),
180            db.table::<tables::TransactionSenders>().unwrap().len()
181        );
182
183        let test_prune = |to_block: BlockNumber, expected_result: (PruneProgress, usize)| {
184            let prune_mode = PruneMode::Before(to_block);
185            let segment = SenderRecovery::new(prune_mode);
186            let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
187            let input = PruneInput {
188                previous_checkpoint: db
189                    .factory
190                    .provider()
191                    .unwrap()
192                    .get_prune_checkpoint(PruneSegment::SenderRecovery)
193                    .unwrap(),
194                to_block,
195                limiter: limiter.clone(),
196            };
197
198            let next_tx_number_to_prune = db
199                .factory
200                .provider()
201                .unwrap()
202                .get_prune_checkpoint(PruneSegment::SenderRecovery)
203                .unwrap()
204                .and_then(|checkpoint| checkpoint.tx_number)
205                .map(|tx_number| tx_number + 1)
206                .unwrap_or_default();
207
208            let last_pruned_tx_number = blocks
209                .iter()
210                .take(to_block as usize)
211                .map(|block| block.transaction_count())
212                .sum::<usize>()
213                .min(
214                    next_tx_number_to_prune as usize +
215                        input.limiter.deleted_entries_limit().unwrap(),
216                )
217                .sub(1);
218
219            let last_pruned_block_number = blocks
220                .iter()
221                .fold_while((0, 0), |(_, mut tx_count), block| {
222                    tx_count += block.transaction_count();
223
224                    if tx_count > last_pruned_tx_number {
225                        Done((block.number, tx_count))
226                    } else {
227                        Continue((block.number, tx_count))
228                    }
229                })
230                .into_inner()
231                .0;
232
233            let provider = db.factory.database_provider_rw().unwrap();
234            let result = segment.prune(&provider, input).unwrap();
235            limiter.increment_deleted_entries_count_by(result.pruned);
236
237            assert_matches!(
238                result,
239                SegmentOutput {progress, pruned, checkpoint: Some(_)}
240                    if (progress, pruned) == expected_result
241            );
242
243            segment
244                .save_checkpoint(
245                    &provider,
246                    result.checkpoint.unwrap().as_prune_checkpoint(prune_mode),
247                )
248                .unwrap();
249            provider.commit().expect("commit");
250
251            let last_pruned_block_number = last_pruned_block_number
252                .checked_sub(if result.progress.is_finished() { 0 } else { 1 });
253
254            assert_eq!(
255                db.table::<tables::TransactionSenders>().unwrap().len(),
256                transaction_senders_len - (last_pruned_tx_number + 1)
257            );
258            assert_eq!(
259                db.factory
260                    .provider()
261                    .unwrap()
262                    .get_prune_checkpoint(PruneSegment::SenderRecovery)
263                    .unwrap(),
264                Some(PruneCheckpoint {
265                    block_number: last_pruned_block_number,
266                    tx_number: Some(last_pruned_tx_number as TxNumber),
267                    prune_mode
268                })
269            );
270        };
271
272        test_prune(
273            6,
274            (
275                PruneProgress::HasMoreData(
276                    reth_prune_types::PruneInterruptReason::DeletedEntriesLimitReached,
277                ),
278                10,
279            ),
280        );
281        test_prune(6, (PruneProgress::Finished, 2));
282        test_prune(10, (PruneProgress::Finished, 8));
283    }
284}