reth_prune/segments/user/
sender_recovery.rs1use 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 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 .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}