Skip to main content

reth_stages/stages/
sender_recovery.rs

1use alloy_primitives::{Address, BlockNumber, TxNumber};
2use reth_config::config::SenderRecoveryConfig;
3use reth_consensus::ConsensusError;
4use reth_db::static_file::TransactionMask;
5use reth_db_api::{
6    cursor::DbCursorRW,
7    table::Value,
8    tables,
9    transaction::{DbTx, DbTxMut},
10    RawValue,
11};
12use reth_primitives_traits::{
13    FastInstant as Instant, GotExpected, NodePrimitives, SignedTransaction,
14};
15use reth_provider::{
16    BlockReader, DBProvider, EitherWriter, HeaderProvider, ProviderError, PruneCheckpointReader,
17    PruneCheckpointWriter, StaticFileProviderFactory, StatsReader, StorageSettingsCache,
18    TransactionsProvider,
19};
20use reth_prune_types::{PruneCheckpoint, PruneMode, PrunePurpose, PruneSegment};
21use reth_stages_api::{
22    BlockErrorKind, EntitiesCheckpoint, ExecInput, ExecOutput, Stage, StageCheckpoint, StageError,
23    StageId, UnwindInput, UnwindOutput,
24};
25use reth_static_file_types::StaticFileSegment;
26use std::{fmt::Debug, ops::Range, sync::mpsc};
27use thiserror::Error;
28use tracing::*;
29
30/// Maximum amount of transactions to read from disk at one time before we flush their senders to
31/// disk. Since each rayon worker will hold at most 100 transactions (`WORKER_CHUNK_SIZE`), we
32/// effectively max limit each batch to 1000 channels in memory.
33const BATCH_SIZE: usize = 100_000;
34
35/// Maximum number of senders to recover per rayon worker job.
36const WORKER_CHUNK_SIZE: usize = 100;
37
38/// Type alias for a sender that transmits the result of sender recovery.
39type RecoveryResultSender = mpsc::SyncSender<Result<(u64, Address), Box<SenderRecoveryStageError>>>;
40
41/// The sender recovery stage iterates over existing transactions,
42/// recovers the transaction signer and stores them
43/// in [`TransactionSenders`][reth_db_api::tables::TransactionSenders] table.
44#[derive(Clone, Debug)]
45pub struct SenderRecoveryStage {
46    /// The size of inserted items after which the control
47    /// flow will be returned to the pipeline for commit
48    pub commit_threshold: u64,
49    /// Prune mode for sender recovery. When set to `PruneMode::Full`, the stage will
50    /// fast-forward its checkpoint to skip all work, since senders will be recovered
51    /// inline by the execution stage instead.
52    pub prune_mode: Option<PruneMode>,
53}
54
55impl SenderRecoveryStage {
56    /// Create new instance of [`SenderRecoveryStage`].
57    pub const fn new(config: SenderRecoveryConfig, prune_mode: Option<PruneMode>) -> Self {
58        Self { commit_threshold: config.commit_threshold, prune_mode }
59    }
60}
61
62impl Default for SenderRecoveryStage {
63    fn default() -> Self {
64        Self { commit_threshold: 5_000_000, prune_mode: None }
65    }
66}
67
68impl<Provider> Stage<Provider> for SenderRecoveryStage
69where
70    Provider: DBProvider<Tx: DbTxMut>
71        + BlockReader
72        + StaticFileProviderFactory<Primitives: NodePrimitives<SignedTx: Value + SignedTransaction>>
73        + StatsReader
74        + PruneCheckpointReader
75        + PruneCheckpointWriter
76        + StorageSettingsCache,
77{
78    /// Return the id of the stage
79    fn id(&self) -> StageId {
80        StageId::SenderRecovery
81    }
82
83    /// Retrieve the range of transactions to iterate over by querying
84    /// [`BlockBodyIndices`][reth_db_api::tables::BlockBodyIndices],
85    /// collect transactions within that range, recover signer for each transaction and store
86    /// entries in the [`TransactionSenders`][reth_db_api::tables::TransactionSenders] table or
87    /// static files depending on configuration.
88    fn execute(
89        &mut self,
90        provider: &Provider,
91        mut input: ExecInput,
92    ) -> Result<ExecOutput, StageError> {
93        // TODO: when senders are fully pruned, batch recover in execution stage instead of per-tx
94        // fallback
95        if let Some((target_prunable_block, prune_mode)) = self
96            .prune_mode
97            .map(|mode| {
98                mode.prune_target_block(
99                    input.target(),
100                    PruneSegment::SenderRecovery,
101                    PrunePurpose::User,
102                )
103            })
104            .transpose()?
105            .flatten() &&
106            target_prunable_block > input.checkpoint().block_number
107        {
108            input.checkpoint = Some(StageCheckpoint::new(target_prunable_block));
109
110            if provider.get_prune_checkpoint(PruneSegment::SenderRecovery)?.is_none() {
111                let target_prunable_tx_number = provider
112                    .block_body_indices(target_prunable_block)?
113                    .ok_or(ProviderError::BlockBodyIndicesNotFound(target_prunable_block))?
114                    .last_tx_num();
115
116                provider.save_prune_checkpoint(
117                    PruneSegment::SenderRecovery,
118                    PruneCheckpoint {
119                        block_number: Some(target_prunable_block),
120                        tx_number: Some(target_prunable_tx_number),
121                        prune_mode,
122                    },
123                )?;
124            }
125        }
126
127        if input.target_reached() {
128            return Ok(ExecOutput::done(input.checkpoint()))
129        }
130
131        let Some(range_output) =
132            input.next_block_range_with_transaction_threshold(provider, self.commit_threshold)?
133        else {
134            info!(target: "sync::stages::sender_recovery", "No transaction senders to recover");
135            EitherWriter::new_senders(
136                provider,
137                provider
138                    .static_file_provider()
139                    .get_highest_static_file_block(StaticFileSegment::TransactionSenders)
140                    .unwrap_or_default(),
141            )?
142            .ensure_at_block(input.target())?;
143            return Ok(ExecOutput::done(
144                StageCheckpoint::new(input.target())
145                    .with_entities_stage_checkpoint(stage_checkpoint(provider)?),
146            ))
147        };
148        let end_block = *range_output.block_range.end();
149
150        let mut writer = EitherWriter::new_senders(provider, *range_output.block_range.start())?;
151
152        info!(target: "sync::stages::sender_recovery", tx_range = ?range_output.tx_range, "Recovering senders");
153
154        // Iterate over transactions in batches, recover the senders and append them
155        let batch = range_output
156            .tx_range
157            .clone()
158            .step_by(BATCH_SIZE)
159            .map(|start| start..std::cmp::min(start + BATCH_SIZE as u64, range_output.tx_range.end))
160            .collect::<Vec<Range<u64>>>();
161
162        let tx_batch_sender = setup_range_recovery(provider);
163
164        let start = Instant::now();
165        let block_body_indices =
166            provider.block_body_indices_range(range_output.block_range.clone())?;
167        let block_body_indices_elapsed = start.elapsed();
168        let mut blocks_with_indices = range_output.block_range.zip(block_body_indices).peekable();
169
170        for range in batch {
171            // Pair each transaction number with its block number
172            let start = Instant::now();
173            let block_numbers = range.clone().fold(Vec::new(), |mut block_numbers, tx| {
174                while let Some((block, index)) = blocks_with_indices.peek() {
175                    if index.contains_tx(tx) {
176                        block_numbers.push(*block);
177                        return block_numbers
178                    }
179                    blocks_with_indices.next();
180                }
181                block_numbers
182            });
183            let fold_elapsed = start.elapsed();
184            debug!(target: "sync::stages::sender_recovery", ?block_body_indices_elapsed, ?fold_elapsed, len = block_numbers.len(), "Calculated block numbers");
185            recover_range(range, block_numbers, provider, tx_batch_sender.clone(), &mut writer)?;
186        }
187
188        // Advance the static file header to the end of this range to account for empty blocks.
189        writer.ensure_at_block(end_block)?;
190
191        Ok(ExecOutput {
192            checkpoint: StageCheckpoint::new(end_block)
193                .with_entities_stage_checkpoint(stage_checkpoint(provider)?),
194            done: range_output.is_final_range,
195        })
196    }
197
198    /// Unwind the stage.
199    fn unwind(
200        &mut self,
201        provider: &Provider,
202        input: UnwindInput,
203    ) -> Result<UnwindOutput, StageError> {
204        let (_, unwind_to, _) = input.unwind_block_range_with_threshold(self.commit_threshold);
205
206        if self.prune_mode.is_none_or(|mode| !mode.is_full()) {
207            // Lookup the next tx id after unwind_to block (first tx to remove)
208            let unwind_tx_from = provider.next_tx_num_after_block(unwind_to)?;
209
210            EitherWriter::new_senders(provider, unwind_to)?
211                .prune_senders(unwind_tx_from, unwind_to)?;
212        }
213
214        Ok(UnwindOutput {
215            checkpoint: StageCheckpoint::new(unwind_to)
216                .with_entities_stage_checkpoint(stage_checkpoint(provider)?),
217        })
218    }
219}
220
221fn recover_range<Provider, CURSOR>(
222    tx_range: Range<TxNumber>,
223    block_numbers: Vec<BlockNumber>,
224    provider: &Provider,
225    tx_batch_sender: mpsc::Sender<Vec<(Range<u64>, RecoveryResultSender)>>,
226    writer: &mut EitherWriter<'_, CURSOR, Provider::Primitives>,
227) -> Result<(), StageError>
228where
229    Provider: DBProvider + HeaderProvider + TransactionsProvider + StaticFileProviderFactory,
230    CURSOR: DbCursorRW<tables::TransactionSenders>,
231{
232    debug_assert_eq!(
233        tx_range.clone().count(),
234        block_numbers.len(),
235        "Transaction range and block numbers count mismatch"
236    );
237
238    debug!(target: "sync::stages::sender_recovery", ?tx_range, "Sending batch for processing");
239
240    // Preallocate channels for each chunks in the batch
241    let (chunks, receivers): (Vec<_>, Vec<_>) = tx_range
242        .clone()
243        .step_by(WORKER_CHUNK_SIZE)
244        .map(|start| {
245            let range = start..std::cmp::min(start + WORKER_CHUNK_SIZE as u64, tx_range.end);
246            let (tx, rx) = mpsc::sync_channel((range.end - range.start) as usize);
247            // Range and channel sender will be sent to rayon worker
248            ((range, tx), rx)
249        })
250        .unzip();
251
252    if let Some(err) = tx_batch_sender.send(chunks).err() {
253        return Err(StageError::Fatal(err.into()));
254    }
255
256    debug!(target: "sync::stages::sender_recovery", ?tx_range, "Appending recovered senders to the database");
257
258    let mut processed_transactions = 0;
259    let mut block_numbers = block_numbers.into_iter();
260    for channel in receivers {
261        while let Ok(recovered) = channel.recv() {
262            let (tx_id, sender) = match recovered {
263                Ok(result) => result,
264                Err(error) => {
265                    return match *error {
266                        SenderRecoveryStageError::FailedRecovery(err) => {
267                            // get the block number for the bad transaction
268                            let block_number = provider
269                                .tx_ref()
270                                .get::<tables::TransactionBlocks>(err.tx)?
271                                .ok_or(ProviderError::BlockNumberForTransactionIndexNotFound)?;
272
273                            // fetch the sealed header so we can use it in the sender recovery
274                            // unwind
275                            let sealed_header =
276                                provider.sealed_header(block_number)?.ok_or_else(|| {
277                                    ProviderError::HeaderNotFound(block_number.into())
278                                })?;
279
280                            Err(StageError::Block {
281                                block: Box::new(sealed_header.block_with_parent()),
282                                error: BlockErrorKind::Validation(
283                                    ConsensusError::TransactionSignerRecoveryError,
284                                ),
285                            })
286                        }
287                        SenderRecoveryStageError::StageError(err) => Err(err),
288                        SenderRecoveryStageError::RecoveredSendersMismatch(expectation) => {
289                            Err(StageError::Fatal(
290                                SenderRecoveryStageError::RecoveredSendersMismatch(expectation)
291                                    .into(),
292                            ))
293                        }
294                    }
295                }
296            };
297
298            let new_block_number = block_numbers
299                .next()
300                .expect("block numbers iterator has the same length as the number of transactions");
301            writer.ensure_at_block(new_block_number)?;
302            writer.append_sender(tx_id, &sender)?;
303            processed_transactions += 1;
304        }
305    }
306    debug!(target: "sync::stages::sender_recovery", ?tx_range, "Finished recovering senders batch");
307
308    // Fail safe to ensure that we do not proceed without having recovered all senders.
309    let expected = tx_range.end - tx_range.start;
310    if processed_transactions != expected {
311        return Err(StageError::Fatal(
312            SenderRecoveryStageError::RecoveredSendersMismatch(GotExpected {
313                got: processed_transactions,
314                expected,
315            })
316            .into(),
317        ));
318    }
319    Ok(())
320}
321
322/// Spawns a thread to handle the recovery of transaction senders for
323/// specified chunks of a given batch. It processes incoming ranges, fetching and recovering
324/// transactions in parallel using global rayon pool
325fn setup_range_recovery<Provider>(
326    provider: &Provider,
327) -> mpsc::Sender<Vec<(Range<u64>, RecoveryResultSender)>>
328where
329    Provider: DBProvider
330        + HeaderProvider
331        + StaticFileProviderFactory<Primitives: NodePrimitives<SignedTx: Value + SignedTransaction>>,
332{
333    let (tx_sender, tx_receiver) = mpsc::channel::<Vec<(Range<u64>, RecoveryResultSender)>>();
334    let static_file_provider = provider.static_file_provider();
335
336    // We do not use `tokio::task::spawn_blocking` because, during a shutdown,
337    // there will be a timeout grace period in which Tokio does not allow spawning
338    // additional blocking tasks. This would cause this function to return
339    // `SenderRecoveryStageError::RecoveredSendersMismatch` at the end.
340    //
341    // However, using `std::thread::spawn` allows us to utilize the timeout grace
342    // period to complete some work without throwing errors during the shutdown.
343    reth_tasks::spawn_os_thread("sender-recovery", move || {
344        while let Ok(chunks) = tx_receiver.recv() {
345            for (chunk_range, recovered_senders_tx) in chunks {
346                // Read the raw value, and let the rayon worker to decompress & decode.
347                let chunk = match static_file_provider.fetch_range_with_predicate(
348                    StaticFileSegment::Transactions,
349                    chunk_range,
350                    |cursor, number| {
351                        Ok(cursor
352                            .get_one::<TransactionMask<
353                                RawValue<<Provider::Primitives as NodePrimitives>::SignedTx>,
354                            >>(number.into())?
355                            .map(|tx| (number, tx)))
356                    },
357                    |_| true,
358                ) {
359                    Ok(chunk) => chunk,
360                    Err(err) => {
361                        // We exit early since we could not process this chunk.
362                        let _ = recovered_senders_tx
363                            .send(Err(Box::new(SenderRecoveryStageError::StageError(err.into()))));
364                        break
365                    }
366                };
367
368                // Spawn the task onto the global rayon pool
369                // This task will send the results through the channel after it has read the
370                // transaction and calculated the sender.
371                rayon::spawn(move || {
372                    let mut rlp_buf = Vec::with_capacity(128);
373                    for (number, tx) in chunk {
374                        let res = tx
375                            .value()
376                            .map_err(|err| {
377                                Box::new(SenderRecoveryStageError::StageError(err.into()))
378                            })
379                            .and_then(|tx| recover_sender((number, tx), &mut rlp_buf));
380
381                        let is_err = res.is_err();
382
383                        let _ = recovered_senders_tx.send(res);
384
385                        // Finish early
386                        if is_err {
387                            break
388                        }
389                    }
390                });
391            }
392        }
393    });
394    tx_sender
395}
396
397#[inline]
398fn recover_sender<T: SignedTransaction>(
399    (tx_id, tx): (TxNumber, T),
400    rlp_buf: &mut Vec<u8>,
401) -> Result<(u64, Address), Box<SenderRecoveryStageError>> {
402    rlp_buf.clear();
403    // We call [Signature::encode_and_recover_unchecked] because transactions run in the pipeline
404    // are known to be valid - this means that we do not need to check whether or not the `s`
405    // value is greater than `secp256k1n / 2` if past EIP-2. There are transactions
406    // pre-homestead which have large `s` values, so using [Signature::recover_signer] here
407    // would not be backwards-compatible.
408    let sender = tx.recover_unchecked_with_buf(rlp_buf).map_err(|_| {
409        SenderRecoveryStageError::FailedRecovery(FailedSenderRecoveryError { tx: tx_id })
410    })?;
411
412    Ok((tx_id, sender))
413}
414
415fn stage_checkpoint<Provider>(provider: &Provider) -> Result<EntitiesCheckpoint, StageError>
416where
417    Provider: StatsReader + StaticFileProviderFactory + PruneCheckpointReader,
418{
419    let pruned_entries = provider
420        .get_prune_checkpoint(PruneSegment::SenderRecovery)?
421        .and_then(|checkpoint| checkpoint.tx_number)
422        .unwrap_or_default();
423    Ok(EntitiesCheckpoint {
424        // If `TransactionSenders` table was pruned, we will have a number of entries in it not
425        // matching the actual number of processed transactions. To fix that, we add the
426        // number of pruned `TransactionSenders` entries.
427        processed: provider.count_entries::<tables::TransactionSenders>()? as u64 + pruned_entries,
428        // Count only static files entries. If we count the database entries too, we may have
429        // duplicates. We're sure that the static files have all entries that database has,
430        // because we run the `StaticFileProducer` before starting the pipeline.
431        total: provider.static_file_provider().count_entries::<tables::Transactions>()? as u64,
432    })
433}
434
435#[derive(Error, Debug)]
436#[error(transparent)]
437enum SenderRecoveryStageError {
438    /// A transaction failed sender recovery
439    #[error(transparent)]
440    FailedRecovery(#[from] FailedSenderRecoveryError),
441
442    /// Number of recovered senders does not match
443    #[error("mismatched sender count during recovery: {_0}")]
444    RecoveredSendersMismatch(GotExpected<u64>),
445
446    /// A different type of stage error occurred
447    #[error(transparent)]
448    StageError(#[from] StageError),
449}
450
451#[derive(Error, Debug)]
452#[error("sender recovery failed for transaction {tx}")]
453struct FailedSenderRecoveryError {
454    /// The transaction that failed sender recovery
455    tx: TxNumber,
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461    use crate::test_utils::{
462        stage_test_suite_ext, ExecuteStageTestRunner, StageTestRunner, StorageKind,
463        TestRunnerError, TestStageDB, UnwindStageTestRunner,
464    };
465    use alloy_primitives::{BlockNumber, B256};
466    use assert_matches::assert_matches;
467    use reth_db_api::{cursor::DbCursorRO, models::StorageSettings};
468    use reth_ethereum_primitives::{Block, TransactionSigned};
469    use reth_primitives_traits::{SealedBlock, SignerRecoverable};
470    use reth_provider::{
471        providers::StaticFileWriter, BlockBodyIndicesProvider, DatabaseProviderFactory,
472        PruneCheckpointWriter, StaticFileProviderFactory, TransactionsProvider,
473    };
474    use reth_prune_types::{PruneCheckpoint, PruneMode};
475    use reth_stages_api::StageUnitCheckpoint;
476    use reth_static_file_types::StaticFileSegment;
477    use reth_testing_utils::generators::{
478        self, random_block, random_block_range, BlockParams, BlockRangeParams,
479    };
480
481    stage_test_suite_ext!(SenderRecoveryTestRunner, sender_recovery);
482
483    /// Execute a block range with a single transaction
484    #[tokio::test]
485    async fn execute_single_transaction() {
486        let (previous_stage, stage_progress) = (500, 100);
487        let mut rng = generators::rng();
488
489        // Set up the runner
490        let runner = SenderRecoveryTestRunner::default();
491        let input = ExecInput {
492            target: Some(previous_stage),
493            checkpoint: Some(StageCheckpoint::new(stage_progress)),
494        };
495
496        // Insert blocks with a single transaction at block `stage_progress + 10`
497        let non_empty_block_number = stage_progress + 10;
498        let blocks = (stage_progress..=input.target())
499            .map(|number| {
500                random_block(
501                    &mut rng,
502                    number,
503                    BlockParams {
504                        tx_count: Some((number == non_empty_block_number) as u8),
505                        ..Default::default()
506                    },
507                )
508            })
509            .collect::<Vec<_>>();
510        runner
511            .db
512            .insert_blocks(blocks.iter(), StorageKind::Static)
513            .expect("failed to insert blocks");
514
515        let rx = runner.execute(input);
516
517        // Assert the successful result
518        let result = rx.await.unwrap();
519        assert_matches!(
520            result,
521            Ok(ExecOutput { checkpoint: StageCheckpoint {
522                block_number,
523                stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
524                    processed: 1,
525                    total: 1
526                }))
527            }, done: true }) if block_number == previous_stage
528        );
529
530        // Validate the stage execution
531        assert!(runner.validate_execution(input, result.ok()).is_ok(), "execution validation");
532    }
533
534    /// Ensure the static file header advances to trailing empty blocks.
535    #[tokio::test]
536    async fn execute_advances_static_file_for_trailing_empty_blocks() {
537        let (stage_progress, target) = (0, 3);
538        let mut rng = generators::rng();
539
540        let runner = SenderRecoveryTestRunner::default();
541        runner.db.factory.set_storage_settings_cache(StorageSettings::v2());
542        let input = ExecInput {
543            target: Some(target),
544            checkpoint: Some(StageCheckpoint::new(stage_progress)),
545        };
546
547        let non_empty_block_number = stage_progress + 1;
548        let blocks = (stage_progress..=input.target())
549            .map(|number| {
550                random_block(
551                    &mut rng,
552                    number,
553                    BlockParams {
554                        tx_count: Some((number == non_empty_block_number) as u8),
555                        ..Default::default()
556                    },
557                )
558            })
559            .collect::<Vec<_>>();
560        runner
561            .db
562            .insert_blocks(blocks.iter(), StorageKind::Static)
563            .expect("failed to insert blocks");
564
565        let result = runner.execute(input).await.unwrap();
566        assert_matches!(result, Ok(ExecOutput { checkpoint, done: true }) if checkpoint.block_number == target);
567
568        let highest_block = runner
569            .db
570            .factory
571            .static_file_provider()
572            .get_highest_static_file_block(StaticFileSegment::TransactionSenders);
573        assert_eq!(Some(target), highest_block);
574    }
575
576    /// Execute the stage twice with input range that exceeds the commit threshold
577    #[tokio::test]
578    async fn execute_intermediate_commit() {
579        let mut rng = generators::rng();
580
581        let threshold = 10;
582        let mut runner = SenderRecoveryTestRunner::default();
583        runner.set_threshold(threshold);
584        let (stage_progress, previous_stage) = (1000, 1100); // input exceeds threshold
585
586        // Manually seed once with full input range
587        let seed = random_block_range(
588            &mut rng,
589            stage_progress + 1..=previous_stage,
590            BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..4, ..Default::default() },
591        ); // set tx count range high enough to hit the threshold
592        runner
593            .db
594            .insert_blocks(seed.iter(), StorageKind::Static)
595            .expect("failed to seed execution");
596
597        let total_transactions = runner
598            .db
599            .factory
600            .static_file_provider()
601            .count_entries::<tables::Transactions>()
602            .unwrap() as u64;
603
604        let first_input = ExecInput {
605            target: Some(previous_stage),
606            checkpoint: Some(StageCheckpoint::new(stage_progress)),
607        };
608
609        // Execute first time
610        let result = runner.execute(first_input).await.unwrap();
611        let mut tx_count = 0;
612        let expected_progress = seed
613            .iter()
614            .find(|x| {
615                tx_count += x.transaction_count();
616                tx_count as u64 > threshold
617            })
618            .map(|x| x.number)
619            .unwrap_or(previous_stage);
620        assert_matches!(result, Ok(_));
621        assert_eq!(
622            result.unwrap(),
623            ExecOutput::in_progress(
624                StageCheckpoint::new(expected_progress).with_entities_stage_checkpoint(
625                    EntitiesCheckpoint {
626                        processed: runner.db.count_entries::<tables::TransactionSenders>().unwrap()
627                            as u64,
628                        total: total_transactions
629                    }
630                )
631            )
632        );
633
634        // Execute second time to completion
635        runner.set_threshold(u64::MAX);
636        let second_input = ExecInput {
637            target: Some(previous_stage),
638            checkpoint: Some(StageCheckpoint::new(expected_progress)),
639        };
640        let result = runner.execute(second_input).await.unwrap();
641        assert_matches!(result, Ok(_));
642        assert_eq!(
643            result.as_ref().unwrap(),
644            &ExecOutput::done(StageCheckpoint::new(previous_stage).with_entities_stage_checkpoint(
645                EntitiesCheckpoint { processed: total_transactions, total: total_transactions }
646            ))
647        );
648
649        assert!(runner.validate_execution(first_input, result.ok()).is_ok(), "validation failed");
650    }
651
652    #[test]
653    fn stage_checkpoint_pruned() {
654        let db = TestStageDB::default();
655        let mut rng = generators::rng();
656
657        let blocks = random_block_range(
658            &mut rng,
659            0..=100,
660            BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..10, ..Default::default() },
661        );
662        db.insert_blocks(blocks.iter(), StorageKind::Static).expect("insert blocks");
663
664        let max_pruned_block = 30;
665        let max_processed_block = 70;
666
667        let mut tx_senders = Vec::new();
668        let mut tx_number = 0;
669        for block in &blocks[..=max_processed_block] {
670            for transaction in &block.body().transactions {
671                if block.number > max_pruned_block {
672                    tx_senders
673                        .push((tx_number, transaction.recover_signer().expect("recover signer")));
674                }
675                tx_number += 1;
676            }
677        }
678        db.insert_transaction_senders(tx_senders).expect("insert tx hash numbers");
679
680        let provider = db.factory.provider_rw().unwrap();
681        provider
682            .save_prune_checkpoint(
683                PruneSegment::SenderRecovery,
684                PruneCheckpoint {
685                    block_number: Some(max_pruned_block),
686                    tx_number: Some(
687                        blocks[..=max_pruned_block as usize]
688                            .iter()
689                            .map(|block| block.transaction_count() as u64)
690                            .sum(),
691                    ),
692                    prune_mode: PruneMode::Full,
693                },
694            )
695            .expect("save stage checkpoint");
696        provider.commit().expect("commit");
697
698        let provider = db.factory.database_provider_rw().unwrap();
699        assert_eq!(
700            stage_checkpoint(&provider).expect("stage checkpoint"),
701            EntitiesCheckpoint {
702                processed: blocks[..=max_processed_block]
703                    .iter()
704                    .map(|block| block.transaction_count() as u64)
705                    .sum(),
706                total: blocks.iter().map(|block| block.transaction_count() as u64).sum()
707            }
708        );
709    }
710
711    struct SenderRecoveryTestRunner {
712        db: TestStageDB,
713        threshold: u64,
714    }
715
716    impl Default for SenderRecoveryTestRunner {
717        fn default() -> Self {
718            Self { threshold: 1000, db: TestStageDB::default() }
719        }
720    }
721
722    impl SenderRecoveryTestRunner {
723        fn set_threshold(&mut self, threshold: u64) {
724            self.threshold = threshold;
725        }
726
727        /// # Panics
728        ///
729        /// 1. If there are any entries in the [`tables::TransactionSenders`] table above a given
730        ///    block number.
731        /// 2. If there is no requested block entry in the bodies table, but
732        ///    [`tables::TransactionSenders`] is not empty.
733        fn ensure_no_senders_by_block(&self, block: BlockNumber) -> Result<(), TestRunnerError> {
734            let body_result = self
735                .db
736                .factory
737                .provider_rw()?
738                .block_body_indices(block)?
739                .ok_or(ProviderError::BlockBodyIndicesNotFound(block));
740            match body_result {
741                Ok(body) => self.db.ensure_no_entry_above::<tables::TransactionSenders, _>(
742                    body.last_tx_num(),
743                    |key| key,
744                )?,
745                Err(_) => {
746                    assert!(self.db.table_is_empty::<tables::TransactionSenders>()?);
747                }
748            };
749
750            Ok(())
751        }
752    }
753
754    impl StageTestRunner for SenderRecoveryTestRunner {
755        type S = SenderRecoveryStage;
756
757        fn db(&self) -> &TestStageDB {
758            &self.db
759        }
760
761        fn stage(&self) -> Self::S {
762            SenderRecoveryStage { commit_threshold: self.threshold, prune_mode: None }
763        }
764    }
765
766    impl ExecuteStageTestRunner for SenderRecoveryTestRunner {
767        type Seed = Vec<SealedBlock<Block>>;
768
769        fn seed_execution(&mut self, input: ExecInput) -> Result<Self::Seed, TestRunnerError> {
770            let mut rng = generators::rng();
771            let stage_progress = input.checkpoint().block_number;
772            let end = input.target();
773
774            let blocks = random_block_range(
775                &mut rng,
776                stage_progress..=end,
777                BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..2, ..Default::default() },
778            );
779            self.db.insert_blocks(blocks.iter(), StorageKind::Static)?;
780            Ok(blocks)
781        }
782
783        fn validate_execution(
784            &self,
785            input: ExecInput,
786            output: Option<ExecOutput>,
787        ) -> Result<(), TestRunnerError> {
788            match output {
789                Some(output) => {
790                    let provider = self.db.factory.provider()?;
791                    let start_block = input.next_block();
792                    let end_block = output.checkpoint.block_number;
793
794                    if start_block > end_block {
795                        return Ok(())
796                    }
797
798                    let mut body_cursor =
799                        provider.tx_ref().cursor_read::<tables::BlockBodyIndices>()?;
800                    body_cursor.seek_exact(start_block)?;
801
802                    while let Some((_, body)) = body_cursor.next()? {
803                        for tx_id in body.tx_num_range() {
804                            let transaction: TransactionSigned = provider
805                                .transaction_by_id_unhashed(tx_id)?
806                                .expect("no transaction entry");
807                            let signer =
808                                transaction.recover_signer().expect("failed to recover signer");
809                            assert_eq!(Some(signer), provider.transaction_sender(tx_id)?)
810                        }
811                    }
812                }
813                None => self.ensure_no_senders_by_block(input.checkpoint().block_number)?,
814            };
815
816            Ok(())
817        }
818    }
819
820    impl UnwindStageTestRunner for SenderRecoveryTestRunner {
821        fn validate_unwind(&self, input: UnwindInput) -> Result<(), TestRunnerError> {
822            self.ensure_no_senders_by_block(input.unwind_to)
823        }
824    }
825}