Skip to main content

reth_stages/stages/
headers.rs

1use alloy_consensus::BlockHeader;
2use alloy_primitives::{BlockHash, BlockNumber, Bytes, B256};
3use alloy_rlp::Decodable;
4use futures_util::StreamExt;
5use reth_config::config::EtlConfig;
6use reth_db_api::{
7    cursor::{DbCursorRO, DbCursorRW},
8    table::Value,
9    tables,
10    transaction::{DbTx, DbTxMut},
11    DbTxUnwindExt, RawKey, RawTable, RawValue,
12};
13use reth_etl::Collector;
14use reth_network_p2p::headers::{
15    downloader::{HeaderDownloader, HeaderSyncGap, SyncTarget},
16    error::HeadersDownloaderError,
17};
18use reth_primitives_traits::{FullBlockHeader, HeaderTy, NodePrimitives, SealedHeader};
19use reth_provider::{
20    providers::StaticFileWriter, BlockHashReader, DBProvider, HeaderSyncGapProvider,
21    StaticFileProviderFactory,
22};
23use reth_stages_api::{
24    CheckpointBlockRange, EntitiesCheckpoint, ExecInput, ExecOutput, HeadersCheckpoint, Stage,
25    StageCheckpoint, StageError, StageId, UnwindInput, UnwindOutput,
26};
27use reth_static_file_types::StaticFileSegment;
28use std::task::{ready, Context, Poll};
29
30use tokio::sync::watch;
31use tracing::*;
32
33/// The headers stage.
34///
35/// The headers stage downloads all block headers from the highest block in storage to
36/// the perceived highest block on the network.
37///
38/// The headers are processed and data is inserted into static files, as well as into the
39/// [`HeaderNumbers`][reth_db_api::tables::HeaderNumbers] table.
40///
41/// NOTE: This stage downloads headers in reverse and pushes them to the ETL [`Collector`]. It then
42/// proceeds to push them sequentially to static files. The stage checkpoint is not updated until
43/// this stage is done.
44#[derive(Debug)]
45pub struct HeaderStage<Provider, Downloader: HeaderDownloader> {
46    /// Database handle.
47    provider: Provider,
48    /// Strategy for downloading the headers
49    downloader: Downloader,
50    /// The tip for the stage.
51    ///
52    /// This determines the sync target of the stage (set by the pipeline).
53    tip: watch::Receiver<B256>,
54    /// Current sync gap.
55    sync_gap: Option<HeaderSyncGap<Downloader::Header>>,
56    /// ETL collector with `HeaderHash` -> `BlockNumber`
57    hash_collector: Collector<BlockHash, BlockNumber>,
58    /// ETL collector with `BlockNumber` -> `RLP-encoded SealedHeader`
59    header_collector: Collector<BlockNumber, Bytes>,
60    /// Returns true if the ETL collector has all necessary headers to fill the gap.
61    is_etl_ready: bool,
62}
63
64// === impl HeaderStage ===
65
66impl<Provider, Downloader> HeaderStage<Provider, Downloader>
67where
68    Downloader: HeaderDownloader,
69{
70    /// Create a new header stage
71    pub fn new(
72        database: Provider,
73        downloader: Downloader,
74        tip: watch::Receiver<B256>,
75        etl_config: EtlConfig,
76    ) -> Self {
77        Self {
78            provider: database,
79            downloader,
80            tip,
81            sync_gap: None,
82            hash_collector: Collector::new(etl_config.file_size / 2, etl_config.dir.clone()),
83            header_collector: Collector::new(etl_config.file_size / 2, etl_config.dir),
84            is_etl_ready: false,
85        }
86    }
87
88    /// Clear all ETL state. Called on error paths to prevent buffer pollution on retry.
89    fn clear_etl_state(&mut self) {
90        self.sync_gap = None;
91        self.hash_collector.clear();
92        self.header_collector.clear();
93        self.is_etl_ready = false;
94    }
95
96    /// Write downloaded headers to storage from ETL.
97    ///
98    /// Writes to static files ( `Header | HeaderTD | HeaderHash` ) and [`tables::HeaderNumbers`]
99    /// database table.
100    fn write_headers<P>(&mut self, provider: &P) -> Result<BlockNumber, StageError>
101    where
102        P: DBProvider<Tx: DbTxMut> + StaticFileProviderFactory,
103        Downloader: HeaderDownloader<Header = <P::Primitives as NodePrimitives>::BlockHeader>,
104        <P::Primitives as NodePrimitives>::BlockHeader: Value + FullBlockHeader,
105    {
106        let total_headers = self.header_collector.len();
107
108        info!(target: "sync::stages::headers", total = total_headers, "Writing headers");
109
110        let static_file_provider = provider.static_file_provider();
111
112        // Consistency check of expected headers in static files vs DB is done on provider::sync_gap
113        // when poll_execute_ready is polled.
114        let mut last_header_number = static_file_provider
115            .get_highest_static_file_block(StaticFileSegment::Headers)
116            .unwrap_or_default();
117
118        // Although headers were downloaded in reverse order, the collector iterates it in ascending
119        // order
120        let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers)?;
121        let interval = (total_headers / 10).max(1);
122        for (index, header) in self.header_collector.iter()?.enumerate() {
123            let (_, header_buf) = header?;
124
125            if index > 0 && index.is_multiple_of(interval) && total_headers > 100 {
126                info!(target: "sync::stages::headers", progress = %format_args!("{:.2}%", (index as f64 / total_headers as f64) * 100.0), "Writing headers");
127            }
128
129            let sealed_header: SealedHeader<Downloader::Header> = SealedHeader::new_unhashed(
130                Decodable::decode(&mut header_buf.as_slice())
131                    .map_err(|err| StageError::Fatal(Box::new(err)))?,
132            );
133
134            let (header, header_hash) = sealed_header.split_ref();
135            if header.number() == 0 {
136                continue
137            }
138            last_header_number = header.number();
139
140            // Append to Headers segment
141            writer.append_header(header, header_hash)?;
142        }
143
144        info!(target: "sync::stages::headers", total = total_headers, "Writing headers hash index");
145
146        let mut cursor_header_numbers =
147            provider.tx_ref().cursor_write::<RawTable<tables::HeaderNumbers>>()?;
148        // If we only have the genesis block hash, then we are at first sync, and we can remove it,
149        // add it to the collector and use tx.append on all hashes.
150        let first_sync = if provider.tx_ref().entries::<RawTable<tables::HeaderNumbers>>()? == 1 &&
151            let Some((hash, block_number)) = cursor_header_numbers.last()? &&
152            block_number.value()? == 0
153        {
154            self.hash_collector.insert(hash.key()?, 0)?;
155            cursor_header_numbers.delete_current()?;
156            true
157        } else {
158            false
159        };
160
161        // Since ETL sorts all entries by hashes, we are either appending (first sync) or inserting
162        // in order (further syncs).
163        for (index, hash_to_number) in self.hash_collector.iter()?.enumerate() {
164            let (hash, number) = hash_to_number?;
165
166            if index > 0 && index.is_multiple_of(interval) && total_headers > 100 {
167                info!(target: "sync::stages::headers", progress = %format_args!("{:.2}%", (index as f64 / total_headers as f64) * 100.0), "Writing headers hash index");
168            }
169
170            if first_sync {
171                cursor_header_numbers.append(
172                    RawKey::<BlockHash>::from_vec(hash),
173                    &RawValue::<BlockNumber>::from_vec(number),
174                )?;
175            } else {
176                cursor_header_numbers.upsert(
177                    RawKey::<BlockHash>::from_vec(hash),
178                    &RawValue::<BlockNumber>::from_vec(number),
179                )?;
180            }
181        }
182
183        Ok(last_header_number)
184    }
185}
186
187impl<Provider, P, D> Stage<Provider> for HeaderStage<P, D>
188where
189    Provider: DBProvider<Tx: DbTxMut> + StaticFileProviderFactory,
190    P: HeaderSyncGapProvider<Header = <Provider::Primitives as NodePrimitives>::BlockHeader>,
191    D: HeaderDownloader<Header = <Provider::Primitives as NodePrimitives>::BlockHeader>,
192    <Provider::Primitives as NodePrimitives>::BlockHeader: FullBlockHeader + Value,
193{
194    /// Return the id of the stage
195    fn id(&self) -> StageId {
196        StageId::Headers
197    }
198
199    fn poll_execute_ready(
200        &mut self,
201        cx: &mut Context<'_>,
202        input: ExecInput,
203    ) -> Poll<Result<(), StageError>> {
204        let current_checkpoint = input.checkpoint();
205
206        // Return if stage has already completed the gap on the ETL files
207        if self.is_etl_ready {
208            return Poll::Ready(Ok(()))
209        }
210
211        // Lookup the head and tip of the sync range
212        let local_head = self.provider.local_tip_header(current_checkpoint.block_number)?;
213        let target = SyncTarget::Tip(*self.tip.borrow());
214        let gap = HeaderSyncGap { local_head, target };
215        let tip = gap.target.tip();
216
217        // Nothing to sync
218        if gap.is_closed() {
219            info!(
220                target: "sync::stages::headers",
221                checkpoint = %current_checkpoint.block_number,
222                target = ?tip,
223                "Target block already reached"
224            );
225            self.is_etl_ready = true;
226            self.sync_gap = Some(gap);
227            return Poll::Ready(Ok(()))
228        }
229
230        debug!(target: "sync::stages::headers", ?tip, head = ?gap.local_head.hash(), "Commencing sync");
231        let local_head_number = gap.local_head.number();
232
233        // let the downloader know what to sync
234        if self.sync_gap != Some(gap.clone()) {
235            self.sync_gap = Some(gap.clone());
236            self.downloader.update_sync_gap(gap.local_head, gap.target);
237        }
238
239        // We only want to stop once we have all the headers on ETL filespace (disk).
240        loop {
241            match ready!(self.downloader.poll_next_unpin(cx)) {
242                Some(Ok(headers)) => {
243                    info!(target: "sync::stages::headers", total = headers.len(), from_block = headers.first().map(|h| h.number()), to_block = headers.last().map(|h| h.number()), "Received headers");
244                    for header in headers {
245                        let header_number = header.number();
246
247                        self.hash_collector.insert(header.hash(), header_number)?;
248                        self.header_collector
249                            .insert(header_number, Bytes::from(alloy_rlp::encode(&*header)))?;
250
251                        // Headers are downloaded in reverse, so if we reach here, we know we have
252                        // filled the gap.
253                        if header_number == local_head_number + 1 {
254                            self.is_etl_ready = true;
255                            return Poll::Ready(Ok(()))
256                        }
257                    }
258                }
259                Some(Err(HeadersDownloaderError::DetachedHead { local_head, header, error })) => {
260                    error!(target: "sync::stages::headers", %error, "Cannot attach header to head");
261                    self.clear_etl_state();
262                    return Poll::Ready(Err(StageError::DetachedHead {
263                        local_head: Box::new(local_head.block_with_parent()),
264                        header: Box::new(header.block_with_parent()),
265                        error,
266                    }))
267                }
268                None => {
269                    self.clear_etl_state();
270                    return Poll::Ready(Err(StageError::ChannelClosed))
271                }
272            }
273        }
274    }
275
276    /// Download the headers in reverse order (falling block numbers)
277    /// starting from the tip of the chain
278    fn execute(&mut self, provider: &Provider, input: ExecInput) -> Result<ExecOutput, StageError> {
279        let current_checkpoint = input.checkpoint();
280
281        if self.sync_gap.take().ok_or(StageError::MissingSyncGap)?.is_closed() {
282            self.is_etl_ready = false;
283            return Ok(ExecOutput::done(current_checkpoint))
284        }
285
286        // We should be here only after we have downloaded all headers into the disk buffer (ETL).
287        if !self.is_etl_ready {
288            return Err(StageError::MissingDownloadBuffer)
289        }
290
291        // Reset flag
292        self.is_etl_ready = false;
293
294        // Write the headers and related tables to DB from ETL space
295        let to_be_processed = self.hash_collector.len() as u64;
296        let last_header_number = self.write_headers(provider)?;
297
298        // Clear ETL collectors
299        self.hash_collector.clear();
300        self.header_collector.clear();
301
302        // We only reach here if all headers have been downloaded by ETL, and pushed to DB all
303        // in one stage run.
304        Ok(ExecOutput::done(
305            StageCheckpoint::new(last_header_number).with_headers_stage_checkpoint(
306                HeadersCheckpoint {
307                    block_range: CheckpointBlockRange {
308                        from: input.checkpoint().block_number,
309                        to: last_header_number,
310                    },
311                    progress: EntitiesCheckpoint {
312                        processed: input.checkpoint().block_number + to_be_processed,
313                        total: last_header_number,
314                    },
315                },
316            ),
317        ))
318    }
319
320    /// Unwind the stage.
321    fn unwind(
322        &mut self,
323        provider: &Provider,
324        input: UnwindInput,
325    ) -> Result<UnwindOutput, StageError> {
326        self.clear_etl_state();
327
328        // First unwind the db tables, until the unwind_to block number. use the walker to unwind
329        // HeaderNumbers based on the index in CanonicalHeaders
330        // unwind from the next block number since the unwind_to block is exclusive
331        provider
332            .tx_ref()
333            .unwind_table_by_walker::<tables::CanonicalHeaders, tables::HeaderNumbers>(
334                (input.unwind_to + 1)..,
335            )?;
336        provider.tx_ref().unwind_table_by_num::<tables::CanonicalHeaders>(input.unwind_to)?;
337        let unfinalized_headers_unwound = provider.tx_ref().unwind_table_by_num::<tables::Headers<
338            HeaderTy<Provider::Primitives>,
339        >>(input.unwind_to)?;
340
341        // determine how many headers to unwind from the static files based on the highest block and
342        // the unwind_to block
343        let static_file_provider = provider.static_file_provider();
344        let highest_block = static_file_provider
345            .get_highest_static_file_block(StaticFileSegment::Headers)
346            .unwrap_or_default();
347        let static_file_headers_to_unwind = highest_block - input.unwind_to;
348        for block_number in (input.unwind_to + 1)..=highest_block {
349            let hash = static_file_provider.block_hash(block_number)?;
350            // we have to delete from HeaderNumbers here as well as in the above unwind, since that
351            // mapping contains entries for both headers in the db and headers in static files
352            //
353            // so if we are unwinding past the lowest block in the db, we have to iterate through
354            // the HeaderNumbers entries that we'll delete in static files below
355            if let Some(header_hash) = hash {
356                provider.tx_ref().delete::<tables::HeaderNumbers>(header_hash, None)?;
357            }
358        }
359
360        // Now unwind the static files until the unwind_to block number
361        let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers)?;
362        writer.prune_headers(static_file_headers_to_unwind)?;
363
364        // Set the stage checkpoint entities processed based on how much we unwound - we add the
365        // headers unwound from static files and db
366        let stage_checkpoint =
367            input.checkpoint.headers_stage_checkpoint().map(|stage_checkpoint| HeadersCheckpoint {
368                block_range: stage_checkpoint.block_range,
369                progress: EntitiesCheckpoint {
370                    processed: stage_checkpoint.progress.processed.saturating_sub(
371                        static_file_headers_to_unwind + unfinalized_headers_unwound as u64,
372                    ),
373                    total: stage_checkpoint.progress.total,
374                },
375            });
376
377        let mut checkpoint = StageCheckpoint::new(input.unwind_to);
378        if let Some(stage_checkpoint) = stage_checkpoint {
379            checkpoint = checkpoint.with_headers_stage_checkpoint(stage_checkpoint);
380        }
381
382        Ok(UnwindOutput { checkpoint })
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use crate::test_utils::{
390        stage_test_suite, ExecuteStageTestRunner, StageTestRunner, UnwindStageTestRunner,
391    };
392    use alloy_primitives::B256;
393    use assert_matches::assert_matches;
394    use reth_provider::{test_utils::insert_headers, ProviderFactory, StaticFileProviderFactory};
395    use reth_stages_api::StageUnitCheckpoint;
396    use reth_testing_utils::generators::{self, random_header, random_header_range};
397    use std::sync::Arc;
398    use test_runner::HeadersTestRunner;
399
400    mod test_runner {
401        use super::*;
402        use crate::test_utils::{TestRunnerError, TestStageDB};
403        use reth_consensus::test_utils::TestConsensus;
404        use reth_downloaders::headers::reverse_headers::{
405            ReverseHeadersDownloader, ReverseHeadersDownloaderBuilder,
406        };
407        use reth_network_p2p::test_utils::{TestHeaderDownloader, TestHeadersClient};
408        use reth_provider::{test_utils::MockNodeTypesWithDB, BlockNumReader, HeaderProvider};
409        use tokio::sync::watch;
410
411        pub(crate) struct HeadersTestRunner<D: HeaderDownloader> {
412            pub(crate) client: TestHeadersClient,
413            channel: (watch::Sender<B256>, watch::Receiver<B256>),
414            downloader_factory: Box<dyn Fn() -> D + Send + Sync + 'static>,
415            db: TestStageDB,
416        }
417
418        impl Default for HeadersTestRunner<TestHeaderDownloader> {
419            fn default() -> Self {
420                let client = TestHeadersClient::default();
421                Self {
422                    client: client.clone(),
423                    channel: watch::channel(B256::ZERO),
424
425                    downloader_factory: Box::new(move || {
426                        TestHeaderDownloader::new(client.clone(), 1000, 1000)
427                    }),
428                    db: TestStageDB::default(),
429                }
430            }
431        }
432
433        impl<D: HeaderDownloader<Header = alloy_consensus::Header> + 'static> StageTestRunner
434            for HeadersTestRunner<D>
435        {
436            type S = HeaderStage<ProviderFactory<MockNodeTypesWithDB>, D>;
437
438            fn db(&self) -> &TestStageDB {
439                &self.db
440            }
441
442            fn stage(&self) -> Self::S {
443                HeaderStage::new(
444                    self.db.factory.clone(),
445                    (*self.downloader_factory)(),
446                    self.channel.1.clone(),
447                    EtlConfig::default(),
448                )
449            }
450        }
451
452        impl<D: HeaderDownloader<Header = alloy_consensus::Header> + 'static> ExecuteStageTestRunner
453            for HeadersTestRunner<D>
454        {
455            type Seed = Vec<SealedHeader>;
456
457            fn seed_execution(&mut self, input: ExecInput) -> Result<Self::Seed, TestRunnerError> {
458                let mut rng = generators::rng();
459                let start = input.checkpoint().block_number;
460                let headers = random_header_range(&mut rng, 0..start + 1, B256::ZERO);
461                let head = headers.last().cloned().unwrap();
462                self.db.insert_headers(headers.iter())?;
463
464                // use previous checkpoint as seed size
465                let end = input.target.unwrap_or_default() + 1;
466
467                if start + 1 >= end {
468                    return Ok(Vec::default())
469                }
470
471                let mut headers = random_header_range(&mut rng, start + 1..end, head.hash());
472                headers.insert(0, head);
473                Ok(headers)
474            }
475
476            /// Validate stored headers
477            fn validate_execution(
478                &self,
479                input: ExecInput,
480                output: Option<ExecOutput>,
481            ) -> Result<(), TestRunnerError> {
482                let initial_checkpoint = input.checkpoint().block_number;
483                match output {
484                    Some(output) if output.checkpoint.block_number > initial_checkpoint => {
485                        let provider = self.db.factory.provider()?;
486
487                        for block_num in initial_checkpoint..output.checkpoint.block_number {
488                            // look up the header hash
489                            let hash = provider.block_hash(block_num)?.expect("no header hash");
490
491                            // validate the header number
492                            assert_eq!(provider.block_number(hash)?, Some(block_num));
493
494                            // validate the header
495                            let header = provider.header_by_number(block_num)?;
496                            assert!(header.is_some());
497                            let header = SealedHeader::seal_slow(header.unwrap());
498                            assert_eq!(header.hash(), hash);
499                        }
500                    }
501                    _ => self.check_no_header_entry_above(initial_checkpoint)?,
502                };
503                Ok(())
504            }
505
506            async fn after_execution(&self, headers: Self::Seed) -> Result<(), TestRunnerError> {
507                self.client.extend(headers.iter().map(|h| h.clone_header())).await;
508                let tip = if headers.is_empty() {
509                    let tip = random_header(&mut generators::rng(), 0, None);
510                    self.db.insert_headers(std::iter::once(&tip))?;
511                    tip.hash()
512                } else {
513                    headers.last().unwrap().hash()
514                };
515                self.send_tip(tip);
516                Ok(())
517            }
518        }
519
520        impl<D: HeaderDownloader<Header = alloy_consensus::Header> + 'static> UnwindStageTestRunner
521            for HeadersTestRunner<D>
522        {
523            fn validate_unwind(&self, input: UnwindInput) -> Result<(), TestRunnerError> {
524                self.check_no_header_entry_above(input.unwind_to)
525            }
526        }
527
528        impl HeadersTestRunner<ReverseHeadersDownloader<TestHeadersClient>> {
529            pub(crate) fn with_linear_downloader() -> Self {
530                let client = TestHeadersClient::default();
531                Self {
532                    client: client.clone(),
533                    channel: watch::channel(B256::ZERO),
534                    downloader_factory: Box::new(move || {
535                        ReverseHeadersDownloaderBuilder::default()
536                            .stream_batch_size(500)
537                            .build(client.clone(), Arc::new(TestConsensus::default()))
538                    }),
539                    db: TestStageDB::default(),
540                }
541            }
542        }
543
544        impl<D: HeaderDownloader> HeadersTestRunner<D> {
545            pub(crate) fn check_no_header_entry_above(
546                &self,
547                block: BlockNumber,
548            ) -> Result<(), TestRunnerError> {
549                self.db
550                    .ensure_no_entry_above_by_value::<tables::HeaderNumbers, _>(block, |val| val)?;
551                self.db.ensure_no_entry_above::<tables::CanonicalHeaders, _>(block, |key| key)?;
552                self.db.ensure_no_entry_above::<tables::Headers, _>(block, |key| key)?;
553                Ok(())
554            }
555
556            pub(crate) fn send_tip(&self, tip: B256) {
557                self.channel.0.send(tip).expect("failed to send tip");
558            }
559        }
560    }
561
562    stage_test_suite!(HeadersTestRunner, headers);
563
564    /// Execute the stage with linear downloader, unwinds, and ensures that the database tables
565    /// along with the static files are cleaned up.
566    #[tokio::test]
567    async fn execute_with_linear_downloader_unwind() {
568        let mut runner = HeadersTestRunner::with_linear_downloader();
569        let (checkpoint, previous_stage) = (1000, 1200);
570        let input = ExecInput {
571            target: Some(previous_stage),
572            checkpoint: Some(StageCheckpoint::new(checkpoint)),
573        };
574        let headers = runner.seed_execution(input).expect("failed to seed execution");
575        let rx = runner.execute(input);
576
577        runner.client.extend(headers.iter().rev().map(|h| h.clone_header())).await;
578
579        // skip `after_execution` hook for linear downloader
580        let tip = headers.last().unwrap();
581        runner.send_tip(tip.hash());
582
583        let result = rx.await.unwrap();
584        runner.db().factory.static_file_provider().commit().unwrap();
585        assert_matches!(result, Ok(ExecOutput { checkpoint: StageCheckpoint {
586            block_number,
587            stage_checkpoint: Some(StageUnitCheckpoint::Headers(HeadersCheckpoint {
588                block_range: CheckpointBlockRange {
589                    from,
590                    to
591                },
592                progress: EntitiesCheckpoint {
593                    processed,
594                    total,
595                }
596            }))
597        }, done: true }) if block_number == tip.number &&
598            from == checkpoint && to == previous_stage &&
599            // -1 because we don't need to download the local head
600            processed == checkpoint + headers.len() as u64 - 1 && total == tip.number
601        );
602        assert!(runner.validate_execution(input, result.ok()).is_ok(), "validation failed");
603        assert!(runner.stage().hash_collector.is_empty());
604        assert!(runner.stage().header_collector.is_empty());
605
606        // let's insert some blocks using append_blocks_with_state
607        let sealed_headers = random_header_range(
608            &mut generators::rng(),
609            tip.number + 1..tip.number + 10,
610            tip.hash(),
611        );
612
613        insert_headers(&runner.db().factory, &sealed_headers);
614
615        // now we can unwind 10 blocks
616        let unwind_input = UnwindInput {
617            checkpoint: StageCheckpoint::new(tip.number + 10),
618            unwind_to: tip.number,
619            bad_block: None,
620        };
621
622        let unwind_output = runner.unwind(unwind_input).await.unwrap();
623        assert_eq!(unwind_output.checkpoint.block_number, tip.number);
624
625        // validate the unwind, ensure that the tables are cleaned up
626        assert!(runner.validate_unwind(unwind_input).is_ok());
627    }
628
629    /// Execute the stage with linear downloader
630    #[tokio::test]
631    async fn execute_with_linear_downloader() {
632        let mut runner = HeadersTestRunner::with_linear_downloader();
633        let (checkpoint, previous_stage) = (1000, 1200);
634        let input = ExecInput {
635            target: Some(previous_stage),
636            checkpoint: Some(StageCheckpoint::new(checkpoint)),
637        };
638        let headers = runner.seed_execution(input).expect("failed to seed execution");
639        let rx = runner.execute(input);
640
641        runner.client.extend(headers.iter().rev().map(|h| h.clone_header())).await;
642
643        // skip `after_execution` hook for linear downloader
644        let tip = headers.last().unwrap();
645        runner.send_tip(tip.hash());
646
647        let result = rx.await.unwrap();
648        runner.db().factory.static_file_provider().commit().unwrap();
649        assert_matches!(result, Ok(ExecOutput { checkpoint: StageCheckpoint {
650            block_number,
651            stage_checkpoint: Some(StageUnitCheckpoint::Headers(HeadersCheckpoint {
652                block_range: CheckpointBlockRange {
653                    from,
654                    to
655                },
656                progress: EntitiesCheckpoint {
657                    processed,
658                    total,
659                }
660            }))
661        }, done: true }) if block_number == tip.number &&
662            from == checkpoint && to == previous_stage &&
663            // -1 because we don't need to download the local head
664            processed == checkpoint + headers.len() as u64 - 1 && total == tip.number
665        );
666        assert!(runner.validate_execution(input, result.ok()).is_ok(), "validation failed");
667        assert!(runner.stage().hash_collector.is_empty());
668        assert!(runner.stage().header_collector.is_empty());
669    }
670}