1use 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
25pub trait BlockDownloader: Send + Sync {
27 type Block: Block;
29
30 fn on_action(&mut self, action: DownloadAction);
32
33 fn poll(&mut self, cx: &mut Context<'_>) -> Poll<DownloadOutcome<Self::Block>>;
35}
36
37#[derive(Debug)]
39pub enum DownloadAction {
40 Clear,
42 Download(DownloadRequest),
44}
45
46#[derive(Debug)]
48pub enum DownloadOutcome<B: Block> {
49 Blocks(Vec<SealedBlockWithAccessList<B>>),
51 NewDownloadStarted {
53 remaining_blocks: u64,
55 target: B256,
57 },
58}
59
60#[expect(missing_debug_implementations)]
62pub struct BasicBlockDownloader<Client, B: Block>
63where
64 Client: BlockClient + BlockAccessListsClient + 'static,
65{
66 full_block_client: FullBlockClient<Client>,
68 inflight_full_block_requests: Vec<FullBlockDownload<Client>>,
70 inflight_block_range_requests: Vec<FullBlockRangeDownload<Client>>,
72 set_buffered_blocks: BinaryHeap<Reverse<OrderedDownloadedBlock<B>>>,
75 metrics: BlockDownloaderMetrics,
77 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 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 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 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 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 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 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 fn is_inflight_request(&self, hash: B256) -> bool {
194 self.inflight_full_block_requests.iter().any(|req| *req.hash() == hash)
195 }
196
197 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 fn push_pending_event(&mut self, pending_event: DownloadOutcome<B>) {
206 self.pending_events.push_back(pending_event);
207 }
208
209 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 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 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 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 self.inflight_full_block_requests.push(request);
245 }
246 }
247
248 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 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 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 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#[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
311enum FullBlockDownload<Client>
313where
314 Client: BlockClient + BlockAccessListsClient,
315{
316 Block(FetchFullBlockFuture<Client>),
318 WithAccessList(FetchFullBlockWithBalFuture<Client>),
320}
321
322impl<Client> FullBlockDownload<Client>
323where
324 Client: BlockClient + BlockAccessListsClient + 'static,
325{
326 const fn hash(&self) -> &B256 {
328 match self {
329 Self::Block(req) => req.hash(),
330 Self::WithAccessList(req) => req.hash(),
331 }
332 }
333
334 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
343enum FullBlockRangeDownload<Client>
345where
346 Client: BlockClient + BlockAccessListsClient,
347{
348 Blocks(FetchFullBlockRangeFuture<Client>),
350 WithAccessLists(FetchFullBlockRangeWithBalFuture<Client>),
352}
353
354impl<Client> FullBlockRangeDownload<Client>
355where
356 Client: BlockClient + BlockAccessListsClient + 'static,
357{
358 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 const fn count(&self) -> u64 {
368 match self {
369 Self::Blocks(req) => req.count(),
370 Self::WithAccessLists(req) => req.count(),
371 }
372 }
373
374 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#[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 block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(
456 tip.hash(),
457 tip.number,
458 )));
459
460 assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
462
463 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 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 assert_eq!(blocks.len(), TOTAL_BLOCKS);
482
483 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 block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_set(
499 B256Set::from_iter([tip.hash(), tip.parent_hash]),
500 )));
501
502 assert_eq!(block_downloader.inflight_full_block_requests.len(), TOTAL_BLOCKS);
504
505 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 assert_eq!(blocks.len(), TOTAL_BLOCKS);
520
521 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 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 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 block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(
598 tip.hash(),
599 tip.number,
600 )));
601
602 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 assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
609
610 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 assert_eq!(block_downloader.inflight_full_block_requests.len(), download_set.len());
617
618 block_downloader.on_action(DownloadAction::Clear);
620
621 assert_eq!(block_downloader.inflight_block_range_requests.len(), 0);
623
624 assert_eq!(block_downloader.inflight_full_block_requests.len(), 0);
626 }
627}