Skip to main content

reth_network/
eth_requests.rs

1//! Blocks/Headers management for the p2p network.
2
3use 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
35// Limits: <https://github.com/ethereum/go-ethereum/blob/b0d44338bbcefee044f1f635a84487cbbd8f0538/eth/protocols/eth/handler.go#L34-L56>
36
37/// Maximum number of receipts to serve.
38///
39/// Used to limit lookups.
40pub const MAX_RECEIPTS_SERVE: usize = 1024;
41
42/// Maximum number of block headers to serve.
43///
44/// Used to limit lookups.
45pub const MAX_HEADERS_SERVE: usize = 1024;
46
47/// Maximum number of block headers to serve.
48///
49/// Used to limit lookups. With 24KB block sizes nowadays, the practical limit will always be
50/// `SOFT_RESPONSE_LIMIT`.
51pub const MAX_BODIES_SERVE: usize = 1024;
52
53/// Maximum number of block access lists to serve.
54///
55/// Used to limit lookups.
56pub const MAX_BLOCK_ACCESS_LISTS_SERVE: usize = 1024;
57
58/// Maximum number of cell lookups to serve.
59///
60/// Used to limit lookups.
61pub const MAX_CELLS_SERVE: usize = 1024;
62
63/// Maximum size of replies to data retrievals: 2MB
64pub const SOFT_RESPONSE_LIMIT: usize = 2 * 1024 * 1024;
65
66/// Manages eth related requests on top of the p2p network.
67///
68/// This can be spawned to another task and is supposed to be run as background service.
69#[derive(Debug)]
70#[must_use = "Manager does nothing unless polled."]
71pub struct EthRequestHandler<C, N: NetworkPrimitives = EthNetworkPrimitives> {
72    /// The client type that can interact with the chain.
73    client: C,
74    /// Blob store used for serving blob cell requests.
75    blob_store: Box<dyn BlobStore>,
76    /// Used for reporting peers.
77    // TODO use to report spammers
78    #[expect(dead_code)]
79    peers: PeersHandle,
80    /// Incoming request from the [`NetworkManager`](crate::NetworkManager).
81    incoming_requests: ReceiverStream<IncomingEthRequest<N>>,
82    /// Metrics for the eth request handler.
83    metrics: EthRequestHandlerMetrics,
84}
85
86// === impl EthRequestHandler ===
87impl<C, N: NetworkPrimitives> EthRequestHandler<C, N> {
88    /// Create a new instance
89    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    /// Set blob store for the request handler
100    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    /// Returns the list of requested headers
112    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                            // prevent under flows for block.number == 0 and `block.number - skip <
154                            // 0`
155                            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            // skip bloom filter for eth69
239            receipts_by_block
240        });
241
242        let _ = response.send(Ok(Receipts69(receipts)));
243    }
244
245    /// Handles partial responses for [`GetReceipts70`] queries.
246    ///
247    /// This will adhere to the soft limit but allow filling the last vec partially.
248    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    /// Handles [`GetBlockAccessLists`] queries.
369    ///
370    /// EIP-8159 defines the final `BlockAccessLists` response semantics:
371    /// <https://eips.ethereum.org/EIPS/eip-8159>
372    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    /// Handles `snap/2` (EIP-8189) requests.
388    ///
389    /// `GetAccountRange`/`GetStorageRanges`/`GetByteCodes` stay unsupported until a real
390    /// state-trie-backed store exists; an empty response would falsely claim served data.
391    /// `GetBlockAccessLists` is answered from the same [`BalProvider`] store eth71's
392    /// `GetBlockAccessLists` uses, since both serve the same
393    /// underlying data.
394    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            // The peer sent us a response-shaped message instead of a request; not something we
422            // asked for.
423            _ => Err(RequestError::BadResponse),
424        };
425
426        let _ = response.send(result);
427    }
428}
429
430/// An endless future.
431///
432/// This should be spawned or used as part of `tokio::select!`.
433impl<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        // stream is fully drained and import futures pending
489        if maybe_more_incoming_requests {
490            // make sure we're woken up again
491            cx.waker().wake_by_ref();
492        }
493
494        Poll::Pending
495    }
496}
497
498/// All `eth` request related to blocks delegated by the network.
499#[derive(Debug)]
500pub enum IncomingEthRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
501    /// Request Block headers from the peer.
502    ///
503    /// The response should be sent through the channel.
504    GetBlockHeaders {
505        /// The ID of the peer to request block headers from.
506        peer_id: PeerId,
507        /// The specific block headers requested.
508        request: GetBlockHeaders,
509        /// The channel sender for the response containing block headers.
510        response: oneshot::Sender<RequestResult<BlockHeaders<N::BlockHeader>>>,
511    },
512    /// Request Block bodies from the peer.
513    ///
514    /// The response should be sent through the channel.
515    GetBlockBodies {
516        /// The ID of the peer to request block bodies from.
517        peer_id: PeerId,
518        /// The specific block bodies requested.
519        request: GetBlockBodies,
520        /// The channel sender for the response containing block bodies.
521        response: oneshot::Sender<RequestResult<BlockBodies<N::BlockBody>>>,
522    },
523    /// Request Node Data from the peer.
524    ///
525    /// The response should be sent through the channel.
526    GetNodeData {
527        /// The ID of the peer to request node data from.
528        peer_id: PeerId,
529        /// The specific node data requested.
530        request: GetNodeData,
531        /// The channel sender for the response containing node data.
532        response: oneshot::Sender<RequestResult<NodeData>>,
533    },
534    /// Request Receipts from the peer.
535    ///
536    /// The response should be sent through the channel.
537    GetReceipts {
538        /// The ID of the peer to request receipts from.
539        peer_id: PeerId,
540        /// The specific receipts requested.
541        request: GetReceipts,
542        /// The channel sender for the response containing receipts.
543        response: oneshot::Sender<RequestResult<Receipts<N::Receipt>>>,
544    },
545    /// Request Receipts from the peer without bloom filter.
546    ///
547    /// The response should be sent through the channel.
548    GetReceipts69 {
549        /// The ID of the peer to request receipts from.
550        peer_id: PeerId,
551        /// The specific receipts requested.
552        request: GetReceipts,
553        /// The channel sender for the response containing Receipts69.
554        response: oneshot::Sender<RequestResult<Receipts69<N::Receipt>>>,
555    },
556    /// Request Receipts from the peer using eth/70.
557    ///
558    /// The response should be sent through the channel.
559    GetReceipts70 {
560        /// The ID of the peer to request receipts from.
561        peer_id: PeerId,
562        /// The specific receipts requested including the `firstBlockReceiptIndex`.
563        request: GetReceipts70,
564        /// The channel sender for the response containing Receipts70.
565        response: oneshot::Sender<RequestResult<Receipts70<N::Receipt>>>,
566    },
567    /// Request Block Access Lists from the peer.
568    ///
569    /// The response should be sent through the channel.
570    GetBlockAccessLists {
571        /// The ID of the peer to request block access lists from.
572        peer_id: PeerId,
573        /// The requested block hashes.
574        request: GetBlockAccessLists,
575        /// The channel sender for the response containing block access lists.
576        response: oneshot::Sender<RequestResult<BlockAccessLists>>,
577    },
578    /// Request Cells from the peer.
579    ///
580    /// The response should be sent through the channel.
581    GetCells {
582        /// The ID of the peer to request cells from.
583        peer_id: PeerId,
584        /// The requested block hashes.
585        request: GetCells,
586        /// The channel sender for the response containing cells.
587        response: oneshot::Sender<RequestResult<Cells>>,
588    },
589    /// Request a `snap/2` message from the peer.
590    ///
591    /// The response should be sent through the channel.
592    GetSnap {
593        /// The ID of the peer to request from.
594        peer_id: PeerId,
595        /// The `snap/2` request.
596        request: SnapProtocolMessage,
597        /// The channel sender for the response.
598        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}