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