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 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 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 fn is_inflight_request(&self, hash: B256) -> bool {
202 self.inflight_full_block_requests.iter().any(|req| *req.hash() == hash)
203 }
204
205 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 fn push_pending_event(&mut self, pending_event: DownloadOutcome<B>) {
214 self.pending_events.push_back(pending_event);
215 }
216
217 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 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 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 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 self.inflight_full_block_requests.push(request);
253 }
254 }
255
256 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 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 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 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#[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
319enum FullBlockDownload<Client>
321where
322 Client: BlockClient + BlockAccessListsClient,
323{
324 Block(FetchFullBlockFuture<Client>),
326 WithAccessList(FetchFullBlockWithBalFuture<Client>),
328}
329
330impl<Client> FullBlockDownload<Client>
331where
332 Client: BlockClient + BlockAccessListsClient + 'static,
333{
334 const fn hash(&self) -> &B256 {
336 match self {
337 Self::Block(req) => req.hash(),
338 Self::WithAccessList(req) => req.hash(),
339 }
340 }
341
342 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
351enum FullBlockRangeDownload<Client>
353where
354 Client: BlockClient + BlockAccessListsClient,
355{
356 Blocks(FetchFullBlockRangeFuture<Client>),
358 WithAccessLists(FetchFullBlockRangeWithBalFuture<Client>),
360}
361
362impl<Client> FullBlockRangeDownload<Client>
363where
364 Client: BlockClient + BlockAccessListsClient + 'static,
365{
366 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 const fn count(&self) -> u64 {
376 match self {
377 Self::Blocks(req) => req.count(),
378 Self::WithAccessLists(req) => req.count(),
379 }
380 }
381
382 const fn access_lists(&self) -> bool {
384 matches!(self, Self::WithAccessLists(_))
385 }
386
387 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#[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 block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(
469 tip.hash(),
470 tip.number,
471 )));
472
473 assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
475
476 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 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 assert_eq!(blocks.len(), TOTAL_BLOCKS);
495
496 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 block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_set(
527 B256Set::from_iter([tip.hash(), tip.parent_hash]),
528 )));
529
530 assert_eq!(block_downloader.inflight_full_block_requests.len(), TOTAL_BLOCKS);
532
533 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 assert_eq!(blocks.len(), TOTAL_BLOCKS);
548
549 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 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 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 block_downloader.on_action(DownloadAction::Download(DownloadRequest::block_range(
626 tip.hash(),
627 tip.number,
628 )));
629
630 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 assert_eq!(block_downloader.inflight_block_range_requests.len(), 1);
637
638 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 assert_eq!(block_downloader.inflight_full_block_requests.len(), download_set.len());
645
646 block_downloader.on_action(DownloadAction::Clear);
648
649 assert_eq!(block_downloader.inflight_block_range_requests.len(), 0);
651
652 assert_eq!(block_downloader.inflight_full_block_requests.len(), 0);
654 }
655}