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
23const BATCH_SIZE: usize = 100_000;
27
28const WORKER_CHUNK_SIZE: usize = 100;
30
31type RecoveryResultSender = mpsc::SyncSender<Result<(u64, Address), Box<SenderRecoveryStageError>>>;
33
34#[derive(Clone, Debug)]
38pub struct SenderRecoveryStage {
39 pub commit_threshold: u64,
42 pub prune_mode: Option<PruneMode>,
46}
47
48impl SenderRecoveryStage {
49 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 fn id(&self) -> StageId {
73 StageId::SenderRecovery
74 }
75
76 fn execute(
82 &mut self,
83 provider: &Provider,
84 mut input: ExecInput,
85 ) -> Result<ExecOutput, StageError> {
86 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 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 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 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 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 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 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, 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 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 let block_number = block_numbers
285 .next()
286 .ok_or(ProviderError::BlockNumberForTransactionIndexNotFound)?;
287
288 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 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
337fn 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 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 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 let _ = recovered_senders_tx
378 .send(Err(Box::new(SenderRecoveryStageError::StageError(err.into()))));
379 break
380 }
381 };
382
383 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 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 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 processed: provider.count_entries::<tables::TransactionSenders>()? as u64 + pruned_entries,
443 total: provider.static_file_provider().count_entries::<tables::Transactions>()? as u64,
447 })
448}
449
450#[derive(Error, Debug)]
451#[error(transparent)]
452enum SenderRecoveryStageError {
453 #[error(transparent)]
455 FailedRecovery(#[from] FailedSenderRecoveryError),
456
457 #[error("mismatched sender count during recovery: {_0}")]
459 RecoveredSendersMismatch(GotExpected<u64>),
460
461 #[error(transparent)]
463 StageError(#[from] StageError),
464}
465
466#[derive(Error, Debug)]
467#[error("sender recovery failed for transaction {tx}")]
468struct FailedSenderRecoveryError {
469 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 #[tokio::test]
501 async fn execute_single_transaction() {
502 let (previous_stage, stage_progress) = (500, 100);
503 let mut rng = generators::rng();
504
505 let runner = SenderRecoveryTestRunner::default();
507 let input = ExecInput {
508 target: Some(previous_stage),
509 checkpoint: Some(StageCheckpoint::new(stage_progress)),
510 };
511
512 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 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 assert!(runner.validate_execution(input, result.ok()).is_ok(), "execution validation");
548 }
549
550 #[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 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 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 #[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 #[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); 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 ); 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 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 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 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}