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            trace!(
131                target: "engine::download",
132                ?hash,
133                ?count,
134                access_lists,
135                "start downloading full block range."
136            );
137
138            let request = if access_lists {
139                FullBlockRangeDownload::WithAccessLists(
140                    self.full_block_client
141                        .get_full_block_range_with_optional_access_lists(hash, count),
142                )
143            } else {
144                FullBlockRangeDownload::Blocks(
145                    self.full_block_client.get_full_block_range(hash, count),
146                )
147            };
148            self.push_pending_event(DownloadOutcome::NewDownloadStarted {
149                remaining_blocks: request.count(),
150                target: request.start_hash(),
151            });
152            self.inflight_block_range_requests.push(request);
153
154            self.update_block_download_metrics();
155        }
156    }
157
158    /// Starts requesting a full block from the network.
159    ///
160    /// Returns `true` if the request was started, `false` if there's already a request for the
161    /// given hash.
162    fn download_full_block(&mut self, hash: B256, access_lists: bool) -> bool {
163        if self.is_inflight_request(hash) {
164            return false
165        }
166        self.push_pending_event(DownloadOutcome::NewDownloadStarted {
167            remaining_blocks: 1,
168            target: hash,
169        });
170
171        trace!(
172            target: "engine::download",
173            ?hash,
174            access_lists,
175            "Start downloading full block"
176        );
177
178        let request = if access_lists {
179            FullBlockDownload::WithAccessList(
180                self.full_block_client.get_full_block_with_access_lists(hash),
181            )
182        } else {
183            FullBlockDownload::Block(self.full_block_client.get_full_block(hash))
184        };
185        self.inflight_full_block_requests.push(request);
186
187        self.update_block_download_metrics();
188
189        true
190    }
191
192    /// Returns true if there's already a request for the given hash.
193    fn is_inflight_request(&self, hash: B256) -> bool {
194        self.inflight_full_block_requests.iter().any(|req| *req.hash() == hash)
195    }
196
197    /// Sets the metrics for the active downloads
198    fn update_block_download_metrics(&self) {
199        let blocks = self.inflight_full_block_requests.len() +
200            self.inflight_block_range_requests.iter().map(|r| r.count() as usize).sum::<usize>();
201        self.metrics.active_block_downloads.set(blocks as f64);
202    }
203
204    /// Adds a pending event to the FIFO queue.
205    fn push_pending_event(&mut self, pending_event: DownloadOutcome<B>) {
206        self.pending_events.push_back(pending_event);
207    }
208
209    /// Removes a pending event from the FIFO queue.
210    fn pop_pending_event(&mut self) -> Option<DownloadOutcome<B>> {
211        self.pending_events.pop_front()
212    }
213}
214
215impl<Client, B> BlockDownloader for BasicBlockDownloader<Client, B>
216where
217    Client: BlockClient<Block = B> + BlockAccessListsClient,
218    B: Block,
219{
220    type Block = B;
221
222    /// Handles incoming download actions.
223    fn on_action(&mut self, action: DownloadAction) {
224        match action {
225            DownloadAction::Clear => self.clear(),
226            DownloadAction::Download(request) => self.download(request),
227        }
228    }
229
230    /// Advances the download process.
231    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<DownloadOutcome<B>> {
232        if let Some(pending_event) = self.pop_pending_event() {
233            return Poll::Ready(pending_event);
234        }
235
236        // advance all full block requests
237        for idx in (0..self.inflight_full_block_requests.len()).rev() {
238            let mut request = self.inflight_full_block_requests.swap_remove(idx);
239            if let Poll::Ready(block) = request.poll(cx) {
240                trace!(target: "engine::download", block=?block.num_hash(), "Received single full block, buffering");
241                self.set_buffered_blocks.push(Reverse(block.into()));
242            } else {
243                // still pending
244                self.inflight_full_block_requests.push(request);
245            }
246        }
247
248        // advance all full block range requests
249        for idx in (0..self.inflight_block_range_requests.len()).rev() {
250            let mut request = self.inflight_block_range_requests.swap_remove(idx);
251            if let Poll::Ready(blocks) = request.poll(cx) {
252                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");
253                self.set_buffered_blocks
254                    .extend(blocks.into_iter().map(OrderedDownloadedBlock).map(Reverse));
255            } else {
256                // still pending
257                self.inflight_block_range_requests.push(request);
258            }
259        }
260
261        self.update_block_download_metrics();
262
263        if self.set_buffered_blocks.is_empty() {
264            return Poll::Pending;
265        }
266
267        // drain all unique element of the block buffer if there are any
268        let mut downloaded_blocks = Vec::with_capacity(self.set_buffered_blocks.len());
269        while let Some(block) = self.set_buffered_blocks.pop() {
270            let mut block = block.0 .0;
271            // peek ahead and pop duplicates, keeping the copy that includes access list data
272            while let Some(peek) = self.set_buffered_blocks.peek_mut() {
273                if peek.0 .0.hash() == block.hash() {
274                    let duplicate = PeekMut::pop(peek).0 .0;
275                    if block.data().is_none() && duplicate.data().is_some() {
276                        block = duplicate;
277                    }
278                } else {
279                    break
280                }
281            }
282            downloaded_blocks.push(block);
283        }
284        Poll::Ready(DownloadOutcome::Blocks(downloaded_blocks))
285    }
286}
287
288/// A wrapper type around [`SealedBlockWithAccessList`] that implements the [Ord]
289/// trait by block number.
290#[derive(Debug, Clone, PartialEq, Eq)]
291struct OrderedDownloadedBlock<B: Block>(SealedBlockWithAccessList<B>);
292
293impl<B: Block> PartialOrd for OrderedDownloadedBlock<B> {
294    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
295        Some(self.cmp(other))
296    }
297}
298
299impl<B: Block> Ord for OrderedDownloadedBlock<B> {
300    fn cmp(&self, other: &Self) -> Ordering {
301        self.0.number().cmp(&other.0.number())
302    }
303}
304
305impl<B: Block> From<SealedBlockWithAccessList<B>> for OrderedDownloadedBlock<B> {
306    fn from(block: SealedBlockWithAccessList<B>) -> Self {
307        Self(block)
308    }
309}
310
311/// An in-flight full block request that optionally also fetches the block's access list.
312enum FullBlockDownload<Client>
313where
314    Client: BlockClient + BlockAccessListsClient,
315{
316    /// Fetches the block only.
317    Block(FetchFullBlockFuture<Client>),
318    /// Fetches the block and attempts to fetch its access list.
319    WithAccessList(FetchFullBlockWithBalFuture<Client>),
320}
321
322impl<Client> FullBlockDownload<Client>
323where
324    Client: BlockClient + BlockAccessListsClient + 'static,
325{
326    /// Returns the hash of the block being requested.
327    const fn hash(&self) -> &B256 {
328        match self {
329            Self::Block(req) => req.hash(),
330            Self::WithAccessList(req) => req.hash(),
331        }
332    }
333
334    /// Advances the download.
335    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SealedBlockWithAccessList<Client::Block>> {
336        match self {
337            Self::Block(req) => req.poll_unpin(cx).map(SealedBlockWith::from_block),
338            Self::WithAccessList(req) => req.poll_unpin(cx),
339        }
340    }
341}
342
343/// An in-flight full block range request that optionally also fetches the blocks' access lists.
344enum FullBlockRangeDownload<Client>
345where
346    Client: BlockClient + BlockAccessListsClient,
347{
348    /// Fetches the block range only.
349    Blocks(FetchFullBlockRangeFuture<Client>),
350    /// Fetches the block range and attempts to fetch the blocks' access lists.
351    WithAccessLists(FetchFullBlockRangeWithBalFuture<Client>),
352}
353
354impl<Client> FullBlockRangeDownload<Client>
355where
356    Client: BlockClient + BlockAccessListsClient + 'static,
357{
358    /// Returns the block hash the requested range starts at (inclusive).
359    const fn start_hash(&self) -> B256 {
360        match self {
361            Self::Blocks(req) => req.start_hash(),
362            Self::WithAccessLists(req) => req.start_hash(),
363        }
364    }
365
366    /// Returns the number of requested blocks.
367    const fn count(&self) -> u64 {
368        match self {
369            Self::Blocks(req) => req.count(),
370            Self::WithAccessLists(req) => req.count(),
371        }
372    }
373
374    /// Advances the download.
375    fn poll(
376        &mut self,
377        cx: &mut Context<'_>,
378    ) -> Poll<Vec<SealedBlockWithAccessList<Client::Block>>> {
379        match self {
380            Self::Blocks(req) => req
381                .poll_unpin(cx)
382                .map(|blocks| blocks.into_iter().map(SealedBlockWith::from_block).collect()),
383            Self::WithAccessLists(req) => req.poll_unpin(cx),
384        }
385    }
386}
387
388/// A [`BlockDownloader`] that does nothing.
389#[derive(Debug, Clone, Default)]
390#[non_exhaustive]
391pub struct NoopBlockDownloader<B>(core::marker::PhantomData<B>);
392
393impl<B: Block> BlockDownloader for NoopBlockDownloader<B> {
394    type Block = B;
395
396    fn on_action(&mut self, _event: DownloadAction) {}
397
398    fn poll(&mut self, _cx: &mut Context<'_>) -> Poll<DownloadOutcome<B>> {
399        Poll::Pending
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::test_utils::insert_headers_into_client;
407    use alloy_consensus::Header;
408    use alloy_eips::eip1559::ETHEREUM_BLOCK_GAS_LIMIT_30M;
409    use assert_matches::assert_matches;
410    use reth_chainspec::{ChainSpecBuilder, MAINNET};
411    use reth_ethereum_consensus::EthBeaconConsensus;
412    use reth_network_p2p::test_utils::TestFullBlockClient;
413    use reth_primitives_traits::SealedHeader;
414    use std::{future::poll_fn, sync::Arc};
415
416    struct TestHarness {
417        block_downloader:
418            BasicBlockDownloader<TestFullBlockClient, reth_ethereum_primitives::Block>,
419        client: TestFullBlockClient,
420    }
421
422    impl TestHarness {
423        fn new(total_blocks: usize) -> Self {
424            let chain_spec = Arc::new(
425                ChainSpecBuilder::default()
426                    .chain(MAINNET.chain)
427                    .genesis(MAINNET.genesis.clone())
428                    .paris_activated()
429                    .build(),
430            );
431
432            let client = TestFullBlockClient::default();
433            let header = Header {
434                base_fee_per_gas: Some(7),
435                gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
436                ..Default::default()
437            };
438            let header = SealedHeader::seal_slow(header);
439
440            insert_headers_into_client(&client, header, 0..total_blocks);
441            let consensus = Arc::new(EthBeaconConsensus::new(chain_spec));
442
443            let block_downloader = BasicBlockDownloader::new(client.clone(), consensus);
444            Self { block_downloader, client }
445        }
446    }
447
448    #[tokio::test]
449    async fn block_downloader_range_request() {
450        const TOTAL_BLOCKS: usize = 10;
451        let TestHarness { mut block_downloader, client } = TestHarness::new(TOTAL_BLOCKS);
452        let tip = client.highest_block().expect("there should be blocks here");
453
454        // send block range download request
455        block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(
456            tip.hash(),
457            tip.number,
458        )));
459
460        // ensure we have one in flight range request
461        assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
462
463        // ensure the range request is made correctly
464        let first_req = block_downloader.inflight_block_range_requests.first().unwrap();
465        assert_eq!(first_req.start_hash(), tip.hash());
466        assert_eq!(first_req.count(), tip.number);
467
468        // poll downloader
469        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
470        let next_ready = sync_future.await;
471
472        assert_matches!(next_ready, DownloadOutcome::NewDownloadStarted { remaining_blocks, .. } => {
473            assert_eq!(remaining_blocks, TOTAL_BLOCKS as u64);
474        });
475
476        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
477        let next_ready = sync_future.await;
478
479        assert_matches!(next_ready, DownloadOutcome::Blocks(blocks) => {
480            // ensure all blocks were obtained
481            assert_eq!(blocks.len(), TOTAL_BLOCKS);
482
483            // ensure they are in ascending order
484            for num in 1..=TOTAL_BLOCKS {
485                assert_eq!(blocks[num - 1].number(), num as u64);
486            }
487        });
488    }
489
490    #[tokio::test]
491    async fn block_downloader_set_request() {
492        const TOTAL_BLOCKS: usize = 2;
493        let TestHarness { mut block_downloader, client } = TestHarness::new(TOTAL_BLOCKS);
494
495        let tip = client.highest_block().expect("there should be blocks here");
496
497        // send block set download request
498        block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_set(
499            B256Set::from_iter([tip.hash(), tip.parent_hash]),
500        )));
501
502        // ensure we have TOTAL_BLOCKS in flight full block request
503        assert_eq!(block_downloader.inflight_full_block_requests.len(), TOTAL_BLOCKS);
504
505        // poll downloader
506        for _ in 0..TOTAL_BLOCKS {
507            let sync_future = poll_fn(|cx| block_downloader.poll(cx));
508            let next_ready = sync_future.await;
509
510            assert_matches!(next_ready, DownloadOutcome::NewDownloadStarted { remaining_blocks, .. } => {
511                assert_eq!(remaining_blocks, 1);
512            });
513        }
514
515        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
516        let next_ready = sync_future.await;
517        assert_matches!(next_ready, DownloadOutcome::Blocks(blocks) => {
518            // ensure all blocks were obtained
519            assert_eq!(blocks.len(), TOTAL_BLOCKS);
520
521            // ensure they are in ascending order
522            for num in 1..=TOTAL_BLOCKS {
523                assert_eq!(blocks[num - 1].number(), num as u64);
524            }
525        });
526    }
527
528    #[tokio::test]
529    async fn block_downloader_range_request_with_access_lists() {
530        const TOTAL_BLOCKS: usize = 4;
531        let chain_spec = Arc::new(
532            ChainSpecBuilder::default()
533                .chain(MAINNET.chain)
534                .genesis(MAINNET.genesis.clone())
535                .paris_activated()
536                .build(),
537        );
538
539        let client = TestFullBlockClient::default();
540        // empty RLP list
541        let access_list = alloy_primitives::Bytes::from_static(&[0xc0]);
542        let header = Header {
543            base_fee_per_gas: Some(7),
544            gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
545            block_access_list_hash: Some(alloy_primitives::keccak256(access_list.as_ref())),
546            ..Default::default()
547        };
548        let mut sealed_header = SealedHeader::seal_slow(header);
549        let body = reth_ethereum_primitives::BlockBody::default();
550        for _ in 0..TOTAL_BLOCKS {
551            let (mut header, hash) = sealed_header.split();
552            header.parent_hash = hash;
553            header.number += 1;
554            header.timestamp += 1;
555            sealed_header = SealedHeader::seal_slow(header);
556            client.insert(sealed_header.clone(), body.clone());
557            client.insert_access_list(sealed_header.hash(), access_list.clone());
558        }
559
560        let consensus = Arc::new(EthBeaconConsensus::new(chain_spec).with_allow_bal_hashes(true));
561        let mut block_downloader = BasicBlockDownloader::new(client.clone(), consensus);
562
563        let tip = client.highest_block().expect("there should be blocks here");
564
565        block_downloader.on_action(DownloadAction::Download(
566            DownloadRequest::block_range(tip.hash(), tip.number).with_access_lists(true),
567        ));
568
569        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
570        let next_ready = sync_future.await;
571
572        assert_matches!(next_ready, DownloadOutcome::NewDownloadStarted { remaining_blocks, .. } => {
573            assert_eq!(remaining_blocks, TOTAL_BLOCKS as u64);
574        });
575
576        let sync_future = poll_fn(|cx| block_downloader.poll(cx));
577        let next_ready = sync_future.await;
578
579        assert_matches!(next_ready, DownloadOutcome::Blocks(blocks) => {
580            assert_eq!(blocks.len(), TOTAL_BLOCKS);
581
582            // every block carries its validated access list data
583            for block in &blocks {
584                assert!(block.data().is_some());
585            }
586        });
587    }
588
589    #[tokio::test]
590    async fn block_downloader_clear_request() {
591        const TOTAL_BLOCKS: usize = 10;
592        let TestHarness { mut block_downloader, client } = TestHarness::new(TOTAL_BLOCKS);
593
594        let tip = client.highest_block().expect("there should be blocks here");
595
596        // send block range download request
597        block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(
598            tip.hash(),
599            tip.number,
600        )));
601
602        // send block set download request
603        let download_set = B256Set::from_iter([tip.hash(), tip.parent_hash]);
604        block_downloader
605            .on_action(DownloadAction::Download(DownloadRequest::block_set(download_set.clone())));
606
607        // ensure we have one in flight range request
608        assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
609
610        // ensure the range request is made correctly
611        let first_req = block_downloader.inflight_block_range_requests.first().unwrap();
612        assert_eq!(first_req.start_hash(), tip.hash());
613        assert_eq!(first_req.count(), tip.number);
614
615        // ensure we have download_set.len() in flight full block request
616        assert_eq!(block_downloader.inflight_full_block_requests.len(), download_set.len());
617
618        // send clear request
619        block_downloader.on_action(DownloadAction::Clear);
620
621        // ensure we have no in flight range request
622        assert_eq!(block_downloader.inflight_block_range_requests.len(), 0);
623
624        // ensure we have no in flight full block request
625        assert_eq!(block_downloader.inflight_full_block_requests.len(), 0);
626    }
627}