Skip to main content

reth_engine_tree/
download.rs

1//! Handler that can download blocks on demand (e.g. from the network).
2
3use crate::{engine::DownloadRequest, metrics::BlockDownloaderMetrics};
4use alloy_consensus::BlockHeader;
5use alloy_primitives::{map::B256Set, B256};
6use futures::FutureExt;
7use reth_consensus::Consensus;
8use reth_network_p2p::{
9    full_block::{
10        FetchFullBlockFuture, FetchFullBlockRangeFuture, FetchFullBlockRangeWithBalFuture,
11        FetchFullBlockWithBalFuture, FullBlockClient, SealedBlockWithAccessList,
12    },
13    BlockAccessListsClient, BlockClient,
14};
15use reth_primitives_traits::{Block, SealedBlockWith};
16use std::{
17    cmp::{Ordering, Reverse},
18    collections::{binary_heap::PeekMut, BinaryHeap, VecDeque},
19    fmt::Debug,
20    sync::Arc,
21    task::{Context, Poll},
22};
23use tracing::trace;
24
25/// A trait that can download blocks on demand.
26pub trait BlockDownloader: Send + Sync {
27    /// Type of the block being downloaded.
28    type Block: Block;
29
30    /// Handle an action.
31    fn on_action(&mut self, action: DownloadAction);
32
33    /// Advance in progress requests if any
34    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<DownloadOutcome<Self::Block>>;
35}
36
37/// Actions that can be performed by the block downloader.
38#[derive(Debug)]
39pub enum DownloadAction {
40    /// Stop downloading blocks.
41    Clear,
42    /// Download given blocks
43    Download(DownloadRequest),
44}
45
46/// Outcome of downloaded blocks.
47#[derive(Debug)]
48pub enum DownloadOutcome<B: Block> {
49    /// Downloaded blocks with optional block access list data.
50    Blocks(Vec<SealedBlockWithAccessList<B>>),
51    /// New download started.
52    NewDownloadStarted {
53        /// How many blocks are pending in this download.
54        remaining_blocks: u64,
55        /// The hash of the highest block of this download.
56        target: B256,
57    },
58}
59
60/// Basic [`BlockDownloader`].
61#[expect(missing_debug_implementations)]
62pub struct BasicBlockDownloader<Client, B: Block>
63where
64    Client: BlockClient + BlockAccessListsClient + 'static,
65{
66    /// A downloader that can download full blocks from the network.
67    full_block_client: FullBlockClient<Client>,
68    /// In-flight full block requests in progress.
69    inflight_full_block_requests: Vec<FullBlockDownload<Client>>,
70    /// In-flight full block _range_ requests in progress.
71    inflight_block_range_requests: Vec<FullBlockRangeDownload<Client>>,
72    /// Buffered blocks from downloads - this is a min-heap of blocks, using the block number for
73    /// ordering. This means the blocks will be popped from the heap with ascending block numbers.
74    set_buffered_blocks: BinaryHeap<Reverse<OrderedDownloadedBlock<B>>>,
75    /// Engine download metrics.
76    metrics: BlockDownloaderMetrics,
77    /// Pending events to be emitted.
78    pending_events: VecDeque<DownloadOutcome<B>>,
79}
80
81impl<Client, B> BasicBlockDownloader<Client, B>
82where
83    Client: BlockClient<Block = B> + BlockAccessListsClient + 'static,
84    B: Block,
85{
86    /// Create a new instance
87    pub fn new(client: Client, consensus: Arc<dyn Consensus<B>>) -> Self {
88        Self {
89            full_block_client: FullBlockClient::new(client, consensus),
90            inflight_full_block_requests: Vec::new(),
91            inflight_block_range_requests: Vec::new(),
92            set_buffered_blocks: BinaryHeap::new(),
93            metrics: BlockDownloaderMetrics::default(),
94            pending_events: Default::default(),
95        }
96    }
97
98    /// Clears the stored inflight requests.
99    fn clear(&mut self) {
100        self.inflight_full_block_requests.clear();
101        self.inflight_block_range_requests.clear();
102        self.set_buffered_blocks.clear();
103        self.update_block_download_metrics();
104    }
105
106    /// Processes a download request.
107    fn download(&mut self, request: DownloadRequest) {
108        match request {
109            DownloadRequest::BlockSet { hashes, access_lists } => {
110                self.download_block_set(hashes, access_lists)
111            }
112            DownloadRequest::BlockRange { hash, count, access_lists } => {
113                self.download_block_range(hash, count, access_lists)
114            }
115        }
116    }
117
118    /// Processes a block set download request.
119    fn download_block_set(&mut self, hashes: B256Set, access_lists: bool) {
120        for hash in hashes {
121            self.download_full_block(hash, access_lists);
122        }
123    }
124
125    /// Processes a block range download request.
126    fn download_block_range(&mut self, hash: B256, count: u64, access_lists: bool) {
127        if count == 1 {
128            self.download_full_block(hash, access_lists);
129        } else {
130            if self.inflight_block_range_requests.iter().any(|request| {
131                request.start_hash() == hash &&
132                    request.count() == count &&
133                    request.access_lists() == access_lists
134            }) {
135                return
136            }
137
138            trace!(
139                target: "engine::download",
140                ?hash,
141                ?count,
142                access_lists,
143                "start downloading full block range."
144            );
145
146            let request = if access_lists {
147                FullBlockRangeDownload::WithAccessLists(
148                    self.full_block_client
149                        .get_full_block_range_with_optional_access_lists(hash, count),
150                )
151            } else {
152                FullBlockRangeDownload::Blocks(
153                    self.full_block_client.get_full_block_range(hash, count),
154                )
155            };
156            self.push_pending_event(DownloadOutcome::NewDownloadStarted {
157                remaining_blocks: request.count(),
158                target: request.start_hash(),
159            });
160            self.inflight_block_range_requests.push(request);
161
162            self.update_block_download_metrics();
163        }
164    }
165
166    /// Starts requesting a full block from the network.
167    ///
168    /// Returns `true` if the request was started, `false` if there's already a request for the
169    /// given hash.
170    fn download_full_block(&mut self, hash: B256, access_lists: bool) -> bool {
171        if self.is_inflight_request(hash) {
172            return false
173        }
174        self.push_pending_event(DownloadOutcome::NewDownloadStarted {
175            remaining_blocks: 1,
176            target: hash,
177        });
178
179        trace!(
180            target: "engine::download",
181            ?hash,
182            access_lists,
183            "Start downloading full block"
184        );
185
186        let request = if access_lists {
187            FullBlockDownload::WithAccessList(
188                self.full_block_client.get_full_block_with_access_lists(hash),
189            )
190        } else {
191            FullBlockDownload::Block(self.full_block_client.get_full_block(hash))
192        };
193        self.inflight_full_block_requests.push(request);
194
195        self.update_block_download_metrics();
196
197        true
198    }
199
200    /// Returns true if there's already a request for the given hash.
201    fn is_inflight_request(&self, hash: B256) -> bool {
202        self.inflight_full_block_requests.iter().any(|req| *req.hash() == hash)
203    }
204
205    /// Sets the metrics for the active downloads
206    fn update_block_download_metrics(&self) {
207        let blocks = self.inflight_full_block_requests.len() +
208            self.inflight_block_range_requests.iter().map(|r| r.count() as usize).sum::<usize>();
209        self.metrics.active_block_downloads.set(blocks as f64);
210    }
211
212    /// Adds a pending event to the FIFO queue.
213    fn push_pending_event(&mut self, pending_event: DownloadOutcome<B>) {
214        self.pending_events.push_back(pending_event);
215    }
216
217    /// Removes a pending event from the FIFO queue.
218    fn pop_pending_event(&mut self) -> Option<DownloadOutcome<B>> {
219        self.pending_events.pop_front()
220    }
221}
222
223impl<Client, B> BlockDownloader for BasicBlockDownloader<Client, B>
224where
225    Client: BlockClient<Block = B> + BlockAccessListsClient,
226    B: Block,
227{
228    type Block = B;
229
230    /// Handles incoming download actions.
231    fn on_action(&mut self, action: DownloadAction) {
232        match action {
233            DownloadAction::Clear => self.clear(),
234            DownloadAction::Download(request) => self.download(request),
235        }
236    }
237
238    /// Advances the download process.
239    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<DownloadOutcome<B>> {
240        if let Some(pending_event) = self.pop_pending_event() {
241            return Poll::Ready(pending_event);
242        }
243
244        // advance all full block requests
245        for idx in (0..self.inflight_full_block_requests.len()).rev() {
246            let mut request = self.inflight_full_block_requests.swap_remove(idx);
247            if let Poll::Ready(block) = request.poll(cx) {
248                trace!(target: "engine::download", block=?block.num_hash(), "Received single full block, buffering");
249                self.set_buffered_blocks.push(Reverse(block.into()));
250            } else {
251                // still pending
252                self.inflight_full_block_requests.push(request);
253            }
254        }
255
256        // advance all full block range requests
257        for idx in (0..self.inflight_block_range_requests.len()).rev() {
258            let mut request = self.inflight_block_range_requests.swap_remove(idx);
259            if let Poll::Ready(blocks) = request.poll(cx) {
260                trace!(target: "engine::download", len=?blocks.len(), first=?blocks.first().map(|b| b.num_hash()), last=?blocks.last().map(|b| b.num_hash()), "Received full block range, buffering");
261                self.set_buffered_blocks
262                    .extend(blocks.into_iter().map(OrderedDownloadedBlock).map(Reverse));
263            } else {
264                // still pending
265                self.inflight_block_range_requests.push(request);
266            }
267        }
268
269        self.update_block_download_metrics();
270
271        if self.set_buffered_blocks.is_empty() {
272            return Poll::Pending;
273        }
274
275        // drain all unique element of the block buffer if there are any
276        let mut downloaded_blocks = Vec::with_capacity(self.set_buffered_blocks.len());
277        while let Some(block) = self.set_buffered_blocks.pop() {
278            let mut block = block.0 .0;
279            // peek ahead and pop duplicates, keeping the copy that includes access list data
280            while let Some(peek) = self.set_buffered_blocks.peek_mut() {
281                if peek.0 .0.hash() == block.hash() {
282                    let duplicate = PeekMut::pop(peek).0 .0;
283                    if block.data().is_none() && duplicate.data().is_some() {
284                        block = duplicate;
285                    }
286                } else {
287                    break
288                }
289            }
290            downloaded_blocks.push(block);
291        }
292        Poll::Ready(DownloadOutcome::Blocks(downloaded_blocks))
293    }
294}
295
296/// A wrapper type around [`SealedBlockWithAccessList`] that implements the [Ord]
297/// trait by block number.
298#[derive(Debug, Clone, PartialEq, Eq)]
299struct OrderedDownloadedBlock<B: Block>(SealedBlockWithAccessList<B>);
300
301impl<B: Block> PartialOrd for OrderedDownloadedBlock<B> {
302    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
303        Some(self.cmp(other))
304    }
305}
306
307impl<B: Block> Ord for OrderedDownloadedBlock<B> {
308    fn cmp(&self, other: &Self) -> Ordering {
309        self.0.number().cmp(&other.0.number())
310    }
311}
312
313impl<B: Block> From<SealedBlockWithAccessList<B>> for OrderedDownloadedBlock<B> {
314    fn from(block: SealedBlockWithAccessList<B>) -> Self {
315        Self(block)
316    }
317}
318
319/// An in-flight full block request that optionally also fetches the block's access list.
320enum FullBlockDownload<Client>
321where
322    Client: BlockClient + BlockAccessListsClient,
323{
324    /// Fetches the block only.
325    Block(FetchFullBlockFuture<Client>),
326    /// Fetches the block and attempts to fetch its access list.
327    WithAccessList(FetchFullBlockWithBalFuture<Client>),
328}
329
330impl<Client> FullBlockDownload<Client>
331where
332    Client: BlockClient + BlockAccessListsClient + 'static,
333{
334    /// Returns the hash of the block being requested.
335    const fn hash(&self) -> &B256 {
336        match self {
337            Self::Block(req) => req.hash(),
338            Self::WithAccessList(req) => req.hash(),
339        }
340    }
341
342    /// Advances the download.
343    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SealedBlockWithAccessList<Client::Block>> {
344        match self {
345            Self::Block(req) => req.poll_unpin(cx).map(SealedBlockWith::from_block),
346            Self::WithAccessList(req) => req.poll_unpin(cx),
347        }
348    }
349}
350
351/// An in-flight full block range request that optionally also fetches the blocks' access lists.
352enum FullBlockRangeDownload<Client>
353where
354    Client: BlockClient + BlockAccessListsClient,
355{
356    /// Fetches the block range only.
357    Blocks(FetchFullBlockRangeFuture<Client>),
358    /// Fetches the block range and attempts to fetch the blocks' access lists.
359    WithAccessLists(FetchFullBlockRangeWithBalFuture<Client>),
360}
361
362impl<Client> FullBlockRangeDownload<Client>
363where
364    Client: BlockClient + BlockAccessListsClient + 'static,
365{
366    /// Returns the block hash the requested range starts at (inclusive).
367    const fn start_hash(&self) -> B256 {
368        match self {
369            Self::Blocks(req) => req.start_hash(),
370            Self::WithAccessLists(req) => req.start_hash(),
371        }
372    }
373
374    /// Returns the number of requested blocks.
375    const fn count(&self) -> u64 {
376        match self {
377            Self::Blocks(req) => req.count(),
378            Self::WithAccessLists(req) => req.count(),
379        }
380    }
381
382    /// Returns whether the request also fetches block access lists.
383    const fn access_lists(&self) -> bool {
384        matches!(self, Self::WithAccessLists(_))
385    }
386
387    /// Advances the download.
388    fn poll(
389        &mut self,
390        cx: &mut Context<'_>,
391    ) -> Poll<Vec<SealedBlockWithAccessList<Client::Block>>> {
392        match self {
393            Self::Blocks(req) => req
394                .poll_unpin(cx)
395                .map(|blocks| blocks.into_iter().map(SealedBlockWith::from_block).collect()),
396            Self::WithAccessLists(req) => req.poll_unpin(cx),
397        }
398    }
399}
400
401/// A [`BlockDownloader`] that does nothing.
402#[derive(Debug, Clone, Default)]
403#[non_exhaustive]
404pub struct NoopBlockDownloader<B>(core::marker::PhantomData<B>);
405
406impl<B: Block> BlockDownloader for NoopBlockDownloader<B> {
407    type Block = B;
408
409    fn on_action(&mut self, _event: DownloadAction) {}
410
411    fn poll(&mut self, _cx: &mut Context<'_>) -> Poll<DownloadOutcome<B>> {
412        Poll::Pending
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use crate::test_utils::insert_headers_into_client;
420    use alloy_consensus::Header;
421    use alloy_eips::eip1559::ETHEREUM_BLOCK_GAS_LIMIT_30M;
422    use assert_matches::assert_matches;
423    use reth_chainspec::{ChainSpecBuilder, MAINNET};
424    use reth_ethereum_consensus::EthBeaconConsensus;
425    use reth_network_p2p::test_utils::TestFullBlockClient;
426    use reth_primitives_traits::SealedHeader;
427    use std::{future::poll_fn, sync::Arc};
428
429    struct TestHarness {
430        block_downloader:
431            BasicBlockDownloader<TestFullBlockClient, reth_ethereum_primitives::Block>,
432        client: TestFullBlockClient,
433    }
434
435    impl TestHarness {
436        fn new(total_blocks: usize) -> Self {
437            let chain_spec = Arc::new(
438                ChainSpecBuilder::default()
439                    .chain(MAINNET.chain)
440                    .genesis(MAINNET.genesis.clone())
441                    .paris_activated()
442                    .build(),
443            );
444
445            let client = TestFullBlockClient::default();
446            let header = Header {
447                base_fee_per_gas: Some(7),
448                gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
449                ..Default::default()
450            };
451            let header = SealedHeader::seal_slow(header);
452
453            insert_headers_into_client(&client, header, 0..total_blocks);
454            let consensus = Arc::new(EthBeaconConsensus::new(chain_spec));
455
456            let block_downloader = BasicBlockDownloader::new(client.clone(), consensus);
457            Self { block_downloader, client }
458        }
459    }
460
461    #[tokio::test]
462    async fn block_downloader_range_request() {
463        const TOTAL_BLOCKS: usize = 10;
464        let TestHarness { mut block_downloader, client } = TestHarness::new(TOTAL_BLOCKS);
465        let tip = client.highest_block().expect("there should be blocks here");
466
467        // send block range download request
468        block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(
469            tip.hash(),
470            tip.number,
471        )));
472
473        // ensure we have one in flight range request
474        assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
475
476        // ensure the range request is made correctly
477        let first_req = block_downloader.inflight_block_range_requests.first().unwrap();
478        assert_eq!(first_req.start_hash(), tip.hash());
479        assert_eq!(first_req.count(), tip.number);
480
481        // poll downloader
482        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
483        let next_ready = sync_future.await;
484
485        assert_matches!(next_ready, DownloadOutcome::NewDownloadStarted { remaining_blocks, .. } => {
486            assert_eq!(remaining_blocks, TOTAL_BLOCKS as u64);
487        });
488
489        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
490        let next_ready = sync_future.await;
491
492        assert_matches!(next_ready, DownloadOutcome::Blocks(blocks) => {
493            // ensure all blocks were obtained
494            assert_eq!(blocks.len(), TOTAL_BLOCKS);
495
496            // ensure they are in ascending order
497            for num in 1..=TOTAL_BLOCKS {
498                assert_eq!(blocks[num - 1].number(), num as u64);
499            }
500        });
501    }
502
503    #[tokio::test]
504    async fn block_downloader_deduplicates_range_requests() {
505        let TestHarness { mut block_downloader, client } = TestHarness::new(3);
506        let hash = client.highest_block().unwrap().hash();
507        let request = || DownloadRequest::block_range(hash, 2);
508
509        block_downloader.on_action(DownloadAction::Download(request()));
510        block_downloader.on_action(DownloadAction::Download(request()));
511        assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
512
513        block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(hash, 3)));
514        block_downloader.on_action(DownloadAction::Download(request().with_access_lists(true)));
515        assert_eq!(block_downloader.inflight_block_range_requests.len(), 3);
516    }
517
518    #[tokio::test]
519    async fn block_downloader_set_request() {
520        const TOTAL_BLOCKS: usize = 2;
521        let TestHarness { mut block_downloader, client } = TestHarness::new(TOTAL_BLOCKS);
522
523        let tip = client.highest_block().expect("there should be blocks here");
524
525        // send block set download request
526        block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_set(
527            B256Set::from_iter([tip.hash(), tip.parent_hash]),
528        )));
529
530        // ensure we have TOTAL_BLOCKS in flight full block request
531        assert_eq!(block_downloader.inflight_full_block_requests.len(), TOTAL_BLOCKS);
532
533        // poll downloader
534        for _ in 0..TOTAL_BLOCKS {
535            let sync_future = poll_fn(|cx| block_downloader.poll(cx));
536            let next_ready = sync_future.await;
537
538            assert_matches!(next_ready, DownloadOutcome::NewDownloadStarted { remaining_blocks, .. } => {
539                assert_eq!(remaining_blocks, 1);
540            });
541        }
542
543        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
544        let next_ready = sync_future.await;
545        assert_matches!(next_ready, DownloadOutcome::Blocks(blocks) => {
546            // ensure all blocks were obtained
547            assert_eq!(blocks.len(), TOTAL_BLOCKS);
548
549            // ensure they are in ascending order
550            for num in 1..=TOTAL_BLOCKS {
551                assert_eq!(blocks[num - 1].number(), num as u64);
552            }
553        });
554    }
555
556    #[tokio::test]
557    async fn block_downloader_range_request_with_access_lists() {
558        const TOTAL_BLOCKS: usize = 4;
559        let chain_spec = Arc::new(
560            ChainSpecBuilder::default()
561                .chain(MAINNET.chain)
562                .genesis(MAINNET.genesis.clone())
563                .paris_activated()
564                .build(),
565        );
566
567        let client = TestFullBlockClient::default();
568        // empty RLP list
569        let access_list = alloy_primitives::Bytes::from_static(&[0xc0]);
570        let header = Header {
571            base_fee_per_gas: Some(7),
572            gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
573            block_access_list_hash: Some(alloy_primitives::keccak256(access_list.as_ref())),
574            ..Default::default()
575        };
576        let mut sealed_header = SealedHeader::seal_slow(header);
577        let body = reth_ethereum_primitives::BlockBody::default();
578        for _ in 0..TOTAL_BLOCKS {
579            let (mut header, hash) = sealed_header.split();
580            header.parent_hash = hash;
581            header.number += 1;
582            header.timestamp += 1;
583            sealed_header = SealedHeader::seal_slow(header);
584            client.insert(sealed_header.clone(), body.clone());
585            client.insert_access_list(sealed_header.hash(), access_list.clone());
586        }
587
588        let consensus = Arc::new(EthBeaconConsensus::new(chain_spec).with_allow_bal_hashes(true));
589        let mut block_downloader = BasicBlockDownloader::new(client.clone(), consensus);
590
591        let tip = client.highest_block().expect("there should be blocks here");
592
593        block_downloader.on_action(DownloadAction::Download(
594            DownloadRequest::block_range(tip.hash(), tip.number).with_access_lists(true),
595        ));
596
597        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
598        let next_ready = sync_future.await;
599
600        assert_matches!(next_ready, DownloadOutcome::NewDownloadStarted { remaining_blocks, .. } => {
601            assert_eq!(remaining_blocks, TOTAL_BLOCKS as u64);
602        });
603
604        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
605        let next_ready = sync_future.await;
606
607        assert_matches!(next_ready, DownloadOutcome::Blocks(blocks) => {
608            assert_eq!(blocks.len(), TOTAL_BLOCKS);
609
610            // every block carries its validated access list data
611            for block in &blocks {
612                assert!(block.data().is_some());
613            }
614        });
615    }
616
617    #[tokio::test]
618    async fn block_downloader_clear_request() {
619        const TOTAL_BLOCKS: usize = 10;
620        let TestHarness { mut block_downloader, client } = TestHarness::new(TOTAL_BLOCKS);
621
622        let tip = client.highest_block().expect("there should be blocks here");
623
624        // send block range download request
625        block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(
626            tip.hash(),
627            tip.number,
628        )));
629
630        // send block set download request
631        let download_set = B256Set::from_iter([tip.hash(), tip.parent_hash]);
632        block_downloader
633            .on_action(DownloadAction::Download(DownloadRequest::block_set(download_set.clone())));
634
635        // ensure we have one in flight range request
636        assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
637
638        // ensure the range request is made correctly
639        let first_req = block_downloader.inflight_block_range_requests.first().unwrap();
640        assert_eq!(first_req.start_hash(), tip.hash());
641        assert_eq!(first_req.count(), tip.number);
642
643        // ensure we have download_set.len() in flight full block request
644        assert_eq!(block_downloader.inflight_full_block_requests.len(), download_set.len());
645
646        // send clear request
647        block_downloader.on_action(DownloadAction::Clear);
648
649        // ensure we have no in flight range request
650        assert_eq!(block_downloader.inflight_block_range_requests.len(), 0);
651
652        // ensure we have no in flight full block request
653        assert_eq!(block_downloader.inflight_full_block_requests.len(), 0);
654    }
655}