Skip to main content

reth_stages/stages/
bodies.rs

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/// The body stage downloads block bodies.
26///
27/// The body stage downloads block bodies for all block headers stored locally in storage.
28///
29/// # Empty blocks
30///
31/// Blocks with an ommers hash corresponding to no ommers *and* a transaction root corresponding to
32/// no transactions will not have a block body downloaded for them, since it would be meaningless to
33/// do so.
34///
35/// This also means that if there is no body for the block in storage (assuming the
36/// block number <= the synced block of this stage), then the block can be considered empty.
37///
38/// # Tables
39///
40/// The bodies are processed and data is inserted into these tables:
41///
42/// - [`BlockOmmers`][reth_db_api::tables::BlockOmmers]
43/// - [`BlockBodies`][reth_db_api::tables::BlockBodyIndices]
44/// - [`Transactions`][reth_db_api::tables::Transactions]
45/// - [`TransactionBlocks`][reth_db_api::tables::TransactionBlocks]
46///
47/// # Genesis
48///
49/// This stage expects that the genesis has been inserted into the appropriate tables:
50///
51/// - The header tables (see [`HeaderStage`][crate::stages::HeaderStage])
52/// - The [`BlockOmmers`][reth_db_api::tables::BlockOmmers] table
53/// - The [`BlockBodies`][reth_db_api::tables::BlockBodyIndices] table
54/// - The [`Transactions`][reth_db_api::tables::Transactions] table
55#[derive(Debug)]
56pub struct BodyStage<D: BodyDownloader> {
57    /// The body downloader.
58    downloader: D,
59    /// Block response buffer.
60    buffer: Option<Vec<BlockResponse<D::Block>>>,
61}
62
63impl<D: BodyDownloader> BodyStage<D> {
64    /// Create new bodies stage from downloader.
65    pub const fn new(downloader: D) -> Self {
66        Self { downloader, buffer: None }
67    }
68}
69
70/// Ensures that static files and database are in sync.
71pub(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    // Get id for the next tx_num of zero if there are no transactions.
80    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    // Make sure Transactions static file is at the same height. If it's further, this
90    // input execution was interrupted previously and we need to unwind the static file.
91    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        // If static files are ahead, we are currently unwinding the stage or we didn't reach
98        // the database commit in a previous stage run. So, our only solution is to unwind the
99        // static files and proceed from the database expected height.
100        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                // Nothing is stored when the bodies up to the checkpoint were never downloaded.
107                .unwrap_or(checkpoint);
108            // Empty blocks advance the file's height without changing its transaction count.
109            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                // Since this is a database <-> static file inconsistency, we commit the change
119                // straight away.
120                static_file_producer.commit()?;
121            }
122        }
123        // If static files are behind, then there was some corruption or loss of files. This
124        // error will trigger an unwind, that will bring the database to the same height as the
125        // static files.
126        Ordering::Less => {
127            // If we are already in the process of unwind, this might be fine because we will
128            // fix the inconsistency right away.
129            if let Some(unwind_to) = unwind_block {
130                let next_tx_num_after_unwind = provider.next_tx_num_after_block(unwind_to)?;
131
132                // This means we need a deeper unwind.
133                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    /// Return the id of the stage
165    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        // Update the header range on the downloader
179        self.downloader.set_download_range(input.next_block_range())?;
180
181        // Poll next downloader item.
182        let maybe_next_result = ready!(self.downloader.try_poll_next_unpin(cx));
183
184        // Task downloader can return `None` only if the response relaying channel was closed. This
185        // is a fatal error to prevent the pipeline from running forever.
186        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    /// Download block bodies from the last checkpoint for this stage up until the latest synced
198    /// header, limited by the stage's batch size.
199    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        // Write bodies to database.
214        provider.append_block_bodies(
215            buffer.iter().map(|response| (response.block_number(), response.body())).collect(),
216        )?;
217
218        // The stage is "done" if:
219        // - We got fewer blocks than our target
220        // - We reached our target and the target was not limited by the batch size of the stage
221        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    /// Unwind the stage.
230    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
247// TODO(alexey): ideally, we want to measure Bodies stage progress in bytes, but it's hard to know
248//  beforehand how many bytes we need to download. So the good solution would be to measure the
249//  progress in gas as a proxy to size. Execution stage uses a similar approach.
250fn 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        // Count only static files entries. If we count the database entries too, we may have
257        // duplicates. We're sure that the static files have all entries that database has,
258        // because we run the `StaticFileProducer` before starting the pipeline.
259        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    /// Checks that the stage downloads at most `batch_size` blocks.
281    #[tokio::test]
282    async fn partial_body_download() {
283        let (stage_progress, previous_stage) = (1, 200);
284
285        // Set up test runner
286        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        // Set the batch size (max we sync per stage execution) to less than the number of blocks
294        // the previous stage synced (10 vs 20)
295        let batch_size = 10;
296        runner.set_batch_size(batch_size);
297
298        // Run the stage
299        let rx = runner.execute(input);
300
301        // Check that we only synced around `batch_size` blocks even though the number of blocks
302        // synced by the previous stage is higher
303        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, // 1 seeded block body + batch size
311                    total // seeded headers
312                }))
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    /// Same as [`partial_body_download`] except the `batch_size` is not hit.
320    #[tokio::test]
321    async fn full_body_download() {
322        let (stage_progress, previous_stage) = (1, 20);
323
324        // Set up test runner
325        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        // Set the batch size to more than what the previous stage synced (40 vs 20)
333        runner.set_batch_size(40);
334
335        // Run the stage
336        let rx = runner.execute(input);
337
338        // Check that we synced all blocks successfully, even though our `batch_size` allows us to
339        // sync more (if there were more headers)
340        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    /// Same as [`full_body_download`] except we have made progress before
359    #[tokio::test]
360    async fn sync_from_previous_progress() {
361        let (stage_progress, previous_stage) = (1, 21);
362
363        // Set up test runner
364        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        // Run the stage
375        let rx = runner.execute(input);
376
377        // Check that we synced at least 10 blocks
378        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        // Execute again on top of the previous run
394        let input =
395            ExecInput { target: Some(previous_stage), checkpoint: Some(first_run_checkpoint) };
396        let rx = runner.execute(input);
397
398        // Check that we synced more blocks
399        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    /// Checks that the stage unwinds correctly, even if a transaction in a block is missing.
420    #[tokio::test]
421    async fn unwind_missing_tx() {
422        let (stage_progress, previous_stage) = (1, 20);
423
424        // Set up test runner
425        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        // Set the batch size to more than what the previous stage synced (40 vs 20)
433        runner.set_batch_size(40);
434
435        // Run the stage
436        let rx = runner.execute(input);
437
438        // Check that we synced all blocks successfully, even though our `batch_size` allows us to
439        // sync more (if there were more headers)
440        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        // Delete a transaction
459        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        // Unwind all of it
467        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            // The static files commit block 2, the database does not.
503            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            // Bodies stage healing alone, without startup recovery.
510            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            // The transaction files start after the checkpoint, no body is stored up to it.
542            static_files
543                .latest_writer(StaticFileSegment::Transactions)
544                .unwrap()
545                .ensure_at_block(CHECKPOINT)
546                .unwrap();
547            static_files.commit().unwrap();
548
549            // The static files commit the next block, the database does not.
550            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        /// The block hash of the genesis block.
608        pub(crate) const GENESIS_HASH: B256 = B256::ZERO;
609
610        /// A helper to create a collection of block bodies keyed by their hash.
611        pub(crate) fn body_by_hash(block: &SealedBlock<Block>) -> (B256, BlockBody) {
612            (block.hash(), block.body().clone())
613        }
614
615        /// A helper struct for running the [`BodyStage`].
616        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                // Static files do not support gaps in headers, so we need to generate 0 to end
666                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                    // Insert last progress data
678                    {
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            /// Get the last available tx id if any
755            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            /// Validate that the inserted block data is valid
769            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                    // Acquire cursors on body related tables
778                    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                        // Validate sequentiality only after prev progress,
794                        // since the data before is mocked and can contain gaps
795                        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                        // Validate that the current entry is below or equals to the highest allowed block
801                        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                        // Validate that ommers exist if any
808                        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        /// A [`BodyDownloader`] that is backed by an internal [`B256Map`] for testing.
835        #[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}