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
30const BATCH_SIZE: usize = 100_000;
34
35const WORKER_CHUNK_SIZE: usize = 100;
37
38type RecoveryResultSender = mpsc::SyncSender<Result<(u64, Address), Box<SenderRecoveryStageError>>>;
40
41#[derive(Clone, Debug)]
45pub struct SenderRecoveryStage {
46 pub commit_threshold: u64,
49 pub prune_mode: Option<PruneMode>,
53}
54
55impl SenderRecoveryStage {
56 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 fn id(&self) -> StageId {
80 StageId::SenderRecovery
81 }
82
83 fn execute(
89 &mut self,
90 provider: &Provider,
91 mut input: ExecInput,
92 ) -> Result<ExecOutput, StageError> {
93 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 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 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 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 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 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 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, 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 let block_number = provider
269 .tx_ref()
270 .get::<tables::TransactionBlocks>(err.tx)?
271 .ok_or(ProviderError::BlockNumberForTransactionIndexNotFound)?;
272
273 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 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
322fn 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 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 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 let _ = recovered_senders_tx
363 .send(Err(Box::new(SenderRecoveryStageError::StageError(err.into()))));
364 break
365 }
366 };
367
368 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 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 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 processed: provider.count_entries::<tables::TransactionSenders>()? as u64 + pruned_entries,
428 total: provider.static_file_provider().count_entries::<tables::Transactions>()? as u64,
432 })
433}
434
435#[derive(Error, Debug)]
436#[error(transparent)]
437enum SenderRecoveryStageError {
438 #[error(transparent)]
440 FailedRecovery(#[from] FailedSenderRecoveryError),
441
442 #[error("mismatched sender count during recovery: {_0}")]
444 RecoveredSendersMismatch(GotExpected<u64>),
445
446 #[error(transparent)]
448 StageError(#[from] StageError),
449}
450
451#[derive(Error, Debug)]
452#[error("sender recovery failed for transaction {tx}")]
453struct FailedSenderRecoveryError {
454 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 #[tokio::test]
485 async fn execute_single_transaction() {
486 let (previous_stage, stage_progress) = (500, 100);
487 let mut rng = generators::rng();
488
489 let runner = SenderRecoveryTestRunner::default();
491 let input = ExecInput {
492 target: Some(previous_stage),
493 checkpoint: Some(StageCheckpoint::new(stage_progress)),
494 };
495
496 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 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 assert!(runner.validate_execution(input, result.ok()).is_ok(), "execution validation");
532 }
533
534 #[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 #[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); 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 ); 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 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 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 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}