1use super::missing_static_data_error;
2use futures_util::TryStreamExt;
3use reth_db_api::{
4 cursor::DbCursorRO,
5 tables,
6 transaction::{DbTx, DbTxMut},
7};
8use reth_network_p2p::bodies::{downloader::BodyDownloader, response::BlockResponse};
9use reth_provider::{
10 providers::StaticFileWriter, BlockReader, BlockWriter, DBProvider, StaticFileProviderFactory,
11 StatsReader,
12};
13use reth_stages_api::{
14 EntitiesCheckpoint, ExecInput, ExecOutput, Stage, StageCheckpoint, StageError, StageId,
15 UnwindInput, UnwindOutput,
16};
17use reth_static_file_types::StaticFileSegment;
18use reth_storage_errors::provider::ProviderResult;
19use std::{
20 cmp::Ordering,
21 task::{ready, Context, Poll},
22};
23use tracing::*;
24
25#[derive(Debug)]
56pub struct BodyStage<D: BodyDownloader> {
57 downloader: D,
59 buffer: Option<Vec<BlockResponse<D::Block>>>,
61}
62
63impl<D: BodyDownloader> BodyStage<D> {
64 pub const fn new(downloader: D) -> Self {
66 Self { downloader, buffer: None }
67 }
68}
69
70pub(crate) fn ensure_consistency<Provider>(
72 provider: &Provider,
73 checkpoint: u64,
74 unwind_block: Option<u64>,
75) -> Result<(), StageError>
76where
77 Provider: DBProvider<Tx: DbTxMut> + BlockReader + StaticFileProviderFactory,
78{
79 let next_tx_num = provider
81 .tx_ref()
82 .cursor_read::<tables::TransactionBlocks>()?
83 .last()?
84 .map(|(id, _)| id + 1)
85 .unwrap_or_default();
86
87 let static_file_provider = provider.static_file_provider();
88
89 let next_static_file_tx_num = static_file_provider
92 .get_highest_static_file_tx(StaticFileSegment::Transactions)
93 .map(|id| id + 1)
94 .unwrap_or_default();
95
96 match next_static_file_tx_num.cmp(&next_tx_num) {
97 Ordering::Greater | Ordering::Equal => {
101 let highest_db_block = provider
102 .tx_ref()
103 .cursor_read::<tables::BlockBodyIndices>()?
104 .last()?
105 .map(|(block, _)| block)
106 .unwrap_or(checkpoint);
108 if next_static_file_tx_num > next_tx_num ||
110 static_file_provider
111 .get_highest_static_file_block(StaticFileSegment::Transactions)
112 .is_some_and(|block| block > highest_db_block)
113 {
114 let mut static_file_producer =
115 static_file_provider.latest_writer(StaticFileSegment::Transactions)?;
116 static_file_producer
117 .prune_transactions(next_static_file_tx_num - next_tx_num, highest_db_block)?;
118 static_file_producer.commit()?;
121 }
122 }
123 Ordering::Less => {
127 if let Some(unwind_to) = unwind_block {
130 let next_tx_num_after_unwind = provider.next_tx_num_after_block(unwind_to)?;
131
132 if next_tx_num_after_unwind > next_static_file_tx_num {
134 return Err(missing_static_data_error(
135 next_static_file_tx_num.saturating_sub(1),
136 &static_file_provider,
137 provider,
138 StaticFileSegment::Transactions,
139 )?)
140 }
141 } else {
142 return Err(missing_static_data_error(
143 next_static_file_tx_num.saturating_sub(1),
144 &static_file_provider,
145 provider,
146 StaticFileSegment::Transactions,
147 )?)
148 }
149 }
150 }
151
152 Ok(())
153}
154
155impl<Provider, D> Stage<Provider> for BodyStage<D>
156where
157 Provider: DBProvider<Tx: DbTxMut>
158 + StaticFileProviderFactory
159 + StatsReader
160 + BlockReader
161 + BlockWriter<Block = D::Block>,
162 D: BodyDownloader,
163{
164 fn id(&self) -> StageId {
166 StageId::Bodies
167 }
168
169 fn poll_execute_ready(
170 &mut self,
171 cx: &mut Context<'_>,
172 input: ExecInput,
173 ) -> Poll<Result<(), StageError>> {
174 if input.target_reached() || self.buffer.is_some() {
175 return Poll::Ready(Ok(()))
176 }
177
178 self.downloader.set_download_range(input.next_block_range())?;
180
181 let maybe_next_result = ready!(self.downloader.try_poll_next_unpin(cx));
183
184 let response = match maybe_next_result {
187 Some(Ok(downloaded)) => {
188 self.buffer = Some(downloaded);
189 Ok(())
190 }
191 Some(Err(err)) => Err(err.into()),
192 None => Err(StageError::ChannelClosed),
193 };
194 Poll::Ready(response)
195 }
196
197 fn execute(&mut self, provider: &Provider, input: ExecInput) -> Result<ExecOutput, StageError> {
200 if input.target_reached() {
201 return Ok(ExecOutput::done(input.checkpoint()))
202 }
203 let (from_block, to_block) = input.next_block_range().into_inner();
204
205 ensure_consistency(provider, input.checkpoint().block_number, None)?;
206
207 debug!(target: "sync::stages::bodies", stage_progress = from_block, target = to_block, "Commencing sync");
208
209 let buffer = self.buffer.take().ok_or(StageError::MissingDownloadBuffer)?;
210 trace!(target: "sync::stages::bodies", bodies_len = buffer.len(), "Writing blocks");
211 let highest_block = buffer.last().map(|r| r.block_number()).unwrap_or(from_block);
212
213 provider.append_block_bodies(
215 buffer.iter().map(|response| (response.block_number(), response.body())).collect(),
216 )?;
217
218 let done = highest_block == to_block;
222 Ok(ExecOutput {
223 checkpoint: StageCheckpoint::new(highest_block)
224 .with_entities_stage_checkpoint(stage_checkpoint(provider)?),
225 done,
226 })
227 }
228
229 fn unwind(
231 &mut self,
232 provider: &Provider,
233 input: UnwindInput,
234 ) -> Result<UnwindOutput, StageError> {
235 self.buffer.take();
236
237 ensure_consistency(provider, input.checkpoint.block_number, Some(input.unwind_to))?;
238 provider.remove_bodies_above(input.unwind_to)?;
239
240 Ok(UnwindOutput {
241 checkpoint: StageCheckpoint::new(input.unwind_to)
242 .with_entities_stage_checkpoint(stage_checkpoint(provider)?),
243 })
244 }
245}
246
247fn stage_checkpoint<Provider>(provider: &Provider) -> ProviderResult<EntitiesCheckpoint>
251where
252 Provider: StatsReader + StaticFileProviderFactory,
253{
254 Ok(EntitiesCheckpoint {
255 processed: provider.count_entries::<tables::BlockBodyIndices>()? as u64,
256 total: provider.static_file_provider().count_entries::<tables::Headers>()? as u64,
260 })
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::test_utils::{
267 stage_test_suite_ext, ExecuteStageTestRunner, StageTestRunner, TestStageDB,
268 UnwindStageTestRunner,
269 };
270 use alloy_consensus::TxLegacy;
271 use alloy_primitives::Signature;
272 use assert_matches::assert_matches;
273 use reth_ethereum_primitives::{BlockBody, Transaction, TransactionSigned};
274 use reth_provider::{DatabaseProviderFactory, StaticFileProviderFactory};
275 use reth_stages_api::StageUnitCheckpoint;
276 use test_utils::*;
277
278 stage_test_suite_ext!(BodyTestRunner, body);
279
280 #[tokio::test]
282 async fn partial_body_download() {
283 let (stage_progress, previous_stage) = (1, 200);
284
285 let mut runner = BodyTestRunner::default();
287 let input = ExecInput {
288 target: Some(previous_stage),
289 checkpoint: Some(StageCheckpoint::new(stage_progress)),
290 };
291 runner.seed_execution(input).expect("failed to seed execution");
292
293 let batch_size = 10;
296 runner.set_batch_size(batch_size);
297
298 let rx = runner.execute(input);
300
301 let output = rx.await.unwrap();
304 runner.db().factory.static_file_provider().commit().unwrap();
305 assert_matches!(
306 output,
307 Ok(ExecOutput { checkpoint: StageCheckpoint {
308 block_number,
309 stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
310 processed, total }))
313 }, done: false }) if block_number < 200 &&
314 processed == batch_size + 1 && total == previous_stage + 1
315 );
316 assert!(runner.validate_execution(input, output.ok()).is_ok(), "execution validation");
317 }
318
319 #[tokio::test]
321 async fn full_body_download() {
322 let (stage_progress, previous_stage) = (1, 20);
323
324 let mut runner = BodyTestRunner::default();
326 let input = ExecInput {
327 target: Some(previous_stage),
328 checkpoint: Some(StageCheckpoint::new(stage_progress)),
329 };
330 runner.seed_execution(input).expect("failed to seed execution");
331
332 runner.set_batch_size(40);
334
335 let rx = runner.execute(input);
337
338 let output = rx.await.unwrap();
341 runner.db().factory.static_file_provider().commit().unwrap();
342 assert_matches!(
343 output,
344 Ok(ExecOutput {
345 checkpoint: StageCheckpoint {
346 block_number: 20,
347 stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
348 processed,
349 total
350 }))
351 },
352 done: true
353 }) if processed + 1 == total && total == previous_stage + 1
354 );
355 assert!(runner.validate_execution(input, output.ok()).is_ok(), "execution validation");
356 }
357
358 #[tokio::test]
360 async fn sync_from_previous_progress() {
361 let (stage_progress, previous_stage) = (1, 21);
362
363 let mut runner = BodyTestRunner::default();
365 let input = ExecInput {
366 target: Some(previous_stage),
367 checkpoint: Some(StageCheckpoint::new(stage_progress)),
368 };
369 runner.seed_execution(input).expect("failed to seed execution");
370
371 let batch_size = 10;
372 runner.set_batch_size(batch_size);
373
374 let rx = runner.execute(input);
376
377 let first_run = rx.await.unwrap();
379 runner.db().factory.static_file_provider().commit().unwrap();
380 assert_matches!(
381 first_run,
382 Ok(ExecOutput { checkpoint: StageCheckpoint {
383 block_number,
384 stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
385 processed,
386 total
387 }))
388 }, done: false }) if block_number >= 10 &&
389 processed - 1 == batch_size && total == previous_stage + 1
390 );
391 let first_run_checkpoint = first_run.unwrap().checkpoint;
392
393 let input =
395 ExecInput { target: Some(previous_stage), checkpoint: Some(first_run_checkpoint) };
396 let rx = runner.execute(input);
397
398 let output = rx.await.unwrap();
400 runner.db().factory.static_file_provider().commit().unwrap();
401 assert_matches!(
402 output,
403 Ok(ExecOutput { checkpoint: StageCheckpoint {
404 block_number,
405 stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
406 processed,
407 total
408 }))
409 }, done: true }) if block_number > first_run_checkpoint.block_number &&
410 processed + 1 == total && total == previous_stage + 1
411 );
412 assert_matches!(
413 runner.validate_execution(input, output.ok()),
414 Ok(_),
415 "execution validation"
416 );
417 }
418
419 #[tokio::test]
421 async fn unwind_missing_tx() {
422 let (stage_progress, previous_stage) = (1, 20);
423
424 let mut runner = BodyTestRunner::default();
426 let input = ExecInput {
427 target: Some(previous_stage),
428 checkpoint: Some(StageCheckpoint::new(stage_progress)),
429 };
430 runner.seed_execution(input).expect("failed to seed execution");
431
432 runner.set_batch_size(40);
434
435 let rx = runner.execute(input);
437
438 let output = rx.await.unwrap();
441 runner.db().factory.static_file_provider().commit().unwrap();
442 assert_matches!(
443 output,
444 Ok(ExecOutput { checkpoint: StageCheckpoint {
445 block_number,
446 stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
447 processed,
448 total
449 }))
450 }, done: true }) if block_number == previous_stage &&
451 processed + 1 == total && total == previous_stage + 1
452 );
453 let checkpoint = output.unwrap().checkpoint;
454 runner
455 .validate_db_blocks(input.checkpoint().block_number, checkpoint.block_number)
456 .expect("Written block data invalid");
457
458 let static_file_provider = runner.db().factory.static_file_provider();
460 {
461 let mut static_file_producer =
462 static_file_provider.latest_writer(StaticFileSegment::Transactions).unwrap();
463 static_file_producer.prune_transactions(1, checkpoint.block_number).unwrap();
464 static_file_producer.commit().unwrap();
465 }
466 let unwind_to = 1;
468 let input = UnwindInput { bad_block: None, checkpoint, unwind_to };
469 let res = runner.unwind(input).await;
470 assert_matches!(
471 res,
472 Ok(UnwindOutput { checkpoint: StageCheckpoint {
473 block_number: 1,
474 stage_checkpoint: Some(StageUnitCheckpoint::Entities(EntitiesCheckpoint {
475 processed: 1,
476 total
477 }))
478 }}) if total == previous_stage + 1
479 );
480
481 assert_matches!(runner.validate_unwind(input), Ok(_), "unwind validation");
482 }
483
484 #[test]
485 fn ensure_consistency_heals_to_the_highest_db_block() {
486 for empty in [false, true] {
487 let db = TestStageDB::default();
488 let factory = &db.factory;
489 let static_files = factory.static_file_provider();
490 let body = |nonce| BlockBody {
491 transactions: vec![TransactionSigned::new_unhashed(
492 Transaction::Legacy(TxLegacy { nonce, ..Default::default() }),
493 Signature::test_signature(),
494 )],
495 ..Default::default()
496 };
497
498 let provider = factory.database_provider_rw().unwrap();
499 provider.append_block_bodies(vec![(0, None), (1, Some(&body(0)))]).unwrap();
500 provider.commit().unwrap();
501
502 let interrupted_body = if empty { BlockBody::default() } else { body(1) };
504 let provider = factory.database_provider_rw().unwrap();
505 provider.append_block_bodies(vec![(2, Some(&interrupted_body))]).unwrap();
506 static_files.commit().unwrap();
507 drop(provider);
508
509 let provider = factory.database_provider_rw().unwrap();
511 ensure_consistency(&provider, 1, None).unwrap();
512 provider.commit().unwrap();
513 assert_eq!(
514 static_files.get_highest_static_file_block(StaticFileSegment::Transactions),
515 Some(1)
516 );
517 let provider = factory.database_provider_rw().unwrap();
518 provider.append_block_bodies(vec![(2, Some(&interrupted_body))]).unwrap();
519 }
520 }
521
522 #[test]
523 fn ensure_consistency_heals_to_the_checkpoint_without_stored_bodies() {
524 const CHECKPOINT: u64 = 10;
525 for empty in [false, true] {
526 let db = TestStageDB::default();
527 let factory = &db.factory;
528 let static_files = factory.static_file_provider();
529 let body = BlockBody {
530 transactions: if empty {
531 Vec::new()
532 } else {
533 vec![TransactionSigned::new_unhashed(
534 Transaction::Legacy(TxLegacy::default()),
535 Signature::test_signature(),
536 )]
537 },
538 ..Default::default()
539 };
540
541 static_files
543 .latest_writer(StaticFileSegment::Transactions)
544 .unwrap()
545 .ensure_at_block(CHECKPOINT)
546 .unwrap();
547 static_files.commit().unwrap();
548
549 let provider = factory.database_provider_rw().unwrap();
551 provider.append_block_bodies(vec![(CHECKPOINT + 1, Some(&body))]).unwrap();
552 static_files.commit().unwrap();
553 drop(provider);
554
555 let provider = factory.database_provider_rw().unwrap();
556 ensure_consistency(&provider, CHECKPOINT, None).unwrap();
557 assert_eq!(
558 static_files.get_highest_static_file_block(StaticFileSegment::Transactions),
559 Some(CHECKPOINT)
560 );
561 provider.append_block_bodies(vec![(CHECKPOINT + 1, Some(&body))]).unwrap();
562 }
563 }
564
565 mod test_utils {
566 use crate::{
567 stages::bodies::BodyStage,
568 test_utils::{
569 ExecuteStageTestRunner, StageTestRunner, TestRunnerError, TestStageDB,
570 UnwindStageTestRunner,
571 },
572 };
573 use alloy_consensus::{BlockHeader, Header};
574 use alloy_primitives::{map::B256Map, BlockNumber, TxNumber, B256};
575 use futures_util::Stream;
576 use reth_db::{static_file::HeaderWithHashMask, tables};
577 use reth_db_api::{
578 cursor::DbCursorRO,
579 models::{StoredBlockBodyIndices, StoredBlockOmmers},
580 transaction::{DbTx, DbTxMut},
581 };
582 use reth_ethereum_primitives::{Block, BlockBody};
583 use reth_network_p2p::{
584 bodies::{
585 downloader::{BodyDownloader, BodyDownloaderResult},
586 response::BlockResponse,
587 },
588 error::DownloadResult,
589 };
590 use reth_primitives_traits::{SealedBlock, SealedHeader};
591 use reth_provider::{
592 providers::StaticFileWriter, test_utils::MockNodeTypesWithDB, HeaderProvider,
593 ProviderFactory, StaticFileProviderFactory, TransactionsProvider,
594 };
595 use reth_stages_api::{ExecInput, ExecOutput, UnwindInput};
596 use reth_static_file_types::StaticFileSegment;
597 use reth_testing_utils::generators::{
598 self, random_block_range, random_signed_tx, BlockRangeParams,
599 };
600 use std::{
601 collections::VecDeque,
602 ops::RangeInclusive,
603 pin::Pin,
604 task::{Context, Poll},
605 };
606
607 pub(crate) const GENESIS_HASH: B256 = B256::ZERO;
609
610 pub(crate) fn body_by_hash(block: &SealedBlock<Block>) -> (B256, BlockBody) {
612 (block.hash(), block.body().clone())
613 }
614
615 pub(crate) struct BodyTestRunner {
617 responses: B256Map<BlockBody>,
618 db: TestStageDB,
619 batch_size: u64,
620 }
621
622 impl Default for BodyTestRunner {
623 fn default() -> Self {
624 Self { responses: B256Map::default(), db: TestStageDB::default(), batch_size: 1000 }
625 }
626 }
627
628 impl BodyTestRunner {
629 pub(crate) fn set_batch_size(&mut self, batch_size: u64) {
630 self.batch_size = batch_size;
631 }
632
633 pub(crate) fn set_responses(&mut self, responses: B256Map<BlockBody>) {
634 self.responses = responses;
635 }
636 }
637
638 impl StageTestRunner for BodyTestRunner {
639 type S = BodyStage<TestBodyDownloader>;
640
641 fn db(&self) -> &TestStageDB {
642 &self.db
643 }
644
645 fn stage(&self) -> Self::S {
646 BodyStage::new(TestBodyDownloader::new(
647 self.db.factory.clone(),
648 self.responses.clone(),
649 self.batch_size,
650 ))
651 }
652 }
653
654 impl ExecuteStageTestRunner for BodyTestRunner {
655 type Seed = Vec<SealedBlock<Block>>;
656
657 fn seed_execution(&mut self, input: ExecInput) -> Result<Self::Seed, TestRunnerError> {
658 let start = input.checkpoint().block_number;
659 let end = input.target();
660
661 let static_file_provider = self.db.factory.static_file_provider();
662
663 let mut rng = generators::rng();
664
665 let blocks = random_block_range(
667 &mut rng,
668 0..=end,
669 BlockRangeParams {
670 parent: Some(GENESIS_HASH),
671 tx_count: 0..2,
672 ..Default::default()
673 },
674 );
675 self.db.insert_headers(blocks.iter().map(|block| block.sealed_header()))?;
676 if let Some(progress) = blocks.get(start as usize) {
677 {
679 let tx = self.db.factory.provider_rw()?.into_tx();
680 let mut static_file_producer = static_file_provider
681 .get_writer(start, StaticFileSegment::Transactions)?;
682
683 let body = StoredBlockBodyIndices {
684 first_tx_num: 0,
685 tx_count: progress.transaction_count() as u64,
686 };
687
688 static_file_producer.set_block_range(0..=progress.number);
689
690 body.tx_num_range().try_for_each(|tx_num| {
691 let transaction = random_signed_tx(&mut rng);
692 static_file_producer.append_transaction(tx_num, &transaction).map(drop)
693 })?;
694
695 if body.tx_count != 0 {
696 tx.put::<tables::TransactionBlocks>(
697 body.last_tx_num(),
698 progress.number,
699 )?;
700 }
701
702 tx.put::<tables::BlockBodyIndices>(progress.number, body)?;
703
704 if !progress.ommers_hash_is_empty() {
705 tx.put::<tables::BlockOmmers>(
706 progress.number,
707 StoredBlockOmmers { ommers: progress.body().ommers.clone() },
708 )?;
709 }
710
711 static_file_producer.commit()?;
712 tx.commit()?;
713 }
714 }
715 self.set_responses(blocks.iter().map(body_by_hash).collect());
716 Ok(blocks)
717 }
718
719 fn validate_execution(
720 &self,
721 input: ExecInput,
722 output: Option<ExecOutput>,
723 ) -> Result<(), TestRunnerError> {
724 let highest_block = match output.as_ref() {
725 Some(output) => output.checkpoint,
726 None => input.checkpoint(),
727 }
728 .block_number;
729 self.validate_db_blocks(highest_block, highest_block)
730 }
731 }
732
733 impl UnwindStageTestRunner for BodyTestRunner {
734 fn validate_unwind(&self, input: UnwindInput) -> Result<(), TestRunnerError> {
735 self.db.ensure_no_entry_above::<tables::BlockBodyIndices, _>(
736 input.unwind_to,
737 |key| key,
738 )?;
739 self.db
740 .ensure_no_entry_above::<tables::BlockOmmers, _>(input.unwind_to, |key| key)?;
741 if let Some(last_tx_id) = self.get_last_tx_id()? {
742 self.db
743 .ensure_no_entry_above::<tables::Transactions, _>(last_tx_id, |key| key)?;
744 self.db.ensure_no_entry_above::<tables::TransactionBlocks, _>(
745 last_tx_id,
746 |key| key,
747 )?;
748 }
749 Ok(())
750 }
751 }
752
753 impl BodyTestRunner {
754 pub(crate) fn get_last_tx_id(&self) -> Result<Option<TxNumber>, TestRunnerError> {
756 let last_body = self.db.query(|tx| {
757 let v = tx.cursor_read::<tables::BlockBodyIndices>()?.last()?;
758 Ok(v)
759 })?;
760 Ok(match last_body {
761 Some((_, body)) if body.tx_count != 0 => {
762 Some(body.first_tx_num + body.tx_count - 1)
763 }
764 _ => None,
765 })
766 }
767
768 pub(crate) fn validate_db_blocks(
770 &self,
771 prev_progress: BlockNumber,
772 highest_block: BlockNumber,
773 ) -> Result<(), TestRunnerError> {
774 let static_file_provider = self.db.factory.static_file_provider();
775
776 self.db.query(|tx| {
777 let mut bodies_cursor = tx.cursor_read::<tables::BlockBodyIndices>()?;
779 let mut ommers_cursor = tx.cursor_read::<tables::BlockOmmers>()?;
780 let mut tx_block_cursor = tx.cursor_read::<tables::TransactionBlocks>()?;
781
782 let first_body_key = match bodies_cursor.first()? {
783 Some((key, _)) => key,
784 None => return Ok(()),
785 };
786
787 let mut prev_number: Option<BlockNumber> = None;
788
789
790 for entry in bodies_cursor.walk(Some(first_body_key))? {
791 let (number, body) = entry?;
792
793 if number > prev_progress
796 && let Some(prev_key) = prev_number {
797 assert_eq!(prev_key + 1, number, "Body entries must be sequential");
798 }
799
800 assert!(
802 number <= highest_block,
803 "We wrote a block body outside of our synced range. Found block with number {number}, highest block according to stage is {highest_block}",
804 );
805
806 let header = static_file_provider.header_by_number(number)?.expect("to be present");
807 let stored_ommers = ommers_cursor.seek_exact(number)?;
809 if header.ommers_hash_is_empty() {
810 assert!(stored_ommers.is_none(), "Unexpected ommers entry");
811 } else {
812 assert!(stored_ommers.is_some(), "Missing ommers entry");
813 }
814
815 let tx_block_id = tx_block_cursor.seek_exact(body.last_tx_num())?.map(|(_,b)| b);
816 if body.tx_count == 0 {
817 assert_ne!(tx_block_id,Some(number));
818 } else {
819 assert_eq!(tx_block_id, Some(number));
820 }
821
822 for tx_id in body.tx_num_range() {
823 assert!(static_file_provider.transaction_by_id(tx_id)?.is_some(), "Transaction is missing.");
824 }
825
826 prev_number = Some(number);
827 }
828 Ok(())
829 })?;
830 Ok(())
831 }
832 }
833
834 #[derive(Debug)]
836 pub(crate) struct TestBodyDownloader {
837 provider_factory: ProviderFactory<MockNodeTypesWithDB>,
838 responses: B256Map<BlockBody>,
839 headers: VecDeque<SealedHeader>,
840 batch_size: u64,
841 }
842
843 impl TestBodyDownloader {
844 pub(crate) fn new(
845 provider_factory: ProviderFactory<MockNodeTypesWithDB>,
846 responses: B256Map<BlockBody>,
847 batch_size: u64,
848 ) -> Self {
849 Self { provider_factory, responses, headers: VecDeque::default(), batch_size }
850 }
851 }
852
853 impl BodyDownloader for TestBodyDownloader {
854 type Block = Block;
855
856 fn set_download_range(
857 &mut self,
858 range: RangeInclusive<BlockNumber>,
859 ) -> DownloadResult<()> {
860 let static_file_provider = self.provider_factory.static_file_provider();
861
862 for header in static_file_provider.fetch_range_iter(
863 StaticFileSegment::Headers,
864 *range.start()..*range.end() + 1,
865 |cursor, number| cursor.get_two::<HeaderWithHashMask<Header>>(number.into()),
866 )? {
867 if let Some((header, hash)) = header? {
868 self.headers.push_back(SealedHeader::new(header, hash));
869 }
870 }
871
872 Ok(())
873 }
874 }
875
876 impl Stream for TestBodyDownloader {
877 type Item = BodyDownloaderResult<Block>;
878 fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
879 let this = self.get_mut();
880
881 if this.headers.is_empty() {
882 return Poll::Ready(None)
883 }
884
885 let mut response =
886 Vec::with_capacity(std::cmp::min(this.headers.len(), this.batch_size as usize));
887 while let Some(header) = this.headers.pop_front() {
888 if header.is_empty() {
889 response.push(BlockResponse::Empty(header))
890 } else {
891 let body =
892 this.responses.remove(&header.hash()).expect("requested unknown body");
893 response.push(BlockResponse::Full(SealedBlock::from_sealed_parts(
894 header, body,
895 )));
896 }
897
898 if response.len() as u64 >= this.batch_size {
899 break
900 }
901 }
902
903 if !response.is_empty() {
904 return Poll::Ready(Some(Ok(response)))
905 }
906
907 panic!("requested bodies without setting headers")
908 }
909 }
910 }
911}