1use crate::{
4 budget::DEFAULT_BUDGET_TRY_DRAIN_DOWNLOADERS, metered_poll_nested_stream_with_budget,
5 metrics::EthRequestHandlerMetrics,
6};
7use alloy_consensus::{BlockHeader, ReceiptWithBloom};
8use alloy_eips::BlockHashOrNumber;
9use alloy_rlp::Encodable;
10use futures::StreamExt;
11use reth_eth_wire::{
12 snap::{BlockAccessListsMessage, SnapProtocolMessage},
13 BlockAccessLists, BlockBodies, BlockHeaders, Cells, EthNetworkPrimitives, GetBlockAccessLists,
14 GetBlockBodies, GetBlockHeaders, GetCells, GetNodeData, GetReceipts, GetReceipts70,
15 HeadersDirection, NetworkPrimitives, NodeData, Receipts, Receipts69, Receipts70,
16};
17use reth_network_api::test_utils::PeersHandle;
18use reth_network_p2p::{
19 error::{RequestError, RequestResult},
20 snap::client::SnapResponse,
21};
22use reth_network_peers::PeerId;
23use reth_primitives_traits::Block;
24use reth_storage_api::{BalProvider, BlockReader, GetBlockAccessListLimit, HeaderProvider};
25use reth_transaction_pool::{blobstore::NoopBlobStore, BlobStore};
26use std::{
27 future::Future,
28 pin::Pin,
29 task::{Context, Poll},
30 time::Duration,
31};
32use tokio::sync::{mpsc::Receiver, oneshot};
33use tokio_stream::wrappers::ReceiverStream;
34
35pub const MAX_RECEIPTS_SERVE: usize = 1024;
41
42pub const MAX_HEADERS_SERVE: usize = 1024;
46
47pub const MAX_BODIES_SERVE: usize = 1024;
52
53pub const MAX_BLOCK_ACCESS_LISTS_SERVE: usize = 1024;
57
58pub const MAX_CELLS_SERVE: usize = 1024;
62
63pub const SOFT_RESPONSE_LIMIT: usize = 2 * 1024 * 1024;
65
66#[derive(Debug)]
70#[must_use = "Manager does nothing unless polled."]
71pub struct EthRequestHandler<C, N: NetworkPrimitives = EthNetworkPrimitives> {
72 client: C,
74 blob_store: Box<dyn BlobStore>,
76 #[expect(dead_code)]
79 peers: PeersHandle,
80 incoming_requests: ReceiverStream<IncomingEthRequest<N>>,
82 metrics: EthRequestHandlerMetrics,
84}
85
86impl<C, N: NetworkPrimitives> EthRequestHandler<C, N> {
88 pub fn new(client: C, peers: PeersHandle, incoming: Receiver<IncomingEthRequest<N>>) -> Self {
90 Self {
91 client,
92 blob_store: Box::<NoopBlobStore>::default(),
93 peers,
94 incoming_requests: ReceiverStream::new(incoming),
95 metrics: Default::default(),
96 }
97 }
98
99 pub fn with_blob_store(mut self, blob_store: Box<dyn BlobStore>) -> Self {
101 self.blob_store = blob_store;
102 self
103 }
104}
105
106impl<C, N> EthRequestHandler<C, N>
107where
108 N: NetworkPrimitives,
109 C: BlockReader,
110{
111 fn get_headers_response(&self, request: GetBlockHeaders) -> Vec<C::Header> {
113 let GetBlockHeaders { start_block, limit, skip, direction } = request;
114
115 let mut headers = Vec::new();
116
117 let mut block: BlockHashOrNumber = match start_block {
118 BlockHashOrNumber::Hash(start) => start.into(),
119 BlockHashOrNumber::Number(num) => {
120 let Some(hash) = self.client.block_hash(num).unwrap_or_default() else {
121 return headers
122 };
123 hash.into()
124 }
125 };
126
127 let skip = skip as u64;
128 let mut total_bytes = 0;
129
130 for _ in 0..limit {
131 if let Some(header) = self.client.header_by_hash_or_number(block).unwrap_or_default() {
132 let number = header.number();
133 let parent_hash = header.parent_hash();
134
135 total_bytes += header.length();
136 headers.push(header);
137
138 if headers.len() >= MAX_HEADERS_SERVE || total_bytes > SOFT_RESPONSE_LIMIT {
139 break
140 }
141
142 match direction {
143 HeadersDirection::Rising => {
144 if let Some(next) = number.checked_add(1).and_then(|n| n.checked_add(skip))
145 {
146 block = next.into()
147 } else {
148 break
149 }
150 }
151 HeadersDirection::Falling => {
152 if skip > 0 {
153 if let Some(next) =
156 number.checked_sub(1).and_then(|num| num.checked_sub(skip))
157 {
158 block = next.into()
159 } else {
160 break
161 }
162 } else {
163 block = parent_hash.into()
164 }
165 }
166 }
167 } else {
168 break
169 }
170 }
171
172 headers
173 }
174
175 fn on_headers_request(
176 &self,
177 _peer_id: PeerId,
178 request: GetBlockHeaders,
179 response: oneshot::Sender<RequestResult<BlockHeaders<C::Header>>>,
180 ) {
181 self.metrics.eth_headers_requests_received_total.increment(1);
182 let headers = self.get_headers_response(request);
183 let _ = response.send(Ok(BlockHeaders(headers)));
184 }
185
186 fn on_bodies_request(
187 &self,
188 _peer_id: PeerId,
189 request: GetBlockBodies,
190 response: oneshot::Sender<RequestResult<BlockBodies<<C::Block as Block>::Body>>>,
191 ) {
192 self.metrics.eth_bodies_requests_received_total.increment(1);
193 let mut bodies = Vec::new();
194
195 let mut total_bytes = 0;
196
197 for hash in request {
198 if let Some(block) = self.client.block_by_hash(hash).unwrap_or_default() {
199 let body = block.into_body();
200 total_bytes += body.length();
201 bodies.push(body);
202
203 if bodies.len() >= MAX_BODIES_SERVE || total_bytes > SOFT_RESPONSE_LIMIT {
204 break
205 }
206 } else {
207 break
208 }
209 }
210
211 let _ = response.send(Ok(BlockBodies(bodies)));
212 }
213
214 fn on_receipts_request(
215 &self,
216 _peer_id: PeerId,
217 request: GetReceipts,
218 response: oneshot::Sender<RequestResult<Receipts<C::Receipt>>>,
219 ) {
220 self.metrics.eth_receipts_requests_received_total.increment(1);
221
222 let receipts = self.get_receipts_response(request, |receipts_by_block| {
223 receipts_by_block.into_iter().map(ReceiptWithBloom::from).collect::<Vec<_>>()
224 });
225
226 let _ = response.send(Ok(Receipts(receipts)));
227 }
228
229 fn on_receipts69_request(
230 &self,
231 _peer_id: PeerId,
232 request: GetReceipts,
233 response: oneshot::Sender<RequestResult<Receipts69<C::Receipt>>>,
234 ) {
235 self.metrics.eth_receipts_requests_received_total.increment(1);
236
237 let receipts = self.get_receipts_response(request, |receipts_by_block| {
238 receipts_by_block
240 });
241
242 let _ = response.send(Ok(Receipts69(receipts)));
243 }
244
245 fn on_receipts70_request(
249 &self,
250 _peer_id: PeerId,
251 request: GetReceipts70,
252 response: oneshot::Sender<RequestResult<Receipts70<C::Receipt>>>,
253 ) {
254 self.metrics.eth_receipts_requests_received_total.increment(1);
255
256 let GetReceipts70 { first_block_receipt_index, block_hashes } = request;
257
258 let mut receipts = Vec::new();
259 let mut total_bytes = 0usize;
260 let mut last_block_incomplete = false;
261
262 for (idx, hash) in block_hashes.into_iter().enumerate() {
263 if idx >= MAX_RECEIPTS_SERVE {
264 break
265 }
266
267 let Some(mut block_receipts) =
268 self.client.receipts_by_block(BlockHashOrNumber::Hash(hash)).unwrap_or_default()
269 else {
270 break
271 };
272
273 if idx == 0 && first_block_receipt_index > 0 {
274 let skip = first_block_receipt_index as usize;
275 if skip >= block_receipts.len() {
276 block_receipts.clear();
277 } else {
278 block_receipts.drain(0..skip);
279 }
280 }
281
282 let block_size = block_receipts.length();
283
284 if total_bytes + block_size <= SOFT_RESPONSE_LIMIT {
285 total_bytes += block_size;
286 receipts.push(block_receipts);
287 continue;
288 }
289
290 let mut partial_block = Vec::new();
291 for receipt in block_receipts {
292 let receipt_size = receipt.length();
293 if total_bytes + receipt_size > SOFT_RESPONSE_LIMIT {
294 break;
295 }
296 total_bytes += receipt_size;
297 partial_block.push(receipt);
298 }
299
300 receipts.push(partial_block);
301 last_block_incomplete = true;
302 break;
303 }
304
305 let _ = response.send(Ok(Receipts70 { last_block_incomplete, receipts }));
306 }
307
308 #[inline]
309 fn get_receipts_response<T, F>(&self, request: GetReceipts, transform_fn: F) -> Vec<Vec<T>>
310 where
311 F: Fn(Vec<C::Receipt>) -> Vec<T>,
312 T: Encodable,
313 {
314 let mut receipts = Vec::new();
315 let mut total_bytes = 0;
316
317 for hash in request {
318 if let Some(receipts_by_block) =
319 self.client.receipts_by_block(BlockHashOrNumber::Hash(hash)).unwrap_or_default()
320 {
321 let transformed_receipts = transform_fn(receipts_by_block);
322 total_bytes += transformed_receipts.length();
323 receipts.push(transformed_receipts);
324
325 if receipts.len() >= MAX_RECEIPTS_SERVE || total_bytes > SOFT_RESPONSE_LIMIT {
326 break
327 }
328 } else {
329 break
330 }
331 }
332
333 receipts
334 }
335
336 fn on_cells_request(
337 &self,
338 _peer_id: PeerId,
339 request: GetCells,
340 response: oneshot::Sender<RequestResult<Cells>>,
341 ) {
342 let mut cells_response = Cells { cell_mask: request.cell_mask, ..Default::default() };
343
344 for hash in request.hashes.into_iter().take(MAX_CELLS_SERVE) {
345 let Some(cells) =
346 self.blob_store.get_cells(hash, request.cell_mask).unwrap_or_default()
347 else {
348 continue;
349 };
350
351 cells_response.hashes.push(hash);
352 cells_response.cells.push(cells);
353
354 if cells_response.length() > SOFT_RESPONSE_LIMIT {
355 break
356 }
357 }
358
359 let _ = response.send(Ok(cells_response));
360 }
361}
362
363impl<C, N> EthRequestHandler<C, N>
364where
365 N: NetworkPrimitives,
366 C: BalProvider,
367{
368 fn on_block_access_lists_request(
373 &self,
374 _peer_id: PeerId,
375 mut request: GetBlockAccessLists,
376 response: oneshot::Sender<RequestResult<BlockAccessLists>>,
377 ) {
378 self.metrics.eth_block_access_lists_requests_received_total.increment(1);
379 request.0.truncate(MAX_BLOCK_ACCESS_LISTS_SERVE);
380
381 let limit = GetBlockAccessListLimit::ResponseSizeSoftLimit(SOFT_RESPONSE_LIMIT);
382 let access_lists =
383 self.client.bal_store().get_by_hashes_with_limit(&request.0, limit).unwrap_or_default();
384 let _ = response.send(Ok(BlockAccessLists(access_lists)));
385 }
386
387 fn on_snap_request(
395 &self,
396 _peer_id: PeerId,
397 request: SnapProtocolMessage,
398 response: oneshot::Sender<RequestResult<SnapResponse>>,
399 ) {
400 self.metrics.snap_requests_received_total.increment(1);
401
402 let result = match request {
403 SnapProtocolMessage::GetAccountRange(_) |
404 SnapProtocolMessage::GetStorageRanges(_) |
405 SnapProtocolMessage::GetByteCodes(_) => Err(RequestError::UnsupportedCapability),
406 SnapProtocolMessage::GetBlockAccessLists(mut req) => {
407 req.block_hashes.truncate(MAX_BLOCK_ACCESS_LISTS_SERVE);
408 let limit = GetBlockAccessListLimit::ResponseSizeSoftLimit(
409 (req.response_bytes as usize).min(SOFT_RESPONSE_LIMIT),
410 );
411 let block_access_lists = self
412 .client
413 .bal_store()
414 .get_by_hashes_with_limit(&req.block_hashes, limit)
415 .unwrap_or_default();
416 Ok(SnapResponse::BlockAccessLists(BlockAccessListsMessage {
417 request_id: req.request_id,
418 block_access_lists: BlockAccessLists(block_access_lists),
419 }))
420 }
421 _ => Err(RequestError::BadResponse),
424 };
425
426 let _ = response.send(result);
427 }
428}
429
430impl<C, N> Future for EthRequestHandler<C, N>
434where
435 N: NetworkPrimitives,
436 C: BalProvider
437 + BlockReader<Block = N::Block, Receipt = N::Receipt>
438 + HeaderProvider<Header = N::BlockHeader>
439 + Unpin,
440{
441 type Output = ();
442
443 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
444 let this = self.get_mut();
445
446 let mut acc = Duration::ZERO;
447 let maybe_more_incoming_requests = metered_poll_nested_stream_with_budget!(
448 acc,
449 "net::eth",
450 "Incoming eth requests stream",
451 DEFAULT_BUDGET_TRY_DRAIN_DOWNLOADERS,
452 this.incoming_requests.poll_next_unpin(cx),
453 |incoming| {
454 match incoming {
455 IncomingEthRequest::GetBlockHeaders { peer_id, request, response } => {
456 this.on_headers_request(peer_id, request, response)
457 }
458 IncomingEthRequest::GetBlockBodies { peer_id, request, response } => {
459 this.on_bodies_request(peer_id, request, response)
460 }
461 IncomingEthRequest::GetNodeData { .. } => {
462 this.metrics.eth_node_data_requests_received_total.increment(1);
463 }
464 IncomingEthRequest::GetReceipts { peer_id, request, response } => {
465 this.on_receipts_request(peer_id, request, response)
466 }
467 IncomingEthRequest::GetReceipts69 { peer_id, request, response } => {
468 this.on_receipts69_request(peer_id, request, response)
469 }
470 IncomingEthRequest::GetReceipts70 { peer_id, request, response } => {
471 this.on_receipts70_request(peer_id, request, response)
472 }
473 IncomingEthRequest::GetBlockAccessLists { peer_id, request, response } => {
474 this.on_block_access_lists_request(peer_id, request, response)
475 }
476 IncomingEthRequest::GetCells { peer_id, request, response } => {
477 this.on_cells_request(peer_id, request, response)
478 }
479 IncomingEthRequest::GetSnap { peer_id, request, response } => {
480 this.on_snap_request(peer_id, request, response)
481 }
482 }
483 },
484 );
485
486 this.metrics.acc_duration_poll_eth_req_handler.set(acc.as_secs_f64());
487
488 if maybe_more_incoming_requests {
490 cx.waker().wake_by_ref();
492 }
493
494 Poll::Pending
495 }
496}
497
498#[derive(Debug)]
500pub enum IncomingEthRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
501 GetBlockHeaders {
505 peer_id: PeerId,
507 request: GetBlockHeaders,
509 response: oneshot::Sender<RequestResult<BlockHeaders<N::BlockHeader>>>,
511 },
512 GetBlockBodies {
516 peer_id: PeerId,
518 request: GetBlockBodies,
520 response: oneshot::Sender<RequestResult<BlockBodies<N::BlockBody>>>,
522 },
523 GetNodeData {
527 peer_id: PeerId,
529 request: GetNodeData,
531 response: oneshot::Sender<RequestResult<NodeData>>,
533 },
534 GetReceipts {
538 peer_id: PeerId,
540 request: GetReceipts,
542 response: oneshot::Sender<RequestResult<Receipts<N::Receipt>>>,
544 },
545 GetReceipts69 {
549 peer_id: PeerId,
551 request: GetReceipts,
553 response: oneshot::Sender<RequestResult<Receipts69<N::Receipt>>>,
555 },
556 GetReceipts70 {
560 peer_id: PeerId,
562 request: GetReceipts70,
564 response: oneshot::Sender<RequestResult<Receipts70<N::Receipt>>>,
566 },
567 GetBlockAccessLists {
571 peer_id: PeerId,
573 request: GetBlockAccessLists,
575 response: oneshot::Sender<RequestResult<BlockAccessLists>>,
577 },
578 GetCells {
582 peer_id: PeerId,
584 request: GetCells,
586 response: oneshot::Sender<RequestResult<Cells>>,
588 },
589 GetSnap {
593 peer_id: PeerId,
595 request: SnapProtocolMessage,
597 response: oneshot::Sender<RequestResult<SnapResponse>>,
599 },
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use alloy_eips::{
606 eip4844::{BlobAndProofV1, BlobAndProofV2, BlobCellsAndProofsV1},
607 eip7594::{BlobTransactionSidecarVariant, Cell},
608 };
609 use alloy_primitives::{TxHash, B128, B256};
610 use reth_network_api::test_utils::PeersHandle;
611 use reth_storage_api::noop::NoopProvider;
612 use reth_transaction_pool::blobstore::{BlobStoreCleanupStat, BlobStoreError};
613 use std::sync::{
614 atomic::{AtomicUsize, Ordering},
615 Arc,
616 };
617 use tokio::sync::mpsc;
618
619 #[derive(Debug, Default)]
620 struct CountingBlobStore {
621 get_cells_calls: Arc<AtomicUsize>,
622 }
623
624 impl BlobStore for CountingBlobStore {
625 fn insert(
626 &self,
627 _tx: B256,
628 _data: BlobTransactionSidecarVariant,
629 ) -> Result<(), BlobStoreError> {
630 Ok(())
631 }
632
633 fn insert_all(
634 &self,
635 _txs: Vec<(B256, BlobTransactionSidecarVariant)>,
636 ) -> Result<(), BlobStoreError> {
637 Ok(())
638 }
639
640 fn delete(&self, _tx: B256) -> Result<(), BlobStoreError> {
641 Ok(())
642 }
643
644 fn delete_all(&self, _txs: Vec<B256>) -> Result<(), BlobStoreError> {
645 Ok(())
646 }
647
648 fn cleanup(&self) -> BlobStoreCleanupStat {
649 BlobStoreCleanupStat::default()
650 }
651
652 fn get(
653 &self,
654 _tx: B256,
655 ) -> Result<Option<Arc<BlobTransactionSidecarVariant>>, BlobStoreError> {
656 Ok(None)
657 }
658
659 fn contains(&self, _tx: B256) -> Result<bool, BlobStoreError> {
660 Ok(false)
661 }
662
663 fn get_all(
664 &self,
665 _txs: Vec<B256>,
666 ) -> Result<Vec<(B256, Arc<BlobTransactionSidecarVariant>)>, BlobStoreError> {
667 Ok(vec![])
668 }
669
670 fn get_exact(
671 &self,
672 txs: Vec<B256>,
673 ) -> Result<Vec<Arc<BlobTransactionSidecarVariant>>, BlobStoreError> {
674 if txs.is_empty() {
675 return Ok(vec![])
676 }
677
678 Err(BlobStoreError::MissingSidecar(txs[0]))
679 }
680
681 fn get_by_versioned_hashes_v1(
682 &self,
683 versioned_hashes: &[B256],
684 ) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError> {
685 Ok(vec![None; versioned_hashes.len()])
686 }
687
688 fn get_by_versioned_hashes_v2(
689 &self,
690 _versioned_hashes: &[B256],
691 ) -> Result<Option<Vec<BlobAndProofV2>>, BlobStoreError> {
692 Ok(None)
693 }
694
695 fn get_by_versioned_hashes_v3(
696 &self,
697 versioned_hashes: &[B256],
698 ) -> Result<Vec<Option<BlobAndProofV2>>, BlobStoreError> {
699 Ok(vec![None; versioned_hashes.len()])
700 }
701
702 fn get_by_versioned_hashes_v4(
703 &self,
704 versioned_hashes: &[B256],
705 _indices_bitarray: B128,
706 ) -> Result<Vec<Option<BlobCellsAndProofsV1>>, BlobStoreError> {
707 Ok(vec![None; versioned_hashes.len()])
708 }
709
710 fn has_versioned_hashes(
711 &self,
712 versioned_hashes: &[B256],
713 ) -> Result<Vec<bool>, BlobStoreError> {
714 Ok(vec![false; versioned_hashes.len()])
715 }
716
717 fn get_cells(
718 &self,
719 _tx_hash: TxHash,
720 _indices_bitarray: B128,
721 ) -> Result<Option<Vec<Cell>>, BlobStoreError> {
722 self.get_cells_calls.fetch_add(1, Ordering::Relaxed);
723 Ok(None)
724 }
725
726 fn data_size_hint(&self) -> Option<usize> {
727 Some(0)
728 }
729
730 fn blobs_len(&self) -> usize {
731 0
732 }
733 }
734
735 #[tokio::test]
736 async fn get_cells_request_limits_blob_store_lookups() {
737 let (peers_tx, _) = mpsc::unbounded_channel();
738 let (_incoming_tx, incoming_rx) = mpsc::channel(1);
739 let get_cells_calls = Arc::new(AtomicUsize::new(0));
740 let blob_store = CountingBlobStore { get_cells_calls: Arc::clone(&get_cells_calls) };
741 let handler = EthRequestHandler::<NoopProvider>::new(
742 NoopProvider::default(),
743 PeersHandle::new(peers_tx),
744 incoming_rx,
745 )
746 .with_blob_store(Box::new(blob_store));
747 let (response, rx) = oneshot::channel();
748 let request =
749 GetCells { hashes: vec![B256::ZERO; MAX_CELLS_SERVE + 1], cell_mask: B128::default() };
750
751 handler.on_cells_request(PeerId::default(), request, response);
752
753 let cells = rx.await.unwrap().unwrap();
754 assert!(cells.hashes.is_empty());
755 assert_eq!(get_cells_calls.load(Ordering::Relaxed), MAX_CELLS_SERVE);
756 }
757}