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::{constants::KECCAK_EMPTY, BlockHeader, ReceiptWithBloom};
8use alloy_eips::BlockHashOrNumber;
9use alloy_primitives::{Bytes, B256};
10use alloy_rlp::{length_of_length, Encodable};
11use futures::StreamExt;
12use reth_eth_wire::{
13    message::MAX_MESSAGE_SIZE,
14    snap::{
15        AccountData, AccountRangeMessage, BlockAccessListsMessage, ByteCodesMessage,
16        GetAccountRangeMessage, GetStorageRangesMessage, SnapProtocolMessage, StorageData,
17        StorageRangesMessage,
18    },
19    BlockAccessLists, BlockBodies, BlockHeaders, Cells, EthNetworkPrimitives, GetBlockAccessLists,
20    GetBlockBodies, GetBlockHeaders, GetCells, GetNodeData, GetReceipts, GetReceipts70,
21    HeadersDirection, NetworkPrimitives, NodeData, Receipts, Receipts69, Receipts70,
22};
23use reth_network_api::test_utils::PeersHandle;
24use reth_network_p2p::{
25    error::{RequestError, RequestResult},
26    snap::client::SnapResponse,
27};
28use reth_network_peers::PeerId;
29use reth_primitives_traits::Block;
30use reth_storage_api::{
31    errors::provider::ProviderResult, BalProvider, BlockReader, BytecodeReader,
32    GetBlockAccessListLimit, HeaderProvider, RangeEnd, RangeResponse, StateProviderFactory,
33    StateRangeProviderFactory,
34};
35use reth_transaction_pool::{blobstore::NoopBlobStore, BlobStore};
36use std::{
37    future::Future,
38    pin::Pin,
39    task::{Context, Poll},
40    time::Duration,
41};
42use tokio::sync::{mpsc::Receiver, oneshot};
43use tokio_stream::wrappers::ReceiverStream;
44
45// Limits: <https://github.com/ethereum/go-ethereum/blob/b0d44338bbcefee044f1f635a84487cbbd8f0538/eth/protocols/eth/handler.go#L34-L56>
46
47/// Maximum number of receipts to serve.
48///
49/// Used to limit lookups.
50pub const MAX_RECEIPTS_SERVE: usize = 1024;
51
52/// Maximum number of block headers to serve.
53///
54/// Used to limit lookups.
55pub const MAX_HEADERS_SERVE: usize = 1024;
56
57/// Maximum number of block headers to serve.
58///
59/// Used to limit lookups. With 24KB block sizes nowadays, the practical limit will always be
60/// `SOFT_RESPONSE_LIMIT`.
61pub const MAX_BODIES_SERVE: usize = 1024;
62
63/// Maximum number of block access lists to serve.
64///
65/// Used to limit lookups.
66pub const MAX_BLOCK_ACCESS_LISTS_SERVE: usize = 1024;
67
68/// Maximum number of cell lookups to serve.
69///
70/// Used to limit lookups.
71pub const MAX_CELLS_SERVE: usize = 1024;
72
73/// Maximum number of bytecode lookups to serve.
74///
75/// Used to limit lookups.
76pub const MAX_BYTE_CODES_SERVE: usize = 1024;
77
78/// Maximum number of storage range account lookups to serve.
79pub const MAX_STORAGE_RANGE_ACCOUNTS_SERVE: usize = 1024;
80
81/// Maximum size of replies to data retrievals: 2MB
82pub const SOFT_RESPONSE_LIMIT: usize = 2 * 1024 * 1024;
83
84/// Upper bound on the headers reserved once the first requested header resolves.
85///
86/// Header responses are bound by [`MAX_HEADERS_SERVE`] rather than by size, so a full batch reaches
87/// its final capacity within a few doublings while a batch that runs past the local tip wastes at
88/// most this many slots.
89const MAX_HEADERS_RESERVE: usize = 64;
90
91/// Upper bound on the block bodies or receipt lists reserved once the first requested block
92/// resolves.
93///
94/// These responses are bound by [`SOFT_RESPONSE_LIMIT`], so the first block's size predicts how
95/// many fit and this only caps the estimate for tiny blocks.
96const MAX_BLOCKS_RESERVE: usize = 16;
97
98/// Manages eth related requests on top of the p2p network.
99///
100/// This can be spawned to another task and is supposed to be run as background service.
101#[derive(Debug)]
102#[must_use = "Manager does nothing unless polled."]
103pub struct EthRequestHandler<C, N: NetworkPrimitives = EthNetworkPrimitives> {
104    /// The client type that can interact with the chain.
105    client: C,
106    /// Blob store used for serving blob cell requests.
107    blob_store: Box<dyn BlobStore>,
108    /// Used for reporting peers.
109    // TODO use to report spammers
110    #[expect(dead_code)]
111    peers: PeersHandle,
112    /// Incoming request from the [`NetworkManager`](crate::NetworkManager).
113    incoming_requests: ReceiverStream<IncomingEthRequest<N>>,
114    /// Metrics for the eth request handler.
115    metrics: EthRequestHandlerMetrics,
116}
117
118// === impl EthRequestHandler ===
119impl<C, N: NetworkPrimitives> EthRequestHandler<C, N> {
120    /// Create a new instance
121    pub fn new(client: C, peers: PeersHandle, incoming: Receiver<IncomingEthRequest<N>>) -> Self {
122        Self {
123            client,
124            blob_store: Box::<NoopBlobStore>::default(),
125            peers,
126            incoming_requests: ReceiverStream::new(incoming),
127            metrics: Default::default(),
128        }
129    }
130
131    /// Set blob store for the request handler
132    pub fn with_blob_store(mut self, blob_store: Box<dyn BlobStore>) -> Self {
133        self.blob_store = blob_store;
134        self
135    }
136}
137
138impl<C, N> EthRequestHandler<C, N>
139where
140    N: NetworkPrimitives,
141    C: BlockReader,
142{
143    /// Returns the list of requested headers
144    fn get_headers_response(&self, request: GetBlockHeaders) -> Vec<C::Header> {
145        let GetBlockHeaders { start_block, limit, skip, direction } = request;
146
147        let mut headers = Vec::new();
148
149        let mut block: BlockHashOrNumber = match start_block {
150            BlockHashOrNumber::Hash(start) => start.into(),
151            BlockHashOrNumber::Number(num) => {
152                let Some(hash) = self.client.block_hash(num).unwrap_or_default() else {
153                    return headers
154                };
155                hash.into()
156            }
157        };
158
159        let skip = skip as u64;
160        let mut total_bytes = 0;
161
162        for _ in 0..limit {
163            if let Some(header) = self.client.header_by_hash_or_number(block).unwrap_or_default() {
164                let number = header.number();
165                let parent_hash = header.parent_hash();
166                let len = header.length();
167
168                if headers.is_empty() {
169                    let count = limit.min(MAX_HEADERS_SERVE as u64) as usize;
170                    headers.reserve(response_reserve_hint(count, len, MAX_HEADERS_RESERVE));
171                }
172
173                total_bytes += len;
174                headers.push(header);
175
176                if headers.len() >= MAX_HEADERS_SERVE || total_bytes > SOFT_RESPONSE_LIMIT {
177                    break
178                }
179
180                match direction {
181                    HeadersDirection::Rising => {
182                        if let Some(next) = number.checked_add(1).and_then(|n| n.checked_add(skip))
183                        {
184                            block = next.into()
185                        } else {
186                            break
187                        }
188                    }
189                    HeadersDirection::Falling => {
190                        if skip > 0 {
191                            // prevent under flows for block.number == 0 and `block.number - skip <
192                            // 0`
193                            if let Some(next) =
194                                number.checked_sub(1).and_then(|num| num.checked_sub(skip))
195                            {
196                                block = next.into()
197                            } else {
198                                break
199                            }
200                        } else {
201                            block = parent_hash.into()
202                        }
203                    }
204                }
205            } else {
206                break
207            }
208        }
209
210        headers
211    }
212
213    fn on_headers_request(
214        &self,
215        _peer_id: PeerId,
216        request: GetBlockHeaders,
217        response: oneshot::Sender<RequestResult<BlockHeaders<C::Header>>>,
218    ) {
219        self.metrics.eth_headers_requests_received_total.increment(1);
220        let headers = self.get_headers_response(request);
221        let _ = response.send(Ok(BlockHeaders(headers)));
222    }
223
224    fn on_bodies_request(
225        &self,
226        _peer_id: PeerId,
227        request: GetBlockBodies,
228        response: oneshot::Sender<RequestResult<BlockBodies<<C::Block as Block>::Body>>>,
229    ) {
230        self.metrics.eth_bodies_requests_received_total.increment(1);
231        let count = request.0.len();
232        let mut bodies = Vec::new();
233
234        let mut total_bytes = 0;
235
236        for hash in request {
237            if let Some(block) = self.client.block_by_hash(hash).unwrap_or_default() {
238                let body = block.into_body();
239                let len = body.length();
240                if bodies.is_empty() {
241                    bodies.reserve(response_reserve_hint(count, len, MAX_BLOCKS_RESERVE));
242                }
243                total_bytes += len;
244                bodies.push(body);
245
246                if bodies.len() >= MAX_BODIES_SERVE || total_bytes > SOFT_RESPONSE_LIMIT {
247                    break
248                }
249            } else {
250                break
251            }
252        }
253
254        let _ = response.send(Ok(BlockBodies(bodies)));
255    }
256
257    /// Replies to `GetNodeData`.
258    ///
259    /// State serving via eth `GetNodeData` was removed; answer with an empty payload so eth/66
260    /// peers receive a response instead of hanging until the request timeout when the oneshot is
261    /// dropped unanswered.
262    fn on_node_data_request(
263        &self,
264        _peer_id: PeerId,
265        _request: GetNodeData,
266        response: oneshot::Sender<RequestResult<NodeData>>,
267    ) {
268        self.metrics.eth_node_data_requests_received_total.increment(1);
269        let _ = response.send(Ok(NodeData(vec![])));
270    }
271
272    fn on_receipts_request(
273        &self,
274        _peer_id: PeerId,
275        request: GetReceipts,
276        response: oneshot::Sender<RequestResult<Receipts<C::Receipt>>>,
277    ) {
278        self.metrics.eth_receipts_requests_received_total.increment(1);
279
280        let receipts = self.get_receipts_response(request, |receipts_by_block| {
281            receipts_by_block.into_iter().map(ReceiptWithBloom::from).collect::<Vec<_>>()
282        });
283
284        let _ = response.send(Ok(Receipts(receipts)));
285    }
286
287    fn on_receipts69_request(
288        &self,
289        _peer_id: PeerId,
290        request: GetReceipts,
291        response: oneshot::Sender<RequestResult<Receipts69<C::Receipt>>>,
292    ) {
293        self.metrics.eth_receipts_requests_received_total.increment(1);
294
295        let receipts = self.get_receipts_response(request, |receipts_by_block| {
296            // skip bloom filter for eth69
297            receipts_by_block
298        });
299
300        let _ = response.send(Ok(Receipts69(receipts)));
301    }
302
303    /// Handles partial responses for [`GetReceipts70`] queries.
304    ///
305    /// This will adhere to the soft limit but allow filling the last vec partially. A response
306    /// never ends with an empty incomplete block: if not even the next receipt fits, the response
307    /// ends after the last complete block, except when it would otherwise be empty, in which case
308    /// the first receipt is served even if it exceeds the soft limit. If it cannot fit within
309    /// [`MAX_MESSAGE_SIZE`], including the response framing, the response is empty and complete.
310    fn on_receipts70_request(
311        &self,
312        _peer_id: PeerId,
313        request: GetReceipts70,
314        response: oneshot::Sender<RequestResult<Receipts70<C::Receipt>>>,
315    ) {
316        self.metrics.eth_receipts_requests_received_total.increment(1);
317
318        let GetReceipts70 { first_block_receipt_index, block_hashes } = request;
319
320        let count = block_hashes.len();
321        let mut receipts = Vec::new();
322        let mut total_bytes = 0usize;
323        let mut last_block_incomplete = false;
324
325        // The request ID is unavailable here. Reserve the message code, the largest request ID,
326        // the incomplete flag, and the three RLP list headers to bound the full encoded response.
327        let max_first_receipt_size = MAX_MESSAGE_SIZE -
328            (1 + u64::MAX.length() + false.length() + 3 * length_of_length(MAX_MESSAGE_SIZE));
329
330        for (idx, hash) in block_hashes.into_iter().enumerate() {
331            if idx >= MAX_RECEIPTS_SERVE {
332                break
333            }
334
335            let Some(mut block_receipts) =
336                self.client.receipts_by_block(BlockHashOrNumber::Hash(hash)).unwrap_or_default()
337            else {
338                break
339            };
340
341            if idx == 0 && first_block_receipt_index > 0 {
342                let skip = first_block_receipt_index as usize;
343                if skip >= block_receipts.len() {
344                    block_receipts.clear();
345                } else {
346                    block_receipts.drain(0..skip);
347                }
348            }
349
350            let block_size = block_receipts.length();
351            if receipts.is_empty() {
352                receipts.reserve(response_reserve_hint(count, block_size, MAX_BLOCKS_RESERVE));
353            }
354
355            if total_bytes + block_size <= SOFT_RESPONSE_LIMIT {
356                total_bytes += block_size;
357                receipts.push(block_receipts);
358                continue;
359            }
360
361            // If nothing has been added to the response yet, serve the first receipt even if it
362            // exceeds the soft limit, provided the framed response fits within the hard limit.
363            let always_serve_first = receipts.is_empty();
364            let remaining = block_receipts.len();
365            let mut partial_block = Vec::new();
366            for receipt in block_receipts {
367                let receipt_size = receipt.length();
368                if total_bytes + receipt_size > SOFT_RESPONSE_LIMIT &&
369                    !(always_serve_first &&
370                        partial_block.is_empty() &&
371                        receipt_size <= max_first_receipt_size)
372                {
373                    break;
374                }
375                total_bytes += receipt_size;
376                partial_block.push(receipt);
377            }
378
379            // An empty partial block delivers nothing, so it is never sent. The requester can
380            // simply continue with this block in its next request.
381            if !partial_block.is_empty() {
382                last_block_incomplete = partial_block.len() < remaining;
383                receipts.push(partial_block);
384            }
385            break;
386        }
387
388        let _ = response.send(Ok(Receipts70 { last_block_incomplete, receipts }));
389    }
390
391    #[inline]
392    fn get_receipts_response<T, F>(&self, request: GetReceipts, transform_fn: F) -> Vec<Vec<T>>
393    where
394        F: Fn(Vec<C::Receipt>) -> Vec<T>,
395        T: Encodable,
396    {
397        let count = request.0.len();
398        let mut receipts = Vec::new();
399        let mut total_bytes = 0;
400
401        for hash in request {
402            if let Some(receipts_by_block) =
403                self.client.receipts_by_block(BlockHashOrNumber::Hash(hash)).unwrap_or_default()
404            {
405                let transformed_receipts = transform_fn(receipts_by_block);
406                let len = transformed_receipts.length();
407                if receipts.is_empty() {
408                    receipts.reserve(response_reserve_hint(count, len, MAX_BLOCKS_RESERVE));
409                }
410                total_bytes += len;
411                receipts.push(transformed_receipts);
412
413                if receipts.len() >= MAX_RECEIPTS_SERVE || total_bytes > SOFT_RESPONSE_LIMIT {
414                    break
415                }
416            } else {
417                break
418            }
419        }
420
421        receipts
422    }
423
424    fn on_cells_request(
425        &self,
426        _peer_id: PeerId,
427        request: GetCells,
428        response: oneshot::Sender<RequestResult<Cells>>,
429    ) {
430        let mut cells_response = Cells { cell_mask: request.cell_mask, ..Default::default() };
431        let mut total_bytes = 0;
432        let cell_mask = request.cell_mask();
433
434        for hash in request.hashes.into_iter().take(MAX_CELLS_SERVE) {
435            let Some(cells) = self.blob_store.get_cells(hash, cell_mask).unwrap_or_default() else {
436                continue;
437            };
438
439            total_bytes += hash.length() + cells.length();
440            cells_response.hashes.push(hash);
441            cells_response.cells.push(cells);
442
443            if total_bytes > SOFT_RESPONSE_LIMIT {
444                break
445            }
446        }
447
448        let _ = response.send(Ok(cells_response));
449    }
450}
451
452/// Number of response items to reserve once the first requested item has resolved.
453///
454/// Assumes the remaining items are about as large as the first one and reserves as many as fit into
455/// [`SOFT_RESPONSE_LIMIT`], bounded by the requested `count` and `max`.
456fn response_reserve_hint(count: usize, first_len: usize, max: usize) -> usize {
457    SOFT_RESPONSE_LIMIT.div_ceil(first_len.max(1)).min(count).min(max)
458}
459
460impl<C, N> EthRequestHandler<C, N>
461where
462    N: NetworkPrimitives,
463    C: BalProvider,
464{
465    /// Handles [`GetBlockAccessLists`] queries.
466    ///
467    /// EIP-8159 defines the final `BlockAccessLists` response semantics:
468    /// <https://eips.ethereum.org/EIPS/eip-8159>
469    fn on_block_access_lists_request(
470        &self,
471        _peer_id: PeerId,
472        mut request: GetBlockAccessLists,
473        response: oneshot::Sender<RequestResult<BlockAccessLists>>,
474    ) {
475        self.metrics.eth_block_access_lists_requests_received_total.increment(1);
476        request.0.truncate(MAX_BLOCK_ACCESS_LISTS_SERVE);
477
478        let limit = GetBlockAccessListLimit::ResponseSizeSoftLimit(SOFT_RESPONSE_LIMIT);
479        let access_lists =
480            self.client.get_bals_by_hashes_with_limit(&request.0, limit).unwrap_or_default();
481        let _ = response.send(Ok(BlockAccessLists(access_lists)));
482    }
483}
484
485impl<C, N> EthRequestHandler<C, N>
486where
487    N: NetworkPrimitives,
488    C: BalProvider + StateProviderFactory + StateRangeProviderFactory,
489{
490    /// Handles `snap/2` (EIP-8189) requests.
491    ///
492    /// `GetAccountRange`/`GetStorageRanges` are hash-native throughout and served from retained
493    /// canonical roots via [`StateRangeProviderFactory`]. `GetByteCodes` is content-addressed and
494    /// independent of any particular state root, so it's served directly.
495    /// `GetBlockAccessLists` is answered from the same [`BalProvider`] store eth71's
496    /// `GetBlockAccessLists` uses, since both serve the same underlying data.
497    fn on_snap_request(
498        &self,
499        _peer_id: PeerId,
500        request: SnapProtocolMessage,
501        response: oneshot::Sender<RequestResult<SnapResponse>>,
502    ) {
503        self.metrics.snap_requests_received_total.increment(1);
504
505        let result = match request {
506            SnapProtocolMessage::GetAccountRange(req) => {
507                let request_id = req.request_id;
508                let response = self.get_account_range_response(req).unwrap_or_else(|error| {
509                    tracing::debug!(target: "net::snap", %error, "failed to serve account range");
510                    AccountRangeMessage { request_id, accounts: Vec::new(), proof: Vec::new() }
511                });
512                Ok(SnapResponse::AccountRange(response))
513            }
514            SnapProtocolMessage::GetStorageRanges(req) => {
515                let request_id = req.request_id;
516                let response = self.get_storage_ranges_response(req).unwrap_or_else(|error| {
517                    tracing::debug!(target: "net::snap", %error, "failed to serve storage ranges");
518                    StorageRangesMessage { request_id, slots: Vec::new(), proof: Vec::new() }
519                });
520                Ok(SnapResponse::StorageRanges(response))
521            }
522            SnapProtocolMessage::GetByteCodes(req) => {
523                let codes = self
524                    .get_byte_codes_response(&req.hashes, req.response_bytes as usize)
525                    .unwrap_or_else(|error| {
526                        tracing::debug!(target: "net::snap", %error, "failed to serve bytecodes");
527                        Vec::new()
528                    });
529                Ok(SnapResponse::ByteCodes(ByteCodesMessage { request_id: req.request_id, codes }))
530            }
531            SnapProtocolMessage::GetBlockAccessLists(mut req) => {
532                req.block_hashes.truncate(MAX_BLOCK_ACCESS_LISTS_SERVE);
533                let limit = GetBlockAccessListLimit::ResponseSizeSoftLimit(
534                    (req.response_bytes as usize).min(SOFT_RESPONSE_LIMIT),
535                );
536                let block_access_lists = self
537                    .client
538                    .get_bals_by_hashes_with_limit(&req.block_hashes, limit)
539                    .unwrap_or_default();
540                Ok(SnapResponse::BlockAccessLists(BlockAccessListsMessage {
541                    request_id: req.request_id,
542                    block_access_lists: BlockAccessLists(block_access_lists),
543                }))
544            }
545            // The peer sent us a response-shaped message instead of a request; not something we
546            // asked for.
547            _ => Err(RequestError::BadResponse),
548        };
549
550        let _ = response.send(result);
551    }
552
553    /// Returns the bytecode for each of `hashes`, skipping hashes with no known code, stopping
554    /// once `response_bytes` (capped at [`SOFT_RESPONSE_LIMIT`]) is exceeded.
555    fn get_byte_codes_response(
556        &self,
557        hashes: &[B256],
558        response_bytes: usize,
559    ) -> ProviderResult<Vec<Bytes>> {
560        let state = self.client.latest()?;
561        let response_bytes = response_bytes.min(SOFT_RESPONSE_LIMIT);
562
563        let mut codes = Vec::new();
564        let mut total_bytes = 0;
565        for hash in hashes.iter().take(MAX_BYTE_CODES_SERVE) {
566            let bytes = if *hash == KECCAK_EMPTY {
567                Bytes::new()
568            } else {
569                match state.bytecode_by_hash(hash)? {
570                    Some(bytecode) => bytecode.original_bytes(),
571                    None => continue,
572                }
573            };
574            total_bytes += bytes.len();
575            codes.push(bytes);
576
577            if total_bytes > response_bytes {
578                break
579            }
580        }
581        Ok(codes)
582    }
583
584    /// Serves a `GetAccountRange` request via [`StateRangeProviderFactory`].
585    ///
586    /// Fails the request if a storage root or proof becomes unavailable after the range lookup.
587    /// Proves the boundary between `starting_hash` and the last returned account, per snap/2's
588    /// boundary-proof requirement, unless the range is a zero-origin range that exhausted the
589    /// trie, in which case no proof is needed.
590    fn get_account_range_response(
591        &self,
592        req: GetAccountRangeMessage,
593    ) -> ProviderResult<AccountRangeMessage> {
594        let empty = AccountRangeMessage {
595            request_id: req.request_id,
596            accounts: Vec::new(),
597            proof: Vec::new(),
598        };
599
600        let response_bytes = (req.response_bytes as usize).min(SOFT_RESPONSE_LIMIT);
601        let Some(state) = self.client.state_range_provider(req.root_hash)? else {
602            return Ok(empty)
603        };
604        let RangeResponse { items: accounts, end } =
605            state.account_range(req.starting_hash, req.limit_hash, response_bytes)?;
606
607        let proof = if req.starting_hash.is_zero() && end == RangeEnd::Exhausted {
608            Vec::new()
609        } else {
610            let boundary_keys = boundary_proof_keys(req.starting_hash, accounts.last());
611            state.account_range_proof(&boundary_keys)?
612        };
613
614        let mut account_data = Vec::with_capacity(accounts.len());
615        for (hash, account) in accounts {
616            let storage_root = state.storage_root_by_hash(hash)?;
617            account_data.push(AccountData::from_trie_account(
618                hash,
619                &account.into_trie_account(storage_root),
620            ));
621        }
622
623        Ok(AccountRangeMessage { request_id: req.request_id, accounts: account_data, proof })
624    }
625
626    /// Serves a `GetStorageRanges` request via [`StateRangeProviderFactory`].
627    ///
628    /// `starting_hash`/`limit_hash` apply only to the first account. An account unavailable at
629    /// this root makes the whole response empty, rather than skipping it and shifting later
630    /// accounts' positions. A proof stops the response at the first account that isn't a
631    /// complete, zero-origin range.
632    fn get_storage_ranges_response(
633        &self,
634        req: GetStorageRangesMessage,
635    ) -> ProviderResult<StorageRangesMessage> {
636        let empty = StorageRangesMessage {
637            request_id: req.request_id,
638            slots: Vec::new(),
639            proof: Vec::new(),
640        };
641        let Some(state) = self.client.state_range_provider(req.root_hash)? else {
642            return Ok(empty)
643        };
644        let mut slots: Vec<Vec<StorageData>> = Vec::new();
645        let mut proof = Vec::new();
646        let mut remaining_bytes = (req.response_bytes as usize).min(SOFT_RESPONSE_LIMIT);
647
648        for (i, &hashed_address) in
649            req.account_hashes.iter().take(MAX_STORAGE_RANGE_ACCOUNTS_SERVE).enumerate()
650        {
651            // Keeps traversing storage-empty accounts under a zero budget so a real slot still
652            // gets served; worst case is a lookup per account, bounded by the take() above.
653            if remaining_bytes == 0 && slots.iter().any(|range| !range.is_empty()) {
654                break
655            }
656            let (origin, limit) = if i == 0 {
657                (
658                    req.starting_hash.unwrap_or(B256::ZERO),
659                    req.limit_hash.unwrap_or(B256::repeat_byte(0xff)),
660                )
661            } else {
662                (B256::ZERO, B256::repeat_byte(0xff))
663            };
664            let Some(RangeResponse { items: account_slots, end }) =
665                state.storage_range(hashed_address, origin, limit, remaining_bytes)?
666            else {
667                return Ok(empty)
668            };
669
670            remaining_bytes = remaining_bytes.saturating_sub(account_slots.len() * 64);
671            let last = account_slots.last().map(|(hash, _)| *hash);
672            let needs_proof = !origin.is_zero() || end != RangeEnd::Exhausted;
673            slots.push(
674                account_slots
675                    .into_iter()
676                    // snap clients verify proofs against RLP-encoded storage trie leaves.
677                    .map(|(hash, value)| StorageData::from_value(hash, value))
678                    .collect(),
679            );
680
681            if needs_proof {
682                let boundary_keys = match last {
683                    Some(last) => vec![origin, last],
684                    None => vec![origin],
685                };
686                proof = state.storage_range_proof(hashed_address, &boundary_keys)?;
687                break
688            }
689        }
690
691        Ok(StorageRangesMessage { request_id: req.request_id, slots, proof })
692    }
693}
694
695/// Boundary-proof keys for a range reply: `origin`, plus the last returned item's key if any.
696fn boundary_proof_keys<T>(origin: B256, last: Option<&(B256, T)>) -> Vec<B256> {
697    match last {
698        Some((last, _)) => vec![origin, *last],
699        None => vec![origin],
700    }
701}
702
703/// An endless future.
704///
705/// This should be spawned or used as part of `tokio::select!`.
706impl<C, N> Future for EthRequestHandler<C, N>
707where
708    N: NetworkPrimitives,
709    C: BalProvider
710        + StateProviderFactory
711        + StateRangeProviderFactory
712        + BlockReader<Block = N::Block, Receipt = N::Receipt>
713        + HeaderProvider<Header = N::BlockHeader>
714        + Unpin,
715{
716    type Output = ();
717
718    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
719        let this = self.get_mut();
720
721        let mut acc = Duration::ZERO;
722        let maybe_more_incoming_requests = metered_poll_nested_stream_with_budget!(
723            acc,
724            "net::eth",
725            "Incoming eth requests stream",
726            DEFAULT_BUDGET_TRY_DRAIN_DOWNLOADERS,
727            this.incoming_requests.poll_next_unpin(cx),
728            |incoming| {
729                match incoming {
730                    IncomingEthRequest::GetBlockHeaders { peer_id, request, response } => {
731                        this.on_headers_request(peer_id, request, response)
732                    }
733                    IncomingEthRequest::GetBlockBodies { peer_id, request, response } => {
734                        this.on_bodies_request(peer_id, request, response)
735                    }
736                    IncomingEthRequest::GetNodeData { peer_id, request, response } => {
737                        this.on_node_data_request(peer_id, request, response)
738                    }
739                    IncomingEthRequest::GetReceipts { peer_id, request, response } => {
740                        this.on_receipts_request(peer_id, request, response)
741                    }
742                    IncomingEthRequest::GetReceipts69 { peer_id, request, response } => {
743                        this.on_receipts69_request(peer_id, request, response)
744                    }
745                    IncomingEthRequest::GetReceipts70 { peer_id, request, response } => {
746                        this.on_receipts70_request(peer_id, request, response)
747                    }
748                    IncomingEthRequest::GetBlockAccessLists { peer_id, request, response } => {
749                        this.on_block_access_lists_request(peer_id, request, response)
750                    }
751                    IncomingEthRequest::GetCells { peer_id, request, response } => {
752                        this.on_cells_request(peer_id, request, response)
753                    }
754                    IncomingEthRequest::GetSnap { peer_id, request, response } => {
755                        this.on_snap_request(peer_id, request, response)
756                    }
757                }
758            },
759        );
760
761        this.metrics.acc_duration_poll_eth_req_handler.set(acc.as_secs_f64());
762
763        // stream is fully drained and import futures pending
764        if maybe_more_incoming_requests {
765            // make sure we're woken up again
766            cx.waker().wake_by_ref();
767        }
768
769        Poll::Pending
770    }
771}
772
773/// All `eth` request related to blocks delegated by the network.
774#[derive(Debug)]
775pub enum IncomingEthRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
776    /// Request Block headers from the peer.
777    ///
778    /// The response should be sent through the channel.
779    GetBlockHeaders {
780        /// The ID of the peer to request block headers from.
781        peer_id: PeerId,
782        /// The specific block headers requested.
783        request: GetBlockHeaders,
784        /// The channel sender for the response containing block headers.
785        response: oneshot::Sender<RequestResult<BlockHeaders<N::BlockHeader>>>,
786    },
787    /// Request Block bodies from the peer.
788    ///
789    /// The response should be sent through the channel.
790    GetBlockBodies {
791        /// The ID of the peer to request block bodies from.
792        peer_id: PeerId,
793        /// The specific block bodies requested.
794        request: GetBlockBodies,
795        /// The channel sender for the response containing block bodies.
796        response: oneshot::Sender<RequestResult<BlockBodies<N::BlockBody>>>,
797    },
798    /// Request Node Data from the peer.
799    ///
800    /// The response should be sent through the channel.
801    GetNodeData {
802        /// The ID of the peer to request node data from.
803        peer_id: PeerId,
804        /// The specific node data requested.
805        request: GetNodeData,
806        /// The channel sender for the response containing node data.
807        response: oneshot::Sender<RequestResult<NodeData>>,
808    },
809    /// Request Receipts from the peer.
810    ///
811    /// The response should be sent through the channel.
812    GetReceipts {
813        /// The ID of the peer to request receipts from.
814        peer_id: PeerId,
815        /// The specific receipts requested.
816        request: GetReceipts,
817        /// The channel sender for the response containing receipts.
818        response: oneshot::Sender<RequestResult<Receipts<N::Receipt>>>,
819    },
820    /// Request Receipts from the peer without bloom filter.
821    ///
822    /// The response should be sent through the channel.
823    GetReceipts69 {
824        /// The ID of the peer to request receipts from.
825        peer_id: PeerId,
826        /// The specific receipts requested.
827        request: GetReceipts,
828        /// The channel sender for the response containing Receipts69.
829        response: oneshot::Sender<RequestResult<Receipts69<N::Receipt>>>,
830    },
831    /// Request Receipts from the peer using eth/70.
832    ///
833    /// The response should be sent through the channel.
834    GetReceipts70 {
835        /// The ID of the peer to request receipts from.
836        peer_id: PeerId,
837        /// The specific receipts requested including the `firstBlockReceiptIndex`.
838        request: GetReceipts70,
839        /// The channel sender for the response containing Receipts70.
840        response: oneshot::Sender<RequestResult<Receipts70<N::Receipt>>>,
841    },
842    /// Request Block Access Lists from the peer.
843    ///
844    /// The response should be sent through the channel.
845    GetBlockAccessLists {
846        /// The ID of the peer to request block access lists from.
847        peer_id: PeerId,
848        /// The requested block hashes.
849        request: GetBlockAccessLists,
850        /// The channel sender for the response containing block access lists.
851        response: oneshot::Sender<RequestResult<BlockAccessLists>>,
852    },
853    /// Request Cells from the peer.
854    ///
855    /// The response should be sent through the channel.
856    GetCells {
857        /// The ID of the peer to request cells from.
858        peer_id: PeerId,
859        /// The requested block hashes.
860        request: GetCells,
861        /// The channel sender for the response containing cells.
862        response: oneshot::Sender<RequestResult<Cells>>,
863    },
864    /// Request a `snap/2` message from the peer.
865    ///
866    /// The response should be sent through the channel.
867    GetSnap {
868        /// The ID of the peer to request from.
869        peer_id: PeerId,
870        /// The `snap/2` request.
871        request: SnapProtocolMessage,
872        /// The channel sender for the response.
873        response: oneshot::Sender<RequestResult<SnapResponse>>,
874    },
875}
876
877#[cfg(test)]
878mod tests {
879    use super::*;
880    use alloy_consensus::{constants::EMPTY_ROOT_HASH, Header, TxType};
881    use alloy_eips::{
882        eip4844::{BlobAndProofV1, BlobAndProofV2, BlobCellsAndProofsV1},
883        eip7594::{BlobCellMask, BlobTransactionSidecarVariant, Cell},
884    };
885    use alloy_primitives::{keccak256, Address, Log, LogData, TxHash, B128, U256};
886    use reth_eth_wire::{message::RequestPair, EthMessage, EthStreamInner, EthVersion};
887    use reth_ethereum_primitives::Receipt;
888    use reth_network_api::test_utils::PeersHandle;
889    use reth_primitives_traits::Account;
890    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
891    use reth_storage_api::noop::NoopProvider;
892    use reth_transaction_pool::blobstore::{
893        BlobStoreCleanupStat, BlobStoreError, PooledBlobSidecar,
894    };
895    use std::sync::{
896        atomic::{AtomicUsize, Ordering},
897        Arc,
898    };
899    use test_case::test_case;
900    use tokio::sync::mpsc;
901
902    #[derive(Debug, Default)]
903    struct CountingBlobStore {
904        get_cells_calls: Arc<AtomicUsize>,
905        expected_cell_mask: Option<BlobCellMask>,
906    }
907
908    impl BlobStore for CountingBlobStore {
909        fn insert(&self, _tx: B256, _data: PooledBlobSidecar) -> Result<(), BlobStoreError> {
910            Ok(())
911        }
912
913        fn insert_all(&self, _txs: Vec<(B256, PooledBlobSidecar)>) -> Result<(), BlobStoreError> {
914            Ok(())
915        }
916
917        fn delete(&self, _tx: B256) -> Result<(), BlobStoreError> {
918            Ok(())
919        }
920
921        fn delete_all(&self, _txs: Vec<B256>) -> Result<(), BlobStoreError> {
922            Ok(())
923        }
924
925        fn cleanup(&self) -> BlobStoreCleanupStat {
926            BlobStoreCleanupStat::default()
927        }
928
929        fn get(
930            &self,
931            _tx: B256,
932        ) -> Result<Option<Arc<BlobTransactionSidecarVariant>>, BlobStoreError> {
933            Ok(None)
934        }
935
936        fn contains(&self, _tx: B256) -> Result<bool, BlobStoreError> {
937            Ok(false)
938        }
939
940        fn get_all(
941            &self,
942            _txs: Vec<B256>,
943        ) -> Result<Vec<(B256, Arc<BlobTransactionSidecarVariant>)>, BlobStoreError> {
944            Ok(vec![])
945        }
946
947        fn get_exact(
948            &self,
949            txs: Vec<B256>,
950        ) -> Result<Vec<Arc<BlobTransactionSidecarVariant>>, BlobStoreError> {
951            if txs.is_empty() {
952                return Ok(vec![])
953            }
954
955            Err(BlobStoreError::MissingSidecar(txs[0]))
956        }
957
958        fn get_by_versioned_hashes_v1(
959            &self,
960            versioned_hashes: &[B256],
961        ) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError> {
962            Ok(vec![None; versioned_hashes.len()])
963        }
964
965        fn get_by_versioned_hashes_v2(
966            &self,
967            _versioned_hashes: &[B256],
968        ) -> Result<Option<Vec<BlobAndProofV2>>, BlobStoreError> {
969            Ok(None)
970        }
971
972        fn get_by_versioned_hashes_v3(
973            &self,
974            versioned_hashes: &[B256],
975        ) -> Result<Vec<Option<BlobAndProofV2>>, BlobStoreError> {
976            Ok(vec![None; versioned_hashes.len()])
977        }
978
979        fn get_by_versioned_hashes_v4(
980            &self,
981            versioned_hashes: &[B256],
982            _cell_mask: BlobCellMask,
983        ) -> Result<Vec<Option<BlobCellsAndProofsV1>>, BlobStoreError> {
984            Ok(vec![None; versioned_hashes.len()])
985        }
986
987        fn has_versioned_hashes(
988            &self,
989            versioned_hashes: &[B256],
990        ) -> Result<Vec<bool>, BlobStoreError> {
991            Ok(vec![false; versioned_hashes.len()])
992        }
993
994        fn get_cells(
995            &self,
996            _tx_hash: TxHash,
997            cell_mask: BlobCellMask,
998        ) -> Result<Option<Vec<Cell>>, BlobStoreError> {
999            self.get_cells_calls.fetch_add(1, Ordering::Relaxed);
1000            if let Some(expected) = self.expected_cell_mask {
1001                assert_eq!(cell_mask, expected);
1002                return Ok(Some(vec![Cell::default()]))
1003            }
1004            Ok(None)
1005        }
1006
1007        fn data_size_hint(&self) -> Option<usize> {
1008            Some(0)
1009        }
1010
1011        fn blobs_len(&self) -> usize {
1012            0
1013        }
1014    }
1015
1016    #[tokio::test]
1017    async fn get_cells_request_limits_blob_store_lookups() {
1018        let (peers_tx, _) = mpsc::unbounded_channel();
1019        let (_incoming_tx, incoming_rx) = mpsc::channel(1);
1020        let get_cells_calls = Arc::new(AtomicUsize::new(0));
1021        let blob_store = CountingBlobStore {
1022            get_cells_calls: Arc::clone(&get_cells_calls),
1023            ..Default::default()
1024        };
1025        let handler = EthRequestHandler::<NoopProvider>::new(
1026            NoopProvider::default(),
1027            PeersHandle::new(peers_tx),
1028            incoming_rx,
1029        )
1030        .with_blob_store(Box::new(blob_store));
1031        let (response, rx) = oneshot::channel();
1032        let request =
1033            GetCells { hashes: vec![B256::ZERO; MAX_CELLS_SERVE + 1], cell_mask: B128::default() };
1034
1035        handler.on_cells_request(PeerId::default(), request, response);
1036
1037        let cells = rx.await.unwrap().unwrap();
1038        assert!(cells.hashes.is_empty());
1039        assert_eq!(get_cells_calls.load(Ordering::Relaxed), MAX_CELLS_SERVE);
1040    }
1041
1042    #[test_case(0)]
1043    #[test_case(7)]
1044    #[test_case(8)]
1045    #[test_case(63)]
1046    #[test_case(64)]
1047    #[test_case(127)]
1048    #[tokio::test]
1049    async fn get_cells_request_uses_little_endian_wire_mask(index: u32) {
1050        use alloy_rlp::{Decodable, Encodable};
1051
1052        let (peers_tx, _) = mpsc::unbounded_channel();
1053        let (_incoming_tx, incoming_rx) = mpsc::channel(1);
1054        let numeric_mask = 1u128 << index;
1055        let blob_store = CountingBlobStore {
1056            expected_cell_mask: Some(BlobCellMask::from_bits(numeric_mask)),
1057            ..Default::default()
1058        };
1059        let handler = EthRequestHandler::<NoopProvider>::new(
1060            NoopProvider::default(),
1061            PeersHandle::new(peers_tx),
1062            incoming_rx,
1063        )
1064        .with_blob_store(Box::new(blob_store));
1065        let wire_mask = B128::from(numeric_mask.to_le_bytes());
1066        let request = GetCells { hashes: vec![B256::ZERO], cell_mask: wire_mask };
1067        let mut encoded = Vec::new();
1068        request.encode(&mut encoded);
1069        let request = GetCells::decode(&mut encoded.as_slice()).unwrap();
1070        let (response, rx) = oneshot::channel();
1071
1072        handler.on_cells_request(PeerId::default(), request, response);
1073
1074        let cells = rx.await.unwrap().unwrap();
1075        assert_eq!(cells.hashes, vec![B256::ZERO]);
1076        assert_eq!(cells.cells, vec![vec![Cell::default()]]);
1077        assert_eq!(cells.cell_mask, wire_mask);
1078        encoded.clear();
1079        cells.encode(&mut encoded);
1080        assert_eq!(Cells::decode(&mut encoded.as_slice()).unwrap(), cells);
1081    }
1082
1083    #[tokio::test]
1084    async fn get_node_data_responds_with_empty_payload() {
1085        let (peers_tx, _) = mpsc::unbounded_channel();
1086        let (_incoming_tx, incoming_rx) = mpsc::channel(1);
1087        let handler = EthRequestHandler::<NoopProvider>::new(
1088            NoopProvider::default(),
1089            PeersHandle::new(peers_tx),
1090            incoming_rx,
1091        );
1092        let (response, rx) = oneshot::channel();
1093
1094        handler.on_node_data_request(PeerId::default(), GetNodeData(vec![B256::ZERO]), response);
1095
1096        let node_data = rx.await.expect("response channel must not be dropped").unwrap();
1097        assert!(node_data.0.is_empty(), "GetNodeData should reply with empty NodeData");
1098    }
1099
1100    /// Creates a request handler backed by the mock provider for snap response tests.
1101    fn snap_handler(
1102        provider: MockEthProvider,
1103    ) -> EthRequestHandler<MockEthProvider, EthNetworkPrimitives> {
1104        let (peers_tx, _) = mpsc::unbounded_channel();
1105        let (_incoming_tx, incoming_rx) = mpsc::channel(1);
1106        EthRequestHandler::new(provider, PeersHandle::new(peers_tx), incoming_rx)
1107    }
1108
1109    #[test_case(
1110        SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
1111            request_id: 1,
1112            root_hash: B256::ZERO,
1113            starting_hash: B256::ZERO,
1114            limit_hash: B256::repeat_byte(0xff),
1115            response_bytes: SOFT_RESPONSE_LIMIT as u64,
1116        }),
1117        SnapResponse::AccountRange(AccountRangeMessage {
1118            request_id: 1,
1119            accounts: Vec::new(),
1120            proof: Vec::new(),
1121        }); "account range"
1122    )]
1123    #[test_case(
1124        SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
1125            request_id: 2,
1126            root_hash: B256::ZERO,
1127            account_hashes: vec![B256::ZERO],
1128            starting_hash: B256::ZERO.into(),
1129            limit_hash: B256::repeat_byte(0xff).into(),
1130            response_bytes: SOFT_RESPONSE_LIMIT as u64,
1131        }),
1132        SnapResponse::StorageRanges(StorageRangesMessage {
1133            request_id: 2,
1134            slots: Vec::new(),
1135            proof: Vec::new(),
1136        }); "storage ranges"
1137    )]
1138    #[test_case(
1139        SnapProtocolMessage::GetByteCodes(reth_eth_wire::snap::GetByteCodesMessage {
1140            request_id: 3,
1141            hashes: vec![B256::repeat_byte(0x11)],
1142            response_bytes: SOFT_RESPONSE_LIMIT as u64,
1143        }),
1144        SnapResponse::ByteCodes(ByteCodesMessage { request_id: 3, codes: Vec::new() }); "bytecodes"
1145    )]
1146    #[tokio::test]
1147    async fn snap_requests_return_empty_responses_on_provider_errors(
1148        request: SnapProtocolMessage,
1149        expected: SnapResponse,
1150    ) {
1151        let provider = MockEthProvider::default();
1152        provider.set_snap_state_reads_fail(true);
1153        let handler = snap_handler(provider);
1154        let (response, rx) = oneshot::channel();
1155
1156        handler.on_snap_request(PeerId::default(), request, response);
1157
1158        assert_eq!(rx.await.unwrap(), Ok(expected));
1159    }
1160
1161    #[tokio::test]
1162    async fn unavailable_snap_state_returns_empty_response() {
1163        let provider = MockEthProvider::default();
1164        let handler = snap_handler(provider.clone());
1165        let (response, rx) = oneshot::channel();
1166        handler.on_snap_request(
1167            PeerId::default(),
1168            SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
1169                request_id: 1,
1170                root_hash: B256::ZERO,
1171                starting_hash: B256::ZERO,
1172                limit_hash: B256::repeat_byte(0xff),
1173                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1174            }),
1175            response,
1176        );
1177
1178        assert_eq!(
1179            rx.await.unwrap(),
1180            Ok(SnapResponse::AccountRange(AccountRangeMessage {
1181                request_id: 1,
1182                accounts: Vec::new(),
1183                proof: Vec::new(),
1184            }))
1185        );
1186        assert_eq!(provider.snap_state_range_resolutions(), 1);
1187    }
1188
1189    #[tokio::test]
1190    async fn snap_requests_return_empty_responses_on_inconsistent_provider_results() {
1191        let missing_storage_root = MockEthProvider::default();
1192        missing_storage_root
1193            .set_snap_account_range(vec![(B256::ZERO, Account::default())], RangeEnd::Exhausted);
1194
1195        let missing_account_proof = MockEthProvider::default();
1196        missing_account_proof.set_snap_account_range(Vec::new(), RangeEnd::Exhausted);
1197
1198        let missing_storage_proof = MockEthProvider::default();
1199        missing_storage_proof
1200            .push_snap_storage_range(vec![(B256::ZERO, U256::ONE)], RangeEnd::ByteLimit);
1201
1202        let storage_disappears = MockEthProvider::default();
1203        storage_disappears.push_snap_storage_range(Vec::new(), RangeEnd::Exhausted);
1204        storage_disappears.push_unavailable_snap_storage_range();
1205
1206        let account_request = || {
1207            SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
1208                request_id: 1,
1209                root_hash: B256::ZERO,
1210                starting_hash: B256::ZERO,
1211                limit_hash: B256::repeat_byte(0xff),
1212                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1213            })
1214        };
1215        let storage_request = |account_hashes| {
1216            SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
1217                request_id: 2,
1218                root_hash: B256::ZERO,
1219                account_hashes,
1220                starting_hash: B256::ZERO.into(),
1221                limit_hash: B256::repeat_byte(0xff).into(),
1222                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1223            })
1224        };
1225        let empty_accounts = SnapResponse::AccountRange(AccountRangeMessage {
1226            request_id: 1,
1227            accounts: Vec::new(),
1228            proof: Vec::new(),
1229        });
1230        let empty_storage = SnapResponse::StorageRanges(StorageRangesMessage {
1231            request_id: 2,
1232            slots: Vec::new(),
1233            proof: Vec::new(),
1234        });
1235        let cases = [
1236            (missing_storage_root, account_request(), empty_accounts.clone()),
1237            (missing_account_proof, account_request(), empty_accounts),
1238            (missing_storage_proof, storage_request(vec![B256::ZERO]), empty_storage.clone()),
1239            (storage_disappears, storage_request(vec![B256::ZERO, B256::ZERO]), empty_storage),
1240        ];
1241
1242        for (provider, request, expected) in cases {
1243            let handler = snap_handler(provider.clone());
1244            let (response, rx) = oneshot::channel();
1245            handler.on_snap_request(PeerId::default(), request, response);
1246            assert_eq!(rx.await.unwrap(), Ok(expected));
1247            assert_eq!(provider.snap_state_range_resolutions(), 1);
1248        }
1249    }
1250
1251    #[tokio::test]
1252    #[allow(clippy::needless_update)]
1253    async fn snap_account_range_response_encodes_accounts_and_proof() {
1254        let provider = MockEthProvider::default();
1255        let first_hash = B256::repeat_byte(0x01);
1256        let second_hash = B256::repeat_byte(0x02);
1257        let storage_root = B256::repeat_byte(0x11);
1258        let code_hash = B256::repeat_byte(0x22);
1259        let proof = vec![Bytes::from_static(&[0xaa])];
1260        provider.set_snap_account_range(
1261            vec![
1262                (
1263                    first_hash,
1264                    Account {
1265                        nonce: 1,
1266                        balance: U256::from(2),
1267                        bytecode_hash: Some(code_hash),
1268                        ..Default::default()
1269                    },
1270                ),
1271                (second_hash, Account { nonce: 3, balance: U256::from(4), ..Default::default() }),
1272            ],
1273            // A hash-limit stop (not an exhausted trie) so a boundary proof is still expected,
1274            // matching the mocked `proof` below.
1275            RangeEnd::HashLimit,
1276        );
1277        provider.set_snap_storage_root(first_hash, storage_root);
1278        provider.set_snap_storage_root(second_hash, EMPTY_ROOT_HASH);
1279        provider.set_snap_account_proof(Some(proof.clone()));
1280
1281        let mut full_body = vec![0xf8, 0x44, 0x01, 0x02, 0xa0];
1282        full_body.extend_from_slice(storage_root.as_slice());
1283        full_body.push(0xa0);
1284        full_body.extend_from_slice(code_hash.as_slice());
1285        let empty_body = Bytes::from_static(&[0xc4, 0x03, 0x04, 0x80, 0x80]);
1286
1287        let handler = snap_handler(provider.clone());
1288        let (response, rx) = oneshot::channel();
1289        handler.on_snap_request(
1290            PeerId::default(),
1291            SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
1292                request_id: 1,
1293                root_hash: B256::ZERO,
1294                starting_hash: B256::ZERO,
1295                limit_hash: B256::repeat_byte(0xff),
1296                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1297            }),
1298            response,
1299        );
1300
1301        assert_eq!(
1302            rx.await.unwrap(),
1303            Ok(SnapResponse::AccountRange(AccountRangeMessage {
1304                request_id: 1,
1305                accounts: vec![
1306                    AccountData {
1307                        hash: first_hash,
1308                        body: alloy_rlp::decode_exact(&full_body).unwrap()
1309                    },
1310                    AccountData {
1311                        hash: second_hash,
1312                        body: alloy_rlp::decode_exact(&empty_body).unwrap()
1313                    },
1314                ],
1315                proof,
1316            }))
1317        );
1318        assert_eq!(provider.snap_state_range_resolutions(), 1);
1319    }
1320
1321    #[tokio::test]
1322    async fn snap_account_range_response_skips_proof_for_exhausted_zero_origin_range() {
1323        let provider = MockEthProvider::default();
1324        let hash = B256::repeat_byte(0x01);
1325        provider.set_snap_account_range(
1326            vec![(hash, Account { nonce: 1, balance: U256::from(2), ..Default::default() })],
1327            RangeEnd::Exhausted,
1328        );
1329        provider.set_snap_storage_root(hash, EMPTY_ROOT_HASH);
1330        // A non-empty mocked proof: if the skip-proof optimization regressed to "never skip",
1331        // this wrongly-included proof would surface in the response and fail the assertion below.
1332        provider.set_snap_account_proof(Some(vec![Bytes::from_static(&[0xaa])]));
1333
1334        let handler = snap_handler(provider.clone());
1335        let (response, rx) = oneshot::channel();
1336        handler.on_snap_request(
1337            PeerId::default(),
1338            SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
1339                request_id: 1,
1340                root_hash: B256::ZERO,
1341                starting_hash: B256::ZERO,
1342                limit_hash: B256::repeat_byte(0xff),
1343                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1344            }),
1345            response,
1346        );
1347
1348        let Ok(SnapResponse::AccountRange(AccountRangeMessage { accounts, proof, .. })) =
1349            rx.await.unwrap()
1350        else {
1351            panic!("expected an account range response");
1352        };
1353        assert_eq!(accounts.len(), 1);
1354        assert!(proof.is_empty(), "a zero-origin, exhausted range must not carry a boundary proof");
1355    }
1356
1357    #[tokio::test]
1358    async fn snap_storage_range_response_encodes_values_and_proof() {
1359        let provider = MockEthProvider::default();
1360        let first_hash = B256::repeat_byte(0x01);
1361        let second_hash = B256::repeat_byte(0x02);
1362        let origin = B256::repeat_byte(0x10);
1363        let proof = vec![Bytes::from_static(&[0xbb])];
1364        provider.push_snap_storage_range(
1365            vec![(first_hash, U256::from(0x0102)), (second_hash, U256::from(0xff))],
1366            RangeEnd::Exhausted,
1367        );
1368        provider.set_snap_storage_proof(Some(proof.clone()));
1369
1370        let handler = snap_handler(provider.clone());
1371        let (response, rx) = oneshot::channel();
1372        handler.on_snap_request(
1373            PeerId::default(),
1374            SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
1375                request_id: 2,
1376                root_hash: B256::ZERO,
1377                account_hashes: vec![B256::repeat_byte(0x03)],
1378                starting_hash: origin.into(),
1379                limit_hash: B256::repeat_byte(0xff).into(),
1380                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1381            }),
1382            response,
1383        );
1384
1385        assert_eq!(
1386            rx.await.unwrap(),
1387            Ok(SnapResponse::StorageRanges(StorageRangesMessage {
1388                request_id: 2,
1389                slots: vec![vec![
1390                    StorageData { hash: first_hash, data: Bytes::from_static(&[0x82, 0x01, 0x02]) },
1391                    StorageData { hash: second_hash, data: Bytes::from_static(&[0x81, 0xff]) },
1392                ]],
1393                proof,
1394            }))
1395        );
1396        assert_eq!(provider.snap_storage_range_requests()[0].1, origin);
1397        assert_eq!(provider.snap_state_range_resolutions(), 1);
1398    }
1399
1400    #[tokio::test]
1401    async fn snap_storage_ranges_only_bound_the_first_account() {
1402        let provider = MockEthProvider::default();
1403        provider.push_snap_storage_range(Vec::new(), RangeEnd::Exhausted);
1404        provider.push_snap_storage_range(Vec::new(), RangeEnd::Exhausted);
1405        let first_account = B256::repeat_byte(0x01);
1406        let second_account = B256::repeat_byte(0x02);
1407        let origin = B256::ZERO;
1408        let limit = B256::repeat_byte(0x22);
1409
1410        let handler = snap_handler(provider.clone());
1411        let (response, rx) = oneshot::channel();
1412        handler.on_snap_request(
1413            PeerId::default(),
1414            SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
1415                request_id: 3,
1416                root_hash: B256::ZERO,
1417                account_hashes: vec![first_account, second_account],
1418                starting_hash: origin.into(),
1419                limit_hash: limit.into(),
1420                response_bytes: 1_000,
1421            }),
1422            response,
1423        );
1424
1425        assert_eq!(
1426            rx.await.unwrap(),
1427            Ok(SnapResponse::StorageRanges(StorageRangesMessage {
1428                request_id: 3,
1429                slots: vec![Vec::new(), Vec::new()],
1430                proof: Vec::new(),
1431            }))
1432        );
1433        assert_eq!(
1434            provider.snap_storage_range_requests(),
1435            vec![
1436                (first_account, origin, limit, 1_000),
1437                (second_account, B256::ZERO, B256::repeat_byte(0xff), 1_000),
1438            ]
1439        );
1440        assert_eq!(provider.snap_state_range_resolutions(), 1);
1441    }
1442
1443    #[tokio::test]
1444    async fn snap_byte_codes_response_preserves_found_code_order() {
1445        let provider = MockEthProvider::default();
1446        let code = Bytes::from_static(&[0x60, 0x00]);
1447        let code_hash = keccak256(&code);
1448        let later_code = Bytes::from_static(&[0x60, 0x01]);
1449        let later_code_hash = keccak256(&later_code);
1450        provider.add_account(
1451            Address::repeat_byte(0x01),
1452            ExtendedAccount::new(1, U256::ZERO).with_bytecode(code.clone()),
1453        );
1454        provider.add_account(
1455            Address::repeat_byte(0x02),
1456            ExtendedAccount::new(1, U256::ZERO).with_bytecode(later_code),
1457        );
1458
1459        let handler = snap_handler(provider);
1460        let (response, rx) = oneshot::channel();
1461        handler.on_snap_request(
1462            PeerId::default(),
1463            SnapProtocolMessage::GetByteCodes(reth_eth_wire::snap::GetByteCodesMessage {
1464                request_id: 3,
1465                hashes: vec![KECCAK_EMPTY, B256::repeat_byte(0xff), code_hash, later_code_hash],
1466                response_bytes: 1,
1467            }),
1468            response,
1469        );
1470
1471        assert_eq!(
1472            rx.await.unwrap(),
1473            Ok(SnapResponse::ByteCodes(ByteCodesMessage {
1474                request_id: 3,
1475                codes: vec![Bytes::new(), code],
1476            }))
1477        );
1478    }
1479
1480    #[tokio::test]
1481    async fn snap_storage_ranges_limit_account_lookups() {
1482        let provider = MockEthProvider::default();
1483        for _ in 0..=MAX_STORAGE_RANGE_ACCOUNTS_SERVE {
1484            provider.push_snap_storage_range(Vec::new(), RangeEnd::Exhausted);
1485        }
1486        let handler = snap_handler(provider.clone());
1487        let (response, rx) = oneshot::channel();
1488        handler.on_snap_request(
1489            PeerId::default(),
1490            SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
1491                request_id: 4,
1492                root_hash: B256::ZERO,
1493                account_hashes: vec![B256::ZERO; MAX_STORAGE_RANGE_ACCOUNTS_SERVE + 1],
1494                starting_hash: B256::ZERO.into(),
1495                limit_hash: B256::repeat_byte(0xff).into(),
1496                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1497            }),
1498            response,
1499        );
1500
1501        assert_eq!(
1502            rx.await.unwrap(),
1503            Ok(SnapResponse::StorageRanges(StorageRangesMessage {
1504                request_id: 4,
1505                slots: vec![Vec::new(); MAX_STORAGE_RANGE_ACCOUNTS_SERVE],
1506                proof: Vec::new(),
1507            }))
1508        );
1509        assert_eq!(provider.snap_storage_ranges_remaining(), 1);
1510        assert_eq!(provider.snap_state_range_resolutions(), 1);
1511    }
1512
1513    #[tokio::test]
1514    async fn snap_storage_range_proves_finite_limit_from_zero_origin() {
1515        let provider = MockEthProvider::default();
1516        let hash = B256::repeat_byte(0x01);
1517        let proof = vec![Bytes::from_static(&[0xcc])];
1518        // More entries exist beyond `limit_hash`, so the cursor stopped at the hash limit
1519        // rather than exhausting the trie -- a proof is required even though origin is zero.
1520        provider.push_snap_storage_range(vec![(hash, U256::ONE)], RangeEnd::HashLimit);
1521        provider.set_snap_storage_proof(Some(proof.clone()));
1522
1523        let handler = snap_handler(provider.clone());
1524        let (response, rx) = oneshot::channel();
1525        handler.on_snap_request(
1526            PeerId::default(),
1527            SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
1528                request_id: 5,
1529                root_hash: B256::ZERO,
1530                account_hashes: vec![B256::repeat_byte(0x03)],
1531                starting_hash: B256::ZERO.into(),
1532                limit_hash: B256::repeat_byte(0x20).into(),
1533                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1534            }),
1535            response,
1536        );
1537
1538        assert_eq!(
1539            rx.await.unwrap(),
1540            Ok(SnapResponse::StorageRanges(StorageRangesMessage {
1541                request_id: 5,
1542                slots: vec![vec![StorageData { hash, data: Bytes::from_static(&[0x01]) }]],
1543                proof,
1544            }))
1545        );
1546    }
1547
1548    #[tokio::test]
1549    async fn snap_storage_ranges_are_entirely_empty_when_an_account_is_missing() {
1550        let provider = MockEthProvider::default();
1551        provider.push_missing_snap_storage_account();
1552        provider.push_snap_storage_range(
1553            vec![(B256::repeat_byte(0x01), U256::ONE)],
1554            RangeEnd::Exhausted,
1555        );
1556        let missing_account = B256::repeat_byte(0x01);
1557        let valid_account = B256::repeat_byte(0x02);
1558
1559        let handler = snap_handler(provider.clone());
1560        let (response, rx) = oneshot::channel();
1561        handler.on_snap_request(
1562            PeerId::default(),
1563            SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
1564                request_id: 6,
1565                root_hash: B256::ZERO,
1566                account_hashes: vec![missing_account, valid_account],
1567                starting_hash: B256::ZERO.into(),
1568                limit_hash: B256::repeat_byte(0xff).into(),
1569                response_bytes: SOFT_RESPONSE_LIMIT as u64,
1570            }),
1571            response,
1572        );
1573
1574        assert_eq!(
1575            rx.await.unwrap(),
1576            Ok(SnapResponse::StorageRanges(StorageRangesMessage {
1577                request_id: 6,
1578                slots: Vec::new(),
1579                proof: Vec::new(),
1580            }))
1581        );
1582        // The valid account's queued range is never consumed: the response bails out at the
1583        // first missing account instead of skipping it and shifting later positions.
1584        assert_eq!(provider.snap_storage_ranges_remaining(), 1);
1585    }
1586
1587    /// Creates a receipt whose single log carries `data_len` bytes of data.
1588    fn receipt_with_data(data_len: usize) -> Receipt {
1589        Receipt {
1590            tx_type: TxType::Legacy,
1591            success: true,
1592            cumulative_gas_used: 21_000,
1593            logs: vec![Log {
1594                address: Address::ZERO,
1595                data: LogData::new_unchecked(Vec::new(), vec![0u8; data_len].into()),
1596            }],
1597        }
1598    }
1599
1600    /// Stores the receipts as block `number` and returns the block hash.
1601    fn insert_receipts_block(
1602        provider: &MockEthProvider,
1603        number: u64,
1604        receipts: Vec<Receipt>,
1605    ) -> B256 {
1606        let hash = B256::with_last_byte(number as u8);
1607        provider.add_header(hash, Header { number, ..Default::default() });
1608        provider.add_receipts(number, receipts);
1609        hash
1610    }
1611
1612    /// Sends a [`GetReceipts70`] request to the handler and returns the response.
1613    async fn get_receipts70(
1614        provider: MockEthProvider,
1615        first_block_receipt_index: u64,
1616        block_hashes: Vec<B256>,
1617    ) -> Receipts70 {
1618        let handler = snap_handler(provider);
1619        let (response, rx) = oneshot::channel();
1620        handler.on_receipts70_request(
1621            PeerId::default(),
1622            GetReceipts70 { first_block_receipt_index, block_hashes },
1623            response,
1624        );
1625        rx.await.unwrap().unwrap()
1626    }
1627
1628    #[tokio::test]
1629    async fn receipts70_serves_only_oversized_first_receipt_of_partial_block() {
1630        let provider = MockEthProvider::default();
1631        let oversized = receipt_with_data(SOFT_RESPONSE_LIMIT + 1024);
1632        let hash =
1633            insert_receipts_block(&provider, 1, vec![oversized.clone(), receipt_with_data(16)]);
1634
1635        let resp = get_receipts70(provider, 0, vec![hash]).await;
1636
1637        // The oversized receipt exhausts the budget, so the rest of the block is left out.
1638        assert_eq!(
1639            resp,
1640            Receipts70 { last_block_incomplete: true, receipts: vec![vec![oversized]] }
1641        );
1642    }
1643
1644    #[tokio::test]
1645    async fn receipts70_omits_empty_incomplete_block() {
1646        let provider = MockEthProvider::default();
1647        let first = vec![receipt_with_data(SOFT_RESPONSE_LIMIT - 1024)];
1648        let first_hash = insert_receipts_block(&provider, 1, first.clone());
1649        // The first receipt of the second block does not fit into the remaining budget.
1650        let second_hash = insert_receipts_block(&provider, 2, vec![receipt_with_data(4096)]);
1651
1652        let resp = get_receipts70(provider, 0, vec![first_hash, second_hash]).await;
1653
1654        assert_eq!(resp, Receipts70 { last_block_incomplete: false, receipts: vec![first] });
1655    }
1656
1657    #[tokio::test]
1658    async fn receipts70_serves_partial_block() {
1659        let provider = MockEthProvider::default();
1660        let receipts = vec![receipt_with_data(16 * 1024); 200];
1661        let hash = insert_receipts_block(&provider, 1, receipts.clone());
1662
1663        let resp = get_receipts70(provider, 0, vec![hash]).await;
1664
1665        assert!(resp.last_block_incomplete);
1666        assert_eq!(resp.receipts.len(), 1);
1667        let partial = &resp.receipts[0];
1668        assert!(!partial.is_empty() && partial.len() < receipts.len());
1669        assert_eq!(partial.as_slice(), &receipts[..partial.len()]);
1670        assert!(partial.iter().map(Encodable::length).sum::<usize>() <= SOFT_RESPONSE_LIMIT);
1671    }
1672
1673    #[test_case(0, 0; "first receipt at hard limit")]
1674    #[test_case(1, 0; "continuation receipt at hard limit")]
1675    #[test_case(0, 1; "first receipt framing exceeds hard limit")]
1676    #[test_case(1, 1; "continuation receipt framing exceeds hard limit")]
1677    #[tokio::test]
1678    async fn receipts70_enforces_framed_response_limit(first_receipt_index: u64, excess: usize) {
1679        // Measure the actual wire encoding with the largest request ID. At these sizes, changing
1680        // the log data length does not change the width of any RLP length prefix.
1681        let data_len = MAX_MESSAGE_SIZE - 1024;
1682        let encoded = encode_receipts70_response(Receipts70 {
1683            last_block_incomplete: false,
1684            receipts: vec![vec![receipt_with_data(data_len)]],
1685        });
1686        let receipt = receipt_with_data(data_len + MAX_MESSAGE_SIZE - encoded.len() + excess);
1687        assert!(receipt.length() < MAX_MESSAGE_SIZE);
1688        let expected =
1689            Receipts70 { last_block_incomplete: false, receipts: vec![vec![receipt.clone()]] };
1690        assert_eq!(encode_receipts70_response(expected.clone()).len(), MAX_MESSAGE_SIZE + excess);
1691
1692        let provider = MockEthProvider::default();
1693        let mut receipts = vec![receipt_with_data(16); first_receipt_index as usize];
1694        receipts.push(receipt);
1695        let hash = insert_receipts_block(&provider, 1, receipts);
1696
1697        let resp = get_receipts70(provider, first_receipt_index, vec![hash]).await;
1698
1699        if excess == 0 {
1700            assert_eq!(resp, expected);
1701        } else {
1702            assert_eq!(resp, Receipts70 { last_block_incomplete: false, receipts: Vec::new() });
1703        }
1704        let encoded = encode_receipts70_response(resp);
1705        assert!(encoded.len() <= MAX_MESSAGE_SIZE);
1706        EthStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth70)
1707            .decode_message(encoded.as_ref().into())
1708            .unwrap();
1709    }
1710
1711    /// Encodes a receipts response with the largest possible request ID.
1712    fn encode_receipts70_response(response: Receipts70) -> Bytes {
1713        EthStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth70)
1714            .encode_message(EthMessage::Receipts70(RequestPair {
1715                request_id: u64::MAX,
1716                message: response,
1717            }))
1718            .unwrap()
1719            .into()
1720    }
1721}