Skip to main content

reth_network/fetch/
mod.rs

1//! Fetch data from the network.
2
3mod client;
4
5pub use client::FetchClient;
6
7use crate::{message::BlockRequest, session::BlockRangeInfo};
8use alloy_primitives::B256;
9use futures::StreamExt;
10use reth_eth_wire::{
11    snap::SnapProtocolMessage, BlockAccessLists, Capabilities, EthNetworkPrimitives, EthVersion,
12    GetBlockAccessLists, GetBlockBodies, GetBlockHeaders, GetReceipts, NetworkPrimitives,
13};
14use reth_network_api::test_utils::PeersHandle;
15use reth_network_p2p::{
16    block_access_lists::client::BalRequirement,
17    error::{EthResponseValidator, PeerRequestResult, RequestError, RequestResult},
18    headers::client::HeadersRequest,
19    priority::Priority,
20    receipts::client::ReceiptsResponse,
21    snap::client::SnapResponse,
22};
23use reth_network_peers::PeerId;
24use reth_network_types::ReputationChangeKind;
25use std::{
26    collections::{HashMap, VecDeque},
27    ops::RangeInclusive,
28    sync::{
29        atomic::{AtomicU64, AtomicUsize, Ordering},
30        Arc,
31    },
32    task::{Context, Poll},
33};
34use tokio::sync::{mpsc, mpsc::UnboundedSender, oneshot};
35use tokio_stream::wrappers::UnboundedReceiverStream;
36
37type InflightHeadersRequest<H> = Request<HeadersRequest, PeerRequestResult<Vec<H>>>;
38type InflightBodiesRequest<B> = Request<(), PeerRequestResult<Vec<B>>>;
39type InflightReceiptsRequest<R> = Request<(), PeerRequestResult<ReceiptsResponse<R>>>;
40type InflightBlockAccessListsRequest = Request<(), PeerRequestResult<BlockAccessLists>>;
41type InflightSnapRequest = Request<(), PeerRequestResult<SnapResponse>>;
42
43/// Manages data fetching operations.
44///
45/// This type is hooked into the staged sync pipeline and delegates download request to available
46/// peers and sends the response once ready.
47///
48/// This type maintains a list of connected peers that are available for requests.
49#[derive(Debug)]
50pub struct StateFetcher<N: NetworkPrimitives = EthNetworkPrimitives> {
51    /// Currently active [`GetBlockHeaders`] requests
52    inflight_headers_requests: HashMap<PeerId, InflightHeadersRequest<N::BlockHeader>>,
53    /// Currently active [`GetBlockBodies`] requests
54    inflight_bodies_requests: HashMap<PeerId, InflightBodiesRequest<N::BlockBody>>,
55    /// Currently active [`GetBlockAccessLists`] requests
56    inflight_bals_requests: HashMap<PeerId, InflightBlockAccessListsRequest>,
57    /// Currently active `GetReceipts` requests
58    inflight_receipts_requests: HashMap<PeerId, InflightReceiptsRequest<N::Receipt>>,
59    /// Currently active `snap/2` requests
60    inflight_snap_requests: HashMap<PeerId, InflightSnapRequest>,
61    /// The list of _available_ peers for requests.
62    peers: HashMap<PeerId, Peer>,
63    /// The handle to the peers manager
64    peers_handle: PeersHandle,
65    /// Number of active peer sessions the node's currently handling.
66    num_active_peers: Arc<AtomicUsize>,
67    /// Requests queued for processing
68    queued_requests: VecDeque<DownloadRequest<N>>,
69    /// Receiver for new incoming download requests
70    download_requests_rx: UnboundedReceiverStream<DownloadRequest<N>>,
71    /// Sender for download requests, used to detach a [`FetchClient`]
72    download_requests_tx: UnboundedSender<DownloadRequest<N>>,
73}
74
75// === impl StateSyncer ===
76
77impl<N: NetworkPrimitives> StateFetcher<N> {
78    pub(crate) fn new(peers_handle: PeersHandle, num_active_peers: Arc<AtomicUsize>) -> Self {
79        let (download_requests_tx, download_requests_rx) = mpsc::unbounded_channel();
80        Self {
81            inflight_headers_requests: Default::default(),
82            inflight_bodies_requests: Default::default(),
83            inflight_bals_requests: Default::default(),
84            inflight_receipts_requests: Default::default(),
85            inflight_snap_requests: Default::default(),
86            peers: Default::default(),
87            peers_handle,
88            num_active_peers,
89            queued_requests: Default::default(),
90            download_requests_rx: UnboundedReceiverStream::new(download_requests_rx),
91            download_requests_tx,
92        }
93    }
94
95    /// Invoked when connected to a new peer.
96    pub(crate) fn new_active_peer(&mut self, peer: NewPeerInfo) {
97        let NewPeerInfo {
98            peer_id,
99            best_hash,
100            best_number,
101            capabilities,
102            timeout,
103            range_info,
104            supports_snap,
105        } = peer;
106        self.peers.insert(
107            peer_id,
108            Peer {
109                state: PeerState::Idle,
110                best_hash,
111                best_number,
112                capabilities,
113                timeout,
114                last_response_likely_bad: false,
115                range_info,
116                supports_snap,
117            },
118        );
119    }
120
121    /// Removes the peer from the peer list, after which it is no longer available for future
122    /// requests.
123    ///
124    /// Invoked when an active session was closed.
125    ///
126    /// This cancels also inflight request and sends an error to the receiver.
127    pub(crate) fn on_session_closed(&mut self, peer: &PeerId) {
128        self.peers.remove(peer);
129        if let Some(req) = self.inflight_headers_requests.remove(peer) {
130            let _ = req.response.send(Err(RequestError::ConnectionDropped));
131        }
132        if let Some(req) = self.inflight_bodies_requests.remove(peer) {
133            let _ = req.response.send(Err(RequestError::ConnectionDropped));
134        }
135        if let Some(req) = self.inflight_bals_requests.remove(peer) {
136            let _ = req.response.send(Err(RequestError::ConnectionDropped));
137        }
138        if let Some(req) = self.inflight_receipts_requests.remove(peer) {
139            let _ = req.response.send(Err(RequestError::ConnectionDropped));
140        }
141        if let Some(req) = self.inflight_snap_requests.remove(peer) {
142            let _ = req.response.send(Err(RequestError::ConnectionDropped));
143        }
144    }
145
146    /// Updates the block information for the peer.
147    ///
148    /// Returns `true` if this a newer block
149    pub(crate) fn update_peer_block(&mut self, peer_id: &PeerId, hash: B256, number: u64) -> bool {
150        if let Some(peer) = self.peers.get_mut(peer_id) &&
151            number > peer.best_number
152        {
153            peer.best_hash = hash;
154            peer.best_number = number;
155            return true
156        }
157        false
158    }
159
160    /// Invoked when an active session is about to be disconnected.
161    pub(crate) fn on_pending_disconnect(&mut self, peer_id: &PeerId) {
162        if let Some(peer) = self.peers.get_mut(peer_id) {
163            peer.state = PeerState::Closing;
164        }
165    }
166
167    /// Returns the _next_ idle peer that's ready to accept a request,
168    /// prioritizing those with the lowest timeout/latency and those that recently responded with
169    /// adequate data. Additionally, if full blocks are required this prioritizes peers that have
170    /// full history available
171    fn next_best_peer(&self, requirement: BestPeerRequirements) -> Option<PeerId> {
172        // filter out peers that aren't idle or don't meet the requirement
173        let mut idle = self
174            .peers
175            .iter()
176            .filter(|(_, peer)| peer.state.is_idle() && peer.satisfies(&requirement));
177
178        let mut best_peer = idle.next()?;
179
180        for maybe_better in idle {
181            // replace best peer if our current best peer sent us a bad response last time
182            if best_peer.1.last_response_likely_bad && !maybe_better.1.last_response_likely_bad {
183                best_peer = maybe_better;
184                continue
185            }
186
187            // replace best peer if this peer meets the requirements better
188            if maybe_better.1.is_better(best_peer.1, &requirement) {
189                best_peer = maybe_better;
190                continue
191            }
192
193            // replace best peer if this peer has better rtt and both have same range quality
194            if maybe_better.1.timeout() < best_peer.1.timeout() &&
195                !maybe_better.1.last_response_likely_bad
196            {
197                best_peer = maybe_better;
198            }
199        }
200
201        Some(*best_peer.0)
202    }
203
204    /// Returns whether any connected peer can serve BAL requests.
205    fn has_eth71_peer(&self) -> bool {
206        self.peers.values().any(|peer| {
207            !matches!(peer.state, PeerState::Closing) &&
208                peer.capabilities.supports_eth_at_least(&EthVersion::Eth71)
209        })
210    }
211
212    /// Returns the next action to return
213    fn poll_action(&mut self) -> PollAction {
214        // we only check and not pop here since we don't know yet whether a peer is available.
215        if self.queued_requests.is_empty() {
216            return PollAction::NoRequests
217        }
218
219        let request = self.queued_requests.pop_front().expect("not empty");
220        let Some(peer_id) = self.next_best_peer(request.best_peer_requirements()) else {
221            // Optional BAL/snap requests can lose their capable peer while queued; complete them
222            // instead of waiting for future peer churn.
223            if self.should_fail_fast(&request) {
224                request.send_err_response(RequestError::UnsupportedCapability);
225            } else {
226                // no peer matches this request's requirements; requeue at the back so other
227                // queued requests get a chance on the next poll instead of head-of-line blocking.
228                self.queued_requests.push_back(request);
229            }
230            return PollAction::NoPeersAvailable
231        };
232
233        let request = self.prepare_block_request(peer_id, request);
234
235        PollAction::Ready(FetchAction::BlockRequest { peer_id, request })
236    }
237
238    /// Advance the state the syncer
239    pub(crate) fn poll(&mut self, cx: &mut Context<'_>) -> Poll<FetchAction> {
240        // drain buffered actions first
241        loop {
242            let no_peers_available = match self.poll_action() {
243                PollAction::Ready(action) => return Poll::Ready(action),
244                PollAction::NoRequests => false,
245                PollAction::NoPeersAvailable => true,
246            };
247
248            loop {
249                // poll incoming requests
250                match self.download_requests_rx.poll_next_unpin(cx) {
251                    Poll::Ready(Some(request)) => {
252                        // Optional BAL/snap requests should not wait for future peer churn if no
253                        // connected peer can serve them right now.
254                        if self.should_fail_fast(&request) {
255                            request.send_err_response(RequestError::UnsupportedCapability);
256                            continue
257                        }
258
259                        match request.get_priority() {
260                            Priority::High => {
261                                // find first normal request and queue before it; add this request
262                                // to the back of the high-priority queue
263                                let pos = self
264                                    .queued_requests
265                                    .iter()
266                                    .position(|req| req.is_normal_priority())
267                                    .unwrap_or(0);
268                                self.queued_requests.insert(pos, request);
269                            }
270                            Priority::Normal => {
271                                self.queued_requests.push_back(request);
272                            }
273                        }
274                    }
275                    Poll::Ready(None) => {
276                        unreachable!("channel can't close")
277                    }
278                    Poll::Pending => break,
279                }
280            }
281
282            if self.queued_requests.is_empty() || no_peers_available {
283                return Poll::Pending
284            }
285        }
286    }
287
288    /// Returns whether any connected peer negotiated `snap/2`.
289    fn has_snap_peer(&self) -> bool {
290        self.peers
291            .values()
292            .any(|peer| !matches!(peer.state, PeerState::Closing) && peer.supports_snap)
293    }
294
295    /// Returns `true` if `request` cannot be served by any currently connected peer and should
296    /// fail immediately instead of waiting for future peer churn.
297    fn should_fail_fast(&self, request: &DownloadRequest<N>) -> bool {
298        (request.is_optional_bal() && !self.has_eth71_peer()) ||
299            (request.is_snap() && !self.has_snap_peer())
300    }
301
302    /// Handles a new request to a peer.
303    ///
304    /// Caution: this assumes the peer exists and is idle
305    fn prepare_block_request(&mut self, peer_id: PeerId, req: DownloadRequest<N>) -> BlockRequest {
306        // update the peer's state
307        if let Some(peer) = self.peers.get_mut(&peer_id) {
308            peer.state = req.peer_state();
309        }
310
311        self.prepare_inflight_block_request(peer_id, req)
312    }
313
314    /// Tracks an inflight request and converts it into a peer request.
315    fn prepare_inflight_block_request(
316        &mut self,
317        peer_id: PeerId,
318        req: DownloadRequest<N>,
319    ) -> BlockRequest {
320        match req {
321            DownloadRequest::GetBlockHeaders { request, response, .. } => {
322                let inflight = Request { request: request.clone(), response };
323                self.inflight_headers_requests.insert(peer_id, inflight);
324                let HeadersRequest { start, limit, direction } = request;
325                BlockRequest::GetBlockHeaders(GetBlockHeaders {
326                    start_block: start,
327                    limit,
328                    skip: 0,
329                    direction,
330                })
331            }
332            DownloadRequest::GetBlockBodies { request, response, .. } => {
333                let inflight = Request { request: (), response };
334                self.inflight_bodies_requests.insert(peer_id, inflight);
335                BlockRequest::GetBlockBodies(GetBlockBodies(request))
336            }
337            DownloadRequest::GetBlockAccessLists { request, response, .. } => {
338                let inflight = Request { request: (), response };
339                self.inflight_bals_requests.insert(peer_id, inflight);
340                BlockRequest::GetBlockAccessLists(GetBlockAccessLists(request))
341            }
342            DownloadRequest::GetReceipts { request, response, .. } => {
343                let inflight = Request { request: (), response };
344                self.inflight_receipts_requests.insert(peer_id, inflight);
345                BlockRequest::GetReceipts(GetReceipts(request))
346            }
347            DownloadRequest::GetSnap { request, response, .. } => {
348                let inflight = Request { request: (), response };
349                self.inflight_snap_requests.insert(peer_id, inflight);
350                BlockRequest::GetSnap(Box::new(request))
351            }
352        }
353    }
354
355    /// Returns a queued followup request the peer can serve.
356    ///
357    /// This is an immediate scheduling shortcut after a successful response. It skips queued
358    /// requests whose hard requirements do not match this peer, leaving them for the regular peer
359    /// selection path.
360    ///
361    /// Caution: this expects that the peer is _not_ closed.
362    fn followup_request(&mut self, peer_id: PeerId) -> Option<BlockResponseOutcome> {
363        let peer = self.peers.get_mut(&peer_id)?;
364        let req_idx = self.queued_requests.iter().position(|req| {
365            // Find the first queued request this peer can serve.
366            peer.satisfies(&req.best_peer_requirements())
367        })?;
368        let req = self.queued_requests.remove(req_idx).expect("valid request index");
369
370        peer.state = req.peer_state();
371        let req = self.prepare_inflight_block_request(peer_id, req);
372        Some(BlockResponseOutcome::Request(peer_id, req))
373    }
374
375    /// Called on a `GetBlockHeaders` response from a peer.
376    ///
377    /// This delegates the response and returns a [`BlockResponseOutcome`] to either queue in a
378    /// direct followup request or get the peer reported if the response was a
379    /// [`EthResponseValidator::reputation_change_err`]
380    pub(crate) fn on_block_headers_response(
381        &mut self,
382        peer_id: PeerId,
383        res: RequestResult<Vec<N::BlockHeader>>,
384    ) -> Option<BlockResponseOutcome> {
385        let is_error = res.is_err();
386        let maybe_reputation_change = res.reputation_change_err();
387
388        let resp = self.inflight_headers_requests.remove(&peer_id);
389
390        let is_likely_bad_response =
391            resp.as_ref().is_some_and(|r| res.is_likely_bad_headers_response(&r.request));
392
393        if let Some(resp) = resp {
394            // delegate the response
395            let _ = resp.response.send(res.map(|h| (peer_id, h).into()));
396        }
397
398        if let Some(peer) = self.peers.get_mut(&peer_id) {
399            // update the peer's response state
400            peer.last_response_likely_bad = is_likely_bad_response;
401
402            // If the peer is still ready to accept new requests, we try to send a followup
403            // request immediately.
404            if peer.state.on_request_finished() && !is_error && !is_likely_bad_response {
405                return self.followup_request(peer_id)
406            }
407        }
408
409        // if the response was an `Err` worth reporting the peer for then we return a `BadResponse`
410        // outcome
411        maybe_reputation_change
412            .map(|reputation_change| BlockResponseOutcome::BadResponse(peer_id, reputation_change))
413    }
414
415    /// Called on a `GetBlockBodies` response from a peer
416    pub(crate) fn on_block_bodies_response(
417        &mut self,
418        peer_id: PeerId,
419        res: RequestResult<Vec<N::BlockBody>>,
420    ) -> Option<BlockResponseOutcome> {
421        let is_likely_bad_response = res.as_ref().map_or(true, |bodies| bodies.is_empty());
422
423        if let Some(resp) = self.inflight_bodies_requests.remove(&peer_id) {
424            let _ = resp.response.send(res.map(|b| (peer_id, b).into()));
425        }
426        if let Some(peer) = self.peers.get_mut(&peer_id) {
427            // update the peer's response state
428            peer.last_response_likely_bad = is_likely_bad_response;
429
430            if peer.state.on_request_finished() && !is_likely_bad_response {
431                return self.followup_request(peer_id)
432            }
433        }
434        None
435    }
436
437    /// Called on a `GetBlockAccessLists` response from a peer
438    pub(crate) fn on_block_access_lists_response(
439        &mut self,
440        peer_id: PeerId,
441        res: RequestResult<BlockAccessLists>,
442    ) -> Option<BlockResponseOutcome> {
443        let is_likely_bad_response = res.is_err();
444
445        if let Some(resp) = self.inflight_bals_requests.remove(&peer_id) {
446            let _ = resp.response.send(res.map(|b| (peer_id, b).into()));
447        }
448        if let Some(peer) = self.peers.get_mut(&peer_id) {
449            peer.last_response_likely_bad = is_likely_bad_response;
450
451            if peer.state.on_request_finished() && !is_likely_bad_response {
452                return self.followup_request(peer_id)
453            }
454        }
455        None
456    }
457
458    /// Called on a `GetReceipts` response from a peer.
459    ///
460    /// All receipt variants (legacy with bloom, eth/69, eth/70) are expected to be normalized
461    /// to [`ReceiptsResponse`] by the caller before invoking this method.
462    pub(crate) fn on_receipts_response(
463        &mut self,
464        peer_id: PeerId,
465        res: RequestResult<ReceiptsResponse<N::Receipt>>,
466    ) -> Option<BlockResponseOutcome> {
467        let is_likely_bad_response = res.as_ref().map_or(true, |resp| resp.receipts.is_empty());
468
469        if let Some(resp) = self.inflight_receipts_requests.remove(&peer_id) {
470            let _ = resp.response.send(res.map(|r| (peer_id, r).into()));
471        }
472        if let Some(peer) = self.peers.get_mut(&peer_id) {
473            peer.last_response_likely_bad = is_likely_bad_response;
474
475            if peer.state.on_request_finished() && !is_likely_bad_response {
476                return self.followup_request(peer_id)
477            }
478        }
479        None
480    }
481
482    /// Called on a `snap/2` response from a peer.
483    pub(crate) fn on_snap_response(
484        &mut self,
485        peer_id: PeerId,
486        res: RequestResult<SnapResponse>,
487    ) -> Option<BlockResponseOutcome> {
488        // Unavailable data is a valid answer, but another peer should serve subsequent requests.
489        // Missing BAL entries can also block ordered catch-up despite lists later in the response.
490        let is_likely_bad_response = match &res {
491            Ok(SnapResponse::AccountRange(response)) => {
492                response.accounts.is_empty() && response.proof.is_empty()
493            }
494            Ok(SnapResponse::StorageRanges(response)) => {
495                response.slots.is_empty() && response.proof.is_empty()
496            }
497            Ok(SnapResponse::ByteCodes(response)) => response.codes.is_empty(),
498            Ok(SnapResponse::BlockAccessLists(response)) => {
499                response.block_access_lists.0.is_empty() ||
500                    response.block_access_lists.0.iter().any(Option::is_none)
501            }
502            Err(_) => true,
503        };
504
505        if let Some(resp) = self.inflight_snap_requests.remove(&peer_id) {
506            let _ = resp.response.send(res.map(|r| (peer_id, r).into()));
507        }
508        if let Some(peer) = self.peers.get_mut(&peer_id) {
509            peer.last_response_likely_bad = is_likely_bad_response;
510
511            if peer.state.on_request_finished() && !is_likely_bad_response {
512                return self.followup_request(peer_id)
513            }
514        }
515        None
516    }
517
518    /// Returns a new [`FetchClient`] that can send requests to this type.
519    pub(crate) fn client(&self) -> FetchClient<N> {
520        FetchClient {
521            request_tx: self.download_requests_tx.clone(),
522            peers_handle: self.peers_handle.clone(),
523            num_active_peers: Arc::clone(&self.num_active_peers),
524        }
525    }
526}
527
528/// The outcome of [`StateFetcher::poll_action`]
529enum PollAction {
530    Ready(FetchAction),
531    NoRequests,
532    NoPeersAvailable,
533}
534
535/// Everything [`StateFetcher::new_active_peer`] needs to register a newly connected peer.
536#[derive(Debug)]
537pub(crate) struct NewPeerInfo {
538    /// The remote peer's identifier.
539    pub(crate) peer_id: PeerId,
540    /// Best known hash that the peer has.
541    pub(crate) best_hash: B256,
542    /// The best block number of the peer.
543    pub(crate) best_number: u64,
544    /// Capabilities announced by the peer.
545    pub(crate) capabilities: Arc<Capabilities>,
546    /// The current timeout value to use for the peer.
547    pub(crate) timeout: Arc<AtomicU64>,
548    /// The range info for the peer.
549    pub(crate) range_info: Option<BlockRangeInfo>,
550    /// Whether the connection negotiated `snap/2` and can serve [`DownloadRequest::GetSnap`].
551    pub(crate) supports_snap: bool,
552}
553
554/// Represents a connected peer
555#[derive(Debug)]
556struct Peer {
557    /// The state this peer currently resides in.
558    state: PeerState,
559    /// Best known hash that the peer has
560    best_hash: B256,
561    /// Tracks the best number of the peer.
562    best_number: u64,
563    /// Capabilities announced by the peer.
564    #[allow(dead_code)]
565    capabilities: Arc<Capabilities>,
566    /// Tracks the current timeout value we use for the peer.
567    timeout: Arc<AtomicU64>,
568    /// Tracks whether the peer has recently responded with a likely bad response.
569    ///
570    /// This is used to de-rank the peer if there are other peers available.
571    /// This exists because empty responses may not be penalized (e.g. when blocks near the tip are
572    /// downloaded), but we still want to avoid requesting from the same peer again if it has the
573    /// lowest timeout.
574    last_response_likely_bad: bool,
575    /// Tracks the range info for the peer.
576    range_info: Option<BlockRangeInfo>,
577    /// Whether the connection negotiated `snap/2` and can serve [`DownloadRequest::GetSnap`].
578    supports_snap: bool,
579}
580
581impl Peer {
582    fn timeout(&self) -> u64 {
583        self.timeout.load(Ordering::Relaxed)
584    }
585
586    /// Returns the earliest block number available from the peer.
587    fn earliest(&self) -> u64 {
588        self.range_info.as_ref().map_or(0, |info| info.earliest())
589    }
590
591    /// Returns true if the peer has the full history available.
592    fn has_full_history(&self) -> bool {
593        self.earliest() == 0
594    }
595
596    fn range(&self) -> Option<RangeInclusive<u64>> {
597        self.range_info.as_ref().map(|info| info.range())
598    }
599
600    /// Returns whether this peer can serve requests with the given hard requirements.
601    fn satisfies(&self, requirement: &BestPeerRequirements) -> bool {
602        match requirement {
603            BestPeerRequirements::EthVersion(ver) => self.capabilities.supports_eth_at_least(ver),
604            BestPeerRequirements::SupportsSnap => self.supports_snap,
605            BestPeerRequirements::None |
606            BestPeerRequirements::FullBlock |
607            BestPeerRequirements::FullBlockRange(_) => true,
608        }
609    }
610
611    /// Returns true if this peer has a better range than the other peer for serving the requested
612    /// range.
613    ///
614    /// A peer has a "better range" if:
615    /// 1. It can fully cover the requested range while the other cannot
616    /// 2. None can fully cover the range, but this peer has lower start value
617    /// 3. If a peer doesn't announce a range we assume it has full history, but check the other's
618    ///    range and treat that as better if it can cover the range
619    fn has_better_range(&self, other: &Self, range: &RangeInclusive<u64>) -> bool {
620        let self_range = self.range();
621        let other_range = other.range();
622
623        match (self_range, other_range) {
624            (Some(self_r), Some(other_r)) => {
625                // Check if each peer can fully cover the requested range
626                let self_covers = self_r.contains(range.start()) && self_r.contains(range.end());
627                let other_covers = other_r.contains(range.start()) && other_r.contains(range.end());
628
629                #[expect(clippy::match_same_arms)]
630                match (self_covers, other_covers) {
631                    (true, false) => true,  // Only self covers the range
632                    (false, true) => false, // Only other covers the range
633                    (true, true) => false,  // Both cover
634                    (false, false) => {
635                        // neither covers - prefer if peer has lower (better) start range
636                        self_r.start() < other_r.start()
637                    }
638                }
639            }
640            (Some(self_r), None) => {
641                // Self has range info, other doesn't (treated as full history with unknown latest)
642                // Self is better only if it covers the range
643                self_r.contains(range.start()) && self_r.contains(range.end())
644            }
645            (None, Some(other_r)) => {
646                // Self has no range info (full history), other has range info
647                // Self is better only if other doesn't cover the range
648                !(other_r.contains(range.start()) && other_r.contains(range.end()))
649            }
650            (None, None) => false, // Neither has range info - no one is better
651        }
652    }
653
654    /// Returns true if this peer is better than the other peer based on the given requirements.
655    fn is_better(&self, other: &Self, requirement: &BestPeerRequirements) -> bool {
656        match requirement {
657            BestPeerRequirements::FullBlockRange(range) => self.has_better_range(other, range),
658            BestPeerRequirements::FullBlock => self.has_full_history() && !other.has_full_history(),
659            // Version/capability-based filtering happens in `next_best_peer`, so by the time we
660            // get here both peers already satisfy the requirement.
661            BestPeerRequirements::None |
662            BestPeerRequirements::EthVersion(_) |
663            BestPeerRequirements::SupportsSnap => false,
664        }
665    }
666}
667
668/// Tracks the state of an individual peer
669#[derive(Debug)]
670enum PeerState {
671    /// Peer is currently not handling requests and is available.
672    Idle,
673    /// Peer is handling a `GetBlockHeaders` request.
674    GetBlockHeaders,
675    /// Peer is handling a `GetBlockBodies` request.
676    GetBlockBodies,
677    /// Peer is handling a `GetBlockAccessLists` request.
678    GetBlockAccessLists,
679    /// Peer is handling a `GetReceipts` request.
680    GetReceipts,
681    /// Peer is handling a `snap/2` request.
682    GetSnap,
683    /// Peer session is about to close
684    Closing,
685}
686
687// === impl PeerState ===
688
689impl PeerState {
690    /// Returns true if the peer is currently idle.
691    const fn is_idle(&self) -> bool {
692        matches!(self, Self::Idle)
693    }
694
695    /// Resets the state on a received response.
696    ///
697    /// If the state was already marked as `Closing` do nothing.
698    ///
699    /// Returns `true` if the peer is ready for another request.
700    const fn on_request_finished(&mut self) -> bool {
701        if !matches!(self, Self::Closing) {
702            *self = Self::Idle;
703            return true
704        }
705        false
706    }
707}
708
709/// A request that waits for a response from the network, so it can send it back through the
710/// response channel.
711#[derive(Debug)]
712struct Request<Req, Resp> {
713    /// The issued request object
714    // TODO: this can be attached to the response in error case
715    request: Req,
716    response: oneshot::Sender<Resp>,
717}
718
719/// Requests that can be sent to the Syncer from a [`FetchClient`]
720#[derive(Debug)]
721#[expect(clippy::enum_variant_names)]
722pub(crate) enum DownloadRequest<N: NetworkPrimitives> {
723    /// Download the requested headers and send response through channel
724    GetBlockHeaders {
725        request: HeadersRequest,
726        response: oneshot::Sender<PeerRequestResult<Vec<N::BlockHeader>>>,
727        priority: Priority,
728    },
729    /// Download the requested bodies and send response through channel
730    GetBlockBodies {
731        request: Vec<B256>,
732        response: oneshot::Sender<PeerRequestResult<Vec<N::BlockBody>>>,
733        priority: Priority,
734        range_hint: Option<RangeInclusive<u64>>,
735    },
736    /// Download the requested access lists and send response through channel
737    GetBlockAccessLists {
738        request: Vec<B256>,
739        response: oneshot::Sender<PeerRequestResult<BlockAccessLists>>,
740        priority: Priority,
741        requirement: BalRequirement,
742    },
743    /// Download receipts for the given block hashes and send response through channel
744    GetReceipts {
745        request: Vec<B256>,
746        response: oneshot::Sender<PeerRequestResult<ReceiptsResponse<N::Receipt>>>,
747        priority: Priority,
748    },
749    /// Send a `snap/2` request and send response through channel
750    GetSnap {
751        request: SnapProtocolMessage,
752        response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
753        priority: Priority,
754    },
755}
756
757// === impl DownloadRequest ===
758
759impl<N: NetworkPrimitives> DownloadRequest<N> {
760    /// Returns the corresponding state for a peer that handles the request.
761    const fn peer_state(&self) -> PeerState {
762        match self {
763            Self::GetBlockHeaders { .. } => PeerState::GetBlockHeaders,
764            Self::GetBlockBodies { .. } => PeerState::GetBlockBodies,
765            Self::GetBlockAccessLists { .. } => PeerState::GetBlockAccessLists,
766            Self::GetReceipts { .. } => PeerState::GetReceipts,
767            Self::GetSnap { .. } => PeerState::GetSnap,
768        }
769    }
770
771    /// Returns the requested priority of this request
772    const fn get_priority(&self) -> &Priority {
773        match self {
774            Self::GetBlockHeaders { priority, .. } |
775            Self::GetBlockBodies { priority, .. } |
776            Self::GetBlockAccessLists { priority, .. } |
777            Self::GetReceipts { priority, .. } |
778            Self::GetSnap { priority, .. } => priority,
779        }
780    }
781
782    /// Returns `true` if this request is normal priority.
783    const fn is_normal_priority(&self) -> bool {
784        self.get_priority().is_normal()
785    }
786
787    /// Returns `true` if this is an optional BAL request.
788    const fn is_optional_bal(&self) -> bool {
789        matches!(self, Self::GetBlockAccessLists { requirement: BalRequirement::Optional, .. })
790    }
791
792    /// Returns `true` if this is a `snap/2` request.
793    const fn is_snap(&self) -> bool {
794        matches!(self, Self::GetSnap { .. })
795    }
796
797    /// Sends an error response to the waiting caller.
798    fn send_err_response(self, err: RequestError) {
799        let _ = match self {
800            Self::GetBlockHeaders { response, .. } => response.send(Err(err)).ok(),
801            Self::GetBlockBodies { response, .. } => response.send(Err(err)).ok(),
802            Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
803            Self::GetReceipts { response, .. } => response.send(Err(err)).ok(),
804            Self::GetSnap { response, .. } => response.send(Err(err)).ok(),
805        };
806    }
807
808    /// Returns the best peer requirements for this request.
809    fn best_peer_requirements(&self) -> BestPeerRequirements {
810        match self {
811            Self::GetBlockHeaders { .. } => BestPeerRequirements::None,
812            Self::GetBlockAccessLists { .. } => BestPeerRequirements::EthVersion(EthVersion::Eth71),
813            Self::GetBlockBodies { range_hint, .. } => {
814                if let Some(range) = range_hint {
815                    BestPeerRequirements::FullBlockRange(range.clone())
816                } else {
817                    BestPeerRequirements::FullBlock
818                }
819            }
820            Self::GetReceipts { .. } => BestPeerRequirements::FullBlock,
821            Self::GetSnap { .. } => BestPeerRequirements::SupportsSnap,
822        }
823    }
824}
825
826/// An action the syncer can emit.
827pub(crate) enum FetchAction {
828    /// Dispatch an eth request to the given peer.
829    BlockRequest {
830        /// The targeted recipient for the request
831        peer_id: PeerId,
832        /// The request to send
833        request: BlockRequest,
834    },
835}
836
837/// Outcome of a processed response.
838///
839/// Returned after processing a response.
840#[derive(Debug, PartialEq, Eq)]
841pub(crate) enum BlockResponseOutcome {
842    /// Continue with another request to the peer.
843    Request(PeerId, BlockRequest),
844    /// How to handle a bad response and the reputation change to apply, if any.
845    BadResponse(PeerId, ReputationChangeKind),
846}
847
848/// Additional requirements for how to rank peers during selection.
849enum BestPeerRequirements {
850    /// No additional requirements
851    None,
852    /// Peer must have this block range available.
853    FullBlockRange(RangeInclusive<u64>),
854    /// Peer must have full range.
855    FullBlock,
856    /// Peer must support at least this eth protocol version.
857    EthVersion(EthVersion),
858    /// Peer must have negotiated `snap/2`.
859    SupportsSnap,
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865    use crate::{peers::PeersManager, PeersConfig};
866    use alloy_consensus::Header;
867    use alloy_primitives::{Bytes, B512};
868    use reth_eth_wire::Capability;
869    use reth_eth_wire_types::snap::{
870        AccountRangeMessage, BlockAccessListsMessage, ByteCodesMessage, GetAccountRangeMessage,
871        StorageRangesMessage,
872    };
873    use std::future::poll_fn;
874    use test_case::test_case;
875
876    #[tokio::test(flavor = "multi_thread")]
877    async fn test_poll_fetcher() {
878        let manager = PeersManager::new(PeersConfig::default());
879        let mut fetcher =
880            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
881
882        poll_fn(move |cx| {
883            assert!(fetcher.poll(cx).is_pending());
884            let (tx, _rx) = oneshot::channel();
885            fetcher.queued_requests.push_back(DownloadRequest::GetBlockBodies {
886                request: vec![],
887                response: tx,
888                priority: Priority::default(),
889                range_hint: None,
890            });
891            assert!(fetcher.poll(cx).is_pending());
892
893            Poll::Ready(())
894        })
895        .await;
896    }
897
898    #[tokio::test]
899    async fn test_peer_rotation() {
900        let manager = PeersManager::new(PeersConfig::default());
901        let mut fetcher =
902            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
903        // Add a few random peers
904        let peer1 = B512::random();
905        let peer2 = B512::random();
906        let capabilities = Arc::new(Capabilities::from(vec![]));
907        fetcher.new_active_peer(NewPeerInfo {
908            peer_id: peer1,
909            best_hash: B256::random(),
910            best_number: 1,
911            capabilities: Arc::clone(&capabilities),
912            timeout: Arc::new(AtomicU64::new(1)),
913            range_info: None,
914            supports_snap: false,
915        });
916        fetcher.new_active_peer(NewPeerInfo {
917            peer_id: peer2,
918            best_hash: B256::random(),
919            best_number: 2,
920            capabilities: Arc::clone(&capabilities),
921            timeout: Arc::new(AtomicU64::new(1)),
922            range_info: None,
923            supports_snap: false,
924        });
925
926        let first_peer = fetcher.next_best_peer(BestPeerRequirements::None).unwrap();
927        assert!(first_peer == peer1 || first_peer == peer2);
928        // Pending disconnect for first_peer
929        fetcher.on_pending_disconnect(&first_peer);
930        // first_peer now isn't idle, so we should get other peer
931        let second_peer = fetcher.next_best_peer(BestPeerRequirements::None).unwrap();
932        assert!(first_peer == peer1 || first_peer == peer2);
933        assert_ne!(first_peer, second_peer);
934        // without idle peers, returns None
935        fetcher.on_pending_disconnect(&second_peer);
936        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), None);
937    }
938
939    #[tokio::test]
940    async fn test_peer_prioritization() {
941        let manager = PeersManager::new(PeersConfig::default());
942        let mut fetcher =
943            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
944        // Add a few random peers
945        let peer1 = B512::random();
946        let peer2 = B512::random();
947        let peer3 = B512::random();
948
949        let peer2_timeout = Arc::new(AtomicU64::new(300));
950
951        let capabilities = Arc::new(Capabilities::from(vec![]));
952        fetcher.new_active_peer(NewPeerInfo {
953            peer_id: peer1,
954            best_hash: B256::random(),
955            best_number: 1,
956            capabilities: Arc::clone(&capabilities),
957            timeout: Arc::new(AtomicU64::new(30)),
958            range_info: None,
959            supports_snap: false,
960        });
961        fetcher.new_active_peer(NewPeerInfo {
962            peer_id: peer2,
963            best_hash: B256::random(),
964            best_number: 2,
965            capabilities: Arc::clone(&capabilities),
966            timeout: Arc::clone(&peer2_timeout),
967            range_info: None,
968            supports_snap: false,
969        });
970        fetcher.new_active_peer(NewPeerInfo {
971            peer_id: peer3,
972            best_hash: B256::random(),
973            best_number: 3,
974            capabilities: Arc::clone(&capabilities),
975            timeout: Arc::new(AtomicU64::new(50)),
976            range_info: None,
977            supports_snap: false,
978        });
979
980        // Must always get peer1 (lowest timeout)
981        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer1));
982        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer1));
983        // peer2's timeout changes below peer1's
984        peer2_timeout.store(10, Ordering::Relaxed);
985        // Then we get peer 2 always (now lowest)
986        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer2));
987        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer2));
988    }
989
990    #[tokio::test]
991    async fn test_on_block_headers_response() {
992        let manager = PeersManager::new(PeersConfig::default());
993        let mut fetcher =
994            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
995        let peer_id = B512::random();
996
997        assert_eq!(fetcher.on_block_headers_response(peer_id, Ok(vec![Header::default()])), None);
998
999        assert_eq!(
1000            fetcher.on_block_headers_response(peer_id, Err(RequestError::Timeout)),
1001            Some(BlockResponseOutcome::BadResponse(peer_id, ReputationChangeKind::Timeout))
1002        );
1003        assert_eq!(
1004            fetcher.on_block_headers_response(peer_id, Err(RequestError::BadResponse)),
1005            None
1006        );
1007        assert_eq!(
1008            fetcher.on_block_headers_response(peer_id, Err(RequestError::ChannelClosed)),
1009            None
1010        );
1011        assert_eq!(
1012            fetcher.on_block_headers_response(peer_id, Err(RequestError::ConnectionDropped)),
1013            None
1014        );
1015        assert_eq!(
1016            fetcher.on_block_headers_response(peer_id, Err(RequestError::UnsupportedCapability)),
1017            None
1018        );
1019    }
1020
1021    #[tokio::test]
1022    async fn test_header_response_outcome() {
1023        let manager = PeersManager::new(PeersConfig::default());
1024        let mut fetcher =
1025            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1026        let peer_id = B512::random();
1027
1028        let request_pair = || {
1029            let (tx, _rx) = oneshot::channel();
1030            let req = Request {
1031                request: HeadersRequest {
1032                    start: 0u64.into(),
1033                    limit: 1,
1034                    direction: Default::default(),
1035                },
1036                response: tx,
1037            };
1038            let header = Header { number: 0, ..Default::default() };
1039            (req, header)
1040        };
1041
1042        fetcher.new_active_peer(NewPeerInfo {
1043            peer_id,
1044            best_hash: Default::default(),
1045            best_number: Default::default(),
1046            capabilities: Arc::new(Capabilities::from(vec![])),
1047            timeout: Default::default(),
1048            range_info: None,
1049            supports_snap: false,
1050        });
1051
1052        let (req, header) = request_pair();
1053        fetcher.inflight_headers_requests.insert(peer_id, req);
1054
1055        let outcome = fetcher.on_block_headers_response(peer_id, Ok(vec![header]));
1056        assert!(outcome.is_none());
1057        assert!(fetcher.peers[&peer_id].state.is_idle());
1058
1059        let outcome =
1060            fetcher.on_block_headers_response(peer_id, Err(RequestError::Timeout)).unwrap();
1061
1062        assert!(EthResponseValidator::reputation_change_err(&Err::<Vec<Header>, _>(
1063            RequestError::Timeout
1064        ))
1065        .is_some());
1066
1067        match outcome {
1068            BlockResponseOutcome::BadResponse(peer, _) => {
1069                assert_eq!(peer, peer_id)
1070            }
1071            BlockResponseOutcome::Request(_, _) => {
1072                unreachable!()
1073            }
1074        };
1075
1076        assert!(fetcher.peers[&peer_id].state.is_idle());
1077    }
1078
1079    #[tokio::test]
1080    async fn test_partial_header_response_triggers_followup() {
1081        let (mut fetcher, peer_id) = fetcher_with_peer();
1082
1083        let (followup_tx, _followup_rx) = oneshot::channel();
1084        fetcher.queued_requests.push_back(DownloadRequest::GetBlockHeaders {
1085            request: HeadersRequest::falling(1u64.into(), 1),
1086            response: followup_tx,
1087            priority: Priority::High,
1088        });
1089
1090        let (response_tx, mut response_rx) = oneshot::channel();
1091        fetcher.inflight_headers_requests.insert(
1092            peer_id,
1093            Request { request: HeadersRequest::falling(2u64.into(), 2), response: response_tx },
1094        );
1095        fetcher.peers.get_mut(&peer_id).unwrap().state = PeerState::GetBlockHeaders;
1096
1097        let header = Header { number: 2, ..Default::default() };
1098        let outcome = fetcher.on_block_headers_response(peer_id, Ok(vec![header]));
1099
1100        let Some(BlockResponseOutcome::Request(
1101            dispatched_peer,
1102            BlockRequest::GetBlockHeaders(request),
1103        )) = outcome
1104        else {
1105            panic!("expected a header followup request")
1106        };
1107        assert_eq!(dispatched_peer, peer_id);
1108        assert_eq!(request.start_block, 1u64.into());
1109        assert_eq!(request.limit, 1);
1110        assert!(!fetcher.peers[&peer_id].last_response_likely_bad);
1111        assert!(response_rx.try_recv().is_ok());
1112    }
1113
1114    #[test]
1115    fn test_peer_is_better_none_requirement() {
1116        let peer1 = Peer {
1117            state: PeerState::Idle,
1118            best_hash: B256::random(),
1119            best_number: 100,
1120            capabilities: Arc::new(Capabilities::new(vec![])),
1121            timeout: Arc::new(AtomicU64::new(10)),
1122            last_response_likely_bad: false,
1123            range_info: Some(BlockRangeInfo::new(0, 100, B256::random())),
1124            supports_snap: false,
1125        };
1126
1127        let peer2 = Peer {
1128            state: PeerState::Idle,
1129            best_hash: B256::random(),
1130            best_number: 50,
1131            capabilities: Arc::new(Capabilities::new(vec![])),
1132            timeout: Arc::new(AtomicU64::new(20)),
1133            last_response_likely_bad: false,
1134            range_info: None,
1135            supports_snap: false,
1136        };
1137
1138        // With None requirement, is_better should always return false
1139        assert!(!peer1.is_better(&peer2, &BestPeerRequirements::None));
1140        assert!(!peer2.is_better(&peer1, &BestPeerRequirements::None));
1141    }
1142
1143    #[test]
1144    fn test_peer_is_better_full_block_requirement() {
1145        // Peer with full history (earliest = 0)
1146        let peer_full = Peer {
1147            state: PeerState::Idle,
1148            best_hash: B256::random(),
1149            best_number: 100,
1150            capabilities: Arc::new(Capabilities::new(vec![])),
1151            timeout: Arc::new(AtomicU64::new(10)),
1152            last_response_likely_bad: false,
1153            range_info: Some(BlockRangeInfo::new(0, 100, B256::random())),
1154            supports_snap: false,
1155        };
1156
1157        // Peer without full history (earliest = 50)
1158        let peer_partial = Peer {
1159            state: PeerState::Idle,
1160            best_hash: B256::random(),
1161            best_number: 100,
1162            capabilities: Arc::new(Capabilities::new(vec![])),
1163            timeout: Arc::new(AtomicU64::new(10)),
1164            last_response_likely_bad: false,
1165            range_info: Some(BlockRangeInfo::new(50, 100, B256::random())),
1166            supports_snap: false,
1167        };
1168
1169        // Peer without range info (treated as full history)
1170        let peer_no_range = Peer {
1171            state: PeerState::Idle,
1172            best_hash: B256::random(),
1173            best_number: 100,
1174            capabilities: Arc::new(Capabilities::new(vec![])),
1175            timeout: Arc::new(AtomicU64::new(10)),
1176            last_response_likely_bad: false,
1177            range_info: None,
1178            supports_snap: false,
1179        };
1180
1181        // Peer with full history is better than peer without
1182        assert!(peer_full.is_better(&peer_partial, &BestPeerRequirements::FullBlock));
1183        assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlock));
1184
1185        // Peer without range info (full history) is better than partial
1186        assert!(peer_no_range.is_better(&peer_partial, &BestPeerRequirements::FullBlock));
1187        assert!(!peer_partial.is_better(&peer_no_range, &BestPeerRequirements::FullBlock));
1188
1189        // Both have full history - no improvement
1190        assert!(!peer_full.is_better(&peer_no_range, &BestPeerRequirements::FullBlock));
1191        assert!(!peer_no_range.is_better(&peer_full, &BestPeerRequirements::FullBlock));
1192    }
1193
1194    #[test]
1195    fn test_peer_is_better_full_block_range_requirement() {
1196        let range = RangeInclusive::new(40, 60);
1197
1198        // Peer that covers the requested range
1199        let peer_covers = Peer {
1200            state: PeerState::Idle,
1201            best_hash: B256::random(),
1202            best_number: 100,
1203            capabilities: Arc::new(Capabilities::new(vec![])),
1204            timeout: Arc::new(AtomicU64::new(10)),
1205            last_response_likely_bad: false,
1206            range_info: Some(BlockRangeInfo::new(0, 100, B256::random())),
1207            supports_snap: false,
1208        };
1209
1210        // Peer that doesn't cover the range (earliest too high)
1211        let peer_no_cover = Peer {
1212            state: PeerState::Idle,
1213            best_hash: B256::random(),
1214            best_number: 100,
1215            capabilities: Arc::new(Capabilities::new(vec![])),
1216            timeout: Arc::new(AtomicU64::new(10)),
1217            last_response_likely_bad: false,
1218            range_info: Some(BlockRangeInfo::new(70, 100, B256::random())),
1219            supports_snap: false,
1220        };
1221
1222        // Peer that covers the requested range is better than one that doesn't
1223        assert!(peer_covers
1224            .is_better(&peer_no_cover, &BestPeerRequirements::FullBlockRange(range.clone())));
1225        assert!(
1226            !peer_no_cover.is_better(&peer_covers, &BestPeerRequirements::FullBlockRange(range))
1227        );
1228    }
1229
1230    #[test]
1231    fn test_peer_is_better_both_cover_range() {
1232        let range = RangeInclusive::new(30, 50);
1233
1234        // Peer with full history that covers the range
1235        let peer_full = Peer {
1236            state: PeerState::Idle,
1237            best_hash: B256::random(),
1238            best_number: 100,
1239            capabilities: Arc::new(Capabilities::new(vec![])),
1240            timeout: Arc::new(AtomicU64::new(10)),
1241            last_response_likely_bad: false,
1242            range_info: Some(BlockRangeInfo::new(0, 50, B256::random())),
1243            supports_snap: false,
1244        };
1245
1246        // Peer without full history that also covers the range
1247        let peer_partial = Peer {
1248            state: PeerState::Idle,
1249            best_hash: B256::random(),
1250            best_number: 100,
1251            capabilities: Arc::new(Capabilities::new(vec![])),
1252            timeout: Arc::new(AtomicU64::new(10)),
1253            last_response_likely_bad: false,
1254            range_info: Some(BlockRangeInfo::new(30, 50, B256::random())),
1255            supports_snap: false,
1256        };
1257
1258        // When both cover the range, prefer none
1259        assert!(!peer_full
1260            .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone())));
1261        assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range)));
1262    }
1263
1264    #[test]
1265    fn test_peer_is_better_lower_start() {
1266        let range = RangeInclusive::new(30, 60);
1267
1268        // Peer with full history that covers the range
1269        let peer_full = Peer {
1270            state: PeerState::Idle,
1271            best_hash: B256::random(),
1272            best_number: 100,
1273            capabilities: Arc::new(Capabilities::new(vec![])),
1274            timeout: Arc::new(AtomicU64::new(10)),
1275            last_response_likely_bad: false,
1276            range_info: Some(BlockRangeInfo::new(0, 50, B256::random())),
1277            supports_snap: false,
1278        };
1279
1280        // Peer without full history that also covers the range
1281        let peer_partial = Peer {
1282            state: PeerState::Idle,
1283            best_hash: B256::random(),
1284            best_number: 100,
1285            capabilities: Arc::new(Capabilities::new(vec![])),
1286            timeout: Arc::new(AtomicU64::new(10)),
1287            last_response_likely_bad: false,
1288            range_info: Some(BlockRangeInfo::new(30, 50, B256::random())),
1289            supports_snap: false,
1290        };
1291
1292        // When both cover the range, prefer lower start value
1293        assert!(peer_full
1294            .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone())));
1295        assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range)));
1296    }
1297
1298    #[test]
1299    fn test_peer_is_better_neither_covers_range() {
1300        let range = RangeInclusive::new(40, 60);
1301
1302        // Peer with full history that doesn't cover the range (latest too low)
1303        let peer_full = Peer {
1304            state: PeerState::Idle,
1305            best_hash: B256::random(),
1306            best_number: 30,
1307            capabilities: Arc::new(Capabilities::new(vec![])),
1308            timeout: Arc::new(AtomicU64::new(10)),
1309            last_response_likely_bad: false,
1310            range_info: Some(BlockRangeInfo::new(0, 30, B256::random())),
1311            supports_snap: false,
1312        };
1313
1314        // Peer without full history that also doesn't cover the range
1315        let peer_partial = Peer {
1316            state: PeerState::Idle,
1317            best_hash: B256::random(),
1318            best_number: 30,
1319            capabilities: Arc::new(Capabilities::new(vec![])),
1320            timeout: Arc::new(AtomicU64::new(10)),
1321            last_response_likely_bad: false,
1322            range_info: Some(BlockRangeInfo::new(10, 30, B256::random())),
1323            supports_snap: false,
1324        };
1325
1326        // When neither covers the range, prefer full history
1327        assert!(peer_full
1328            .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone())));
1329        assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range)));
1330    }
1331
1332    #[test]
1333    fn test_peer_is_better_no_range_info() {
1334        let range = RangeInclusive::new(40, 60);
1335
1336        // Peer with range info
1337        let peer_with_range = Peer {
1338            state: PeerState::Idle,
1339            best_hash: B256::random(),
1340            best_number: 100,
1341            capabilities: Arc::new(Capabilities::new(vec![])),
1342            timeout: Arc::new(AtomicU64::new(10)),
1343            last_response_likely_bad: false,
1344            range_info: Some(BlockRangeInfo::new(30, 100, B256::random())),
1345            supports_snap: false,
1346        };
1347
1348        // Peer without range info
1349        let peer_no_range = Peer {
1350            state: PeerState::Idle,
1351            best_hash: B256::random(),
1352            best_number: 100,
1353            capabilities: Arc::new(Capabilities::new(vec![])),
1354            timeout: Arc::new(AtomicU64::new(10)),
1355            last_response_likely_bad: false,
1356            range_info: None,
1357            supports_snap: false,
1358        };
1359
1360        // Peer without range info is not better (we prefer peers with known ranges)
1361        assert!(!peer_no_range
1362            .is_better(&peer_with_range, &BestPeerRequirements::FullBlockRange(range.clone())));
1363
1364        // Peer with range info is better than peer without
1365        assert!(
1366            peer_with_range.is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range))
1367        );
1368    }
1369
1370    #[test]
1371    fn test_peer_is_better_one_peer_no_range_covers() {
1372        let range = RangeInclusive::new(40, 60);
1373
1374        // Peer with range info that covers the requested range
1375        let peer_with_range_covers = Peer {
1376            state: PeerState::Idle,
1377            best_hash: B256::random(),
1378            best_number: 100,
1379            capabilities: Arc::new(Capabilities::new(vec![])),
1380            timeout: Arc::new(AtomicU64::new(10)),
1381            last_response_likely_bad: false,
1382            range_info: Some(BlockRangeInfo::new(30, 100, B256::random())),
1383            supports_snap: false,
1384        };
1385
1386        // Peer without range info (treated as full history with unknown latest)
1387        let peer_no_range = Peer {
1388            state: PeerState::Idle,
1389            best_hash: B256::random(),
1390            best_number: 100,
1391            capabilities: Arc::new(Capabilities::new(vec![])),
1392            timeout: Arc::new(AtomicU64::new(10)),
1393            last_response_likely_bad: false,
1394            range_info: None,
1395            supports_snap: false,
1396        };
1397
1398        // Peer with range that covers is better than peer without range info
1399        assert!(peer_with_range_covers
1400            .is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range.clone())));
1401
1402        // Peer without range info is not better when other covers
1403        assert!(!peer_no_range
1404            .is_better(&peer_with_range_covers, &BestPeerRequirements::FullBlockRange(range)));
1405    }
1406
1407    #[test]
1408    fn test_peer_is_better_one_peer_no_range_doesnt_cover() {
1409        let range = RangeInclusive::new(40, 60);
1410
1411        // Peer with range info that does NOT cover the requested range (too high)
1412        let peer_with_range_no_cover = Peer {
1413            state: PeerState::Idle,
1414            best_hash: B256::random(),
1415            best_number: 100,
1416            capabilities: Arc::new(Capabilities::new(vec![])),
1417            timeout: Arc::new(AtomicU64::new(10)),
1418            last_response_likely_bad: false,
1419            range_info: Some(BlockRangeInfo::new(70, 100, B256::random())),
1420            supports_snap: false,
1421        };
1422
1423        // Peer without range info (treated as full history)
1424        let peer_no_range = Peer {
1425            state: PeerState::Idle,
1426            best_hash: B256::random(),
1427            best_number: 100,
1428            capabilities: Arc::new(Capabilities::new(vec![])),
1429            timeout: Arc::new(AtomicU64::new(10)),
1430            last_response_likely_bad: false,
1431            range_info: None,
1432            supports_snap: false,
1433        };
1434
1435        // Peer with range that doesn't cover is not better
1436        assert!(!peer_with_range_no_cover
1437            .is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range.clone())));
1438
1439        // Peer without range info (full history) is better when other doesn't cover
1440        assert!(peer_no_range
1441            .is_better(&peer_with_range_no_cover, &BestPeerRequirements::FullBlockRange(range)));
1442    }
1443
1444    #[test]
1445    fn test_peer_is_better_edge_cases() {
1446        // Test exact range boundaries
1447        let range = RangeInclusive::new(50, 100);
1448
1449        // Peer that exactly covers the range
1450        let peer_exact = Peer {
1451            state: PeerState::Idle,
1452            best_hash: B256::random(),
1453            best_number: 100,
1454            capabilities: Arc::new(Capabilities::new(vec![])),
1455            timeout: Arc::new(AtomicU64::new(10)),
1456            last_response_likely_bad: false,
1457            range_info: Some(BlockRangeInfo::new(50, 100, B256::random())),
1458            supports_snap: false,
1459        };
1460
1461        // Peer that's one block short at the start
1462        let peer_short_start = Peer {
1463            state: PeerState::Idle,
1464            best_hash: B256::random(),
1465            best_number: 100,
1466            capabilities: Arc::new(Capabilities::new(vec![])),
1467            timeout: Arc::new(AtomicU64::new(10)),
1468            last_response_likely_bad: false,
1469            range_info: Some(BlockRangeInfo::new(51, 100, B256::random())),
1470            supports_snap: false,
1471        };
1472
1473        // Peer that's one block short at the end
1474        let peer_short_end = Peer {
1475            state: PeerState::Idle,
1476            best_hash: B256::random(),
1477            best_number: 100,
1478            capabilities: Arc::new(Capabilities::new(vec![])),
1479            timeout: Arc::new(AtomicU64::new(10)),
1480            last_response_likely_bad: false,
1481            range_info: Some(BlockRangeInfo::new(50, 99, B256::random())),
1482            supports_snap: false,
1483        };
1484
1485        // Exact coverage is better than short coverage
1486        assert!(peer_exact
1487            .is_better(&peer_short_start, &BestPeerRequirements::FullBlockRange(range.clone())));
1488        assert!(peer_exact
1489            .is_better(&peer_short_end, &BestPeerRequirements::FullBlockRange(range.clone())));
1490
1491        // Short coverage is not better than exact coverage
1492        assert!(!peer_short_start
1493            .is_better(&peer_exact, &BestPeerRequirements::FullBlockRange(range.clone())));
1494        assert!(
1495            !peer_short_end.is_better(&peer_exact, &BestPeerRequirements::FullBlockRange(range))
1496        );
1497    }
1498
1499    /// Creates a `StateFetcher` with a single idle peer and returns both.
1500    fn fetcher_with_peer() -> (StateFetcher<EthNetworkPrimitives>, PeerId) {
1501        let manager = PeersManager::new(PeersConfig::default());
1502        let mut fetcher =
1503            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1504        let peer_id = B512::random();
1505
1506        fetcher.new_active_peer(NewPeerInfo {
1507            peer_id,
1508            best_hash: Default::default(),
1509            best_number: Default::default(),
1510            capabilities: Arc::new(Capabilities::from(vec![])),
1511            timeout: Default::default(),
1512            range_info: None,
1513            supports_snap: false,
1514        });
1515        (fetcher, peer_id)
1516    }
1517
1518    /// Inserts an inflight receipts request into the fetcher and returns the
1519    /// `oneshot::Receiver` that the final response will be sent through.
1520    fn insert_inflight_receipts(
1521        fetcher: &mut StateFetcher<EthNetworkPrimitives>,
1522        peer_id: PeerId,
1523    ) -> oneshot::Receiver<PeerRequestResult<ReceiptsResponse<reth_ethereum_primitives::Receipt>>>
1524    {
1525        let (tx, rx) = oneshot::channel();
1526        fetcher.inflight_receipts_requests.insert(peer_id, Request { request: (), response: tx });
1527        fetcher.peers.get_mut(&peer_id).unwrap().state = PeerState::GetReceipts;
1528        rx
1529    }
1530
1531    // ---- Receipts: basic dispatch ----
1532
1533    #[tokio::test]
1534    async fn test_poll_dispatches_receipts_to_peer() {
1535        let (mut fetcher, peer_id) = fetcher_with_peer();
1536
1537        poll_fn(move |cx| {
1538            let (tx, _rx) = oneshot::channel();
1539            fetcher.queued_requests.push_back(DownloadRequest::GetReceipts {
1540                request: vec![B256::ZERO],
1541                response: tx,
1542                priority: Priority::default(),
1543            });
1544
1545            let Poll::Ready(FetchAction::BlockRequest { peer_id: dispatched_peer, request }) =
1546                fetcher.poll(cx)
1547            else {
1548                panic!("expected Ready(BlockRequest)");
1549            };
1550            assert_eq!(dispatched_peer, peer_id);
1551            assert!(matches!(request, BlockRequest::GetReceipts(_)));
1552
1553            // Peer should now be in GetReceipts state
1554            assert!(matches!(fetcher.peers[&peer_id].state, PeerState::GetReceipts));
1555            // Inflight request should be tracked
1556            assert!(fetcher.inflight_receipts_requests.contains_key(&peer_id));
1557
1558            Poll::Ready(())
1559        })
1560        .await;
1561    }
1562
1563    // ---- Receipts: response handling ----
1564
1565    #[tokio::test]
1566    async fn test_receipts_complete_response_resolves_and_idles_peer() {
1567        let (mut fetcher, peer_id) = fetcher_with_peer();
1568
1569        let rx = insert_inflight_receipts(&mut fetcher, peer_id);
1570
1571        let resp = ReceiptsResponse::new(vec![vec![]]);
1572        let outcome = fetcher.on_receipts_response(peer_id, Ok(resp));
1573
1574        // No queued requests, so no followup
1575        assert!(outcome.is_none());
1576        // Peer back to idle
1577        assert!(fetcher.peers[&peer_id].state.is_idle());
1578        // Inflight cleaned up
1579        assert!(!fetcher.inflight_receipts_requests.contains_key(&peer_id));
1580
1581        // Caller receives the response
1582        let result = rx.await.unwrap().unwrap();
1583        assert_eq!(result.1.receipts.len(), 1);
1584    }
1585
1586    #[tokio::test]
1587    async fn test_receipts_empty_response_marks_peer_bad() {
1588        let (mut fetcher, peer_id) = fetcher_with_peer();
1589        let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
1590
1591        let resp = ReceiptsResponse::new(vec![]);
1592        let _ = fetcher.on_receipts_response(peer_id, Ok(resp));
1593
1594        assert!(fetcher.peers[&peer_id].last_response_likely_bad);
1595    }
1596
1597    #[tokio::test]
1598    async fn test_receipts_error_forwards_and_marks_peer_bad() {
1599        let (mut fetcher, peer_id) = fetcher_with_peer();
1600        let rx = insert_inflight_receipts(&mut fetcher, peer_id);
1601
1602        let _ = fetcher.on_receipts_response(peer_id, Err(RequestError::Timeout));
1603
1604        assert!(fetcher.peers[&peer_id].last_response_likely_bad);
1605        // Error is forwarded to the caller
1606        let result = rx.await.unwrap();
1607        assert_eq!(result.unwrap_err(), RequestError::Timeout);
1608    }
1609
1610    #[tokio::test]
1611    async fn test_session_closed_cancels_inflight_receipts() {
1612        let (mut fetcher, peer_id) = fetcher_with_peer();
1613        let rx = insert_inflight_receipts(&mut fetcher, peer_id);
1614
1615        fetcher.on_session_closed(&peer_id);
1616
1617        assert!(!fetcher.peers.contains_key(&peer_id));
1618        assert!(!fetcher.inflight_receipts_requests.contains_key(&peer_id));
1619
1620        let result = rx.await.unwrap();
1621        assert_eq!(result.unwrap_err(), RequestError::ConnectionDropped);
1622    }
1623
1624    #[tokio::test]
1625    async fn test_receipts_response_triggers_followup() {
1626        let (mut fetcher, peer_id) = fetcher_with_peer();
1627
1628        // Queue a bodies request as a followup candidate
1629        let (followup_tx, _followup_rx) = oneshot::channel();
1630        fetcher.queued_requests.push_back(DownloadRequest::GetBlockBodies {
1631            request: vec![B256::random()],
1632            response: followup_tx,
1633            priority: Priority::default(),
1634            range_hint: None,
1635        });
1636
1637        let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
1638
1639        let resp = ReceiptsResponse::new(vec![vec![]]);
1640        let outcome = fetcher.on_receipts_response(peer_id, Ok(resp));
1641
1642        assert!(matches!(outcome, Some(BlockResponseOutcome::Request(pid, _)) if pid == peer_id));
1643    }
1644
1645    #[tokio::test]
1646    async fn test_followup_skips_request_peer_cannot_serve() {
1647        let (mut fetcher, peer_id) = fetcher_with_peer();
1648
1649        let peer_71 = B512::random();
1650        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1651        fetcher.new_active_peer(NewPeerInfo {
1652            peer_id: peer_71,
1653            best_hash: B256::random(),
1654            best_number: 100,
1655            capabilities: caps_71,
1656            timeout: Arc::new(AtomicU64::new(10)),
1657            range_info: None,
1658            supports_snap: false,
1659        });
1660        fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
1661
1662        let (followup_tx, _followup_rx) = oneshot::channel();
1663        fetcher.queued_requests.push_back(DownloadRequest::GetBlockAccessLists {
1664            request: vec![B256::random()],
1665            response: followup_tx,
1666            priority: Priority::Normal,
1667            requirement: BalRequirement::Optional,
1668        });
1669
1670        let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
1671
1672        let resp = ReceiptsResponse::new(vec![vec![]]);
1673        assert!(fetcher.on_receipts_response(peer_id, Ok(resp)).is_none());
1674        assert!(fetcher.peers[&peer_id].state.is_idle());
1675        assert!(!fetcher.inflight_bals_requests.contains_key(&peer_id));
1676        assert!(matches!(
1677            fetcher.queued_requests.front(),
1678            Some(DownloadRequest::GetBlockAccessLists {
1679                requirement: BalRequirement::Optional,
1680                ..
1681            })
1682        ));
1683    }
1684
1685    #[tokio::test]
1686    async fn test_followup_uses_first_satisfiable_request() {
1687        let (mut fetcher, peer_id) = fetcher_with_peer();
1688
1689        let peer_71 = B512::random();
1690        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1691        fetcher.new_active_peer(NewPeerInfo {
1692            peer_id: peer_71,
1693            best_hash: B256::random(),
1694            best_number: 100,
1695            capabilities: caps_71,
1696            timeout: Arc::new(AtomicU64::new(10)),
1697            range_info: None,
1698            supports_snap: false,
1699        });
1700        fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
1701
1702        let (bal_tx, _bal_rx) = oneshot::channel();
1703        fetcher.queued_requests.push_back(DownloadRequest::GetBlockAccessLists {
1704            request: vec![B256::random()],
1705            response: bal_tx,
1706            priority: Priority::Normal,
1707            requirement: BalRequirement::Optional,
1708        });
1709
1710        let (bodies_tx, _bodies_rx) = oneshot::channel();
1711        fetcher.queued_requests.push_back(DownloadRequest::GetBlockBodies {
1712            request: vec![B256::random()],
1713            response: bodies_tx,
1714            priority: Priority::Normal,
1715            range_hint: None,
1716        });
1717
1718        let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
1719
1720        let resp = ReceiptsResponse::new(vec![vec![]]);
1721        let outcome = fetcher.on_receipts_response(peer_id, Ok(resp));
1722
1723        assert!(matches!(
1724            outcome,
1725            Some(BlockResponseOutcome::Request(pid, BlockRequest::GetBlockBodies(_))) if pid == peer_id
1726        ));
1727        assert!(fetcher.inflight_bodies_requests.contains_key(&peer_id));
1728        assert!(matches!(fetcher.peers[&peer_id].state, PeerState::GetBlockBodies));
1729        assert_eq!(fetcher.queued_requests.len(), 1);
1730        assert!(matches!(
1731            fetcher.queued_requests.front(),
1732            Some(DownloadRequest::GetBlockAccessLists {
1733                requirement: BalRequirement::Optional,
1734                ..
1735            })
1736        ));
1737    }
1738
1739    #[tokio::test]
1740    async fn test_prepare_block_request_creates_inflight_receipts() {
1741        let (mut fetcher, peer_id) = fetcher_with_peer();
1742        let hashes = vec![B256::with_last_byte(1), B256::with_last_byte(2)];
1743
1744        let (tx, _rx) = oneshot::channel();
1745        let req = DownloadRequest::GetReceipts {
1746            request: hashes.clone(),
1747            response: tx,
1748            priority: Priority::default(),
1749        };
1750
1751        let block_request = fetcher.prepare_block_request(peer_id, req);
1752
1753        // Returns a GetReceipts block request with the same hashes
1754        match block_request {
1755            BlockRequest::GetReceipts(ref get) => {
1756                assert_eq!(get.0, hashes);
1757            }
1758            other => panic!("expected GetReceipts, got {other:?}"),
1759        }
1760
1761        // Peer state transitions to GetReceipts
1762        assert!(matches!(fetcher.peers[&peer_id].state, PeerState::GetReceipts));
1763
1764        // Inflight request is tracked
1765        assert!(fetcher.inflight_receipts_requests.contains_key(&peer_id));
1766    }
1767    #[tokio::test]
1768    async fn test_next_best_peer_eth71_no_support() {
1769        let manager = PeersManager::new(PeersConfig::default());
1770        let mut fetcher =
1771            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1772
1773        let peer = B512::random();
1774
1775        // Capabilities WITHOUT eth71
1776        let capabilities = Arc::new(Capabilities::new(vec![]));
1777
1778        fetcher.new_active_peer(NewPeerInfo {
1779            peer_id: peer,
1780            best_hash: B256::random(),
1781            best_number: 100,
1782            capabilities,
1783            timeout: Arc::new(AtomicU64::new(10)),
1784            range_info: None,
1785            supports_snap: false,
1786        });
1787
1788        // Should return None because peer doesn't support eth71
1789        assert_eq!(
1790            fetcher.next_best_peer(BestPeerRequirements::EthVersion(EthVersion::Eth71)),
1791            None
1792        );
1793    }
1794
1795    #[tokio::test]
1796    async fn test_next_best_peer_eth71_supported() {
1797        let manager = PeersManager::new(PeersConfig::default());
1798        let mut fetcher =
1799            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1800
1801        let peer = B512::random();
1802
1803        // Build capability list that includes Eth71
1804        let capabilities = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1805
1806        fetcher.new_active_peer(NewPeerInfo {
1807            peer_id: peer,
1808            best_hash: B256::random(),
1809            best_number: 100,
1810            capabilities,
1811            timeout: Arc::new(AtomicU64::new(10)),
1812            range_info: None,
1813            supports_snap: false,
1814        });
1815
1816        assert_eq!(
1817            fetcher.next_best_peer(BestPeerRequirements::EthVersion(EthVersion::Eth71)),
1818            Some(peer)
1819        );
1820    }
1821
1822    #[tokio::test]
1823    async fn test_next_best_peer_eth71_filters_correctly() {
1824        let manager = PeersManager::new(PeersConfig::default());
1825        let mut fetcher =
1826            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1827
1828        let peer_no_71 = B512::random();
1829        let peer_with_71 = B512::random();
1830
1831        // Peer without eth71
1832        let caps_old = Arc::new(Capabilities::new(vec![]));
1833
1834        // Peer with eth71
1835        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1836
1837        fetcher.new_active_peer(NewPeerInfo {
1838            peer_id: peer_no_71,
1839            best_hash: B256::random(),
1840            best_number: 100,
1841            capabilities: caps_old,
1842            timeout: Arc::new(AtomicU64::new(5)),
1843            range_info: None,
1844            supports_snap: false,
1845        });
1846
1847        fetcher.new_active_peer(NewPeerInfo {
1848            peer_id: peer_with_71,
1849            best_hash: B256::random(),
1850            best_number: 100,
1851            capabilities: caps_71,
1852            timeout: Arc::new(AtomicU64::new(50)),
1853            range_info: None,
1854            supports_snap: false,
1855        });
1856
1857        // Even though peer_no_71 has lower timeout,
1858        // it must NOT be selected.
1859        assert_eq!(
1860            fetcher.next_best_peer(BestPeerRequirements::EthVersion(EthVersion::Eth71)),
1861            Some(peer_with_71)
1862        );
1863    }
1864
1865    #[tokio::test]
1866    async fn test_wakes_when_eth71_peer_connects() {
1867        use futures::task::noop_waker;
1868        use std::task::{Context, Poll};
1869
1870        let manager = PeersManager::new(PeersConfig::default());
1871        let mut fetcher =
1872            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1873
1874        // Queue Eth71-required request
1875        let (tx, _rx) = oneshot::channel();
1876        fetcher.queued_requests.push_back(DownloadRequest::GetBlockAccessLists {
1877            request: vec![],
1878            response: tx,
1879            priority: Priority::Normal,
1880            requirement: BalRequirement::Mandatory,
1881        });
1882
1883        let waker = noop_waker();
1884        let mut cx = Context::from_waker(&waker);
1885
1886        // No peers -> must be Pending
1887        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
1888
1889        // Add peer WITHOUT Eth71 support
1890        let peer_old = B512::random();
1891        let caps_old = Arc::new(Capabilities::new(vec![]));
1892
1893        fetcher.new_active_peer(NewPeerInfo {
1894            peer_id: peer_old,
1895            best_hash: B256::random(),
1896            best_number: 100,
1897            capabilities: caps_old,
1898            timeout: Arc::new(AtomicU64::new(10)),
1899            range_info: None,
1900            supports_snap: false,
1901        });
1902
1903        // Still Pending
1904        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
1905
1906        // Add peer WITH Eth71 support
1907        let peer_71 = B512::random();
1908        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1909
1910        fetcher.new_active_peer(NewPeerInfo {
1911            peer_id: peer_71,
1912            best_hash: B256::random(),
1913            best_number: 100,
1914            capabilities: caps_71,
1915            timeout: Arc::new(AtomicU64::new(10)),
1916            range_info: None,
1917            supports_snap: false,
1918        });
1919
1920        // Now we must get Ready(BlockRequest)
1921        if let Poll::Ready(FetchAction::BlockRequest { peer_id, .. }) = fetcher.poll(&mut cx) {
1922            assert_eq!(peer_id, peer_71);
1923        }
1924    }
1925
1926    #[tokio::test]
1927    async fn test_optional_bal_request_rejected_without_eth71_peer() {
1928        use futures::task::noop_waker;
1929        use std::task::{Context, Poll};
1930
1931        let manager = PeersManager::new(PeersConfig::default());
1932        let mut fetcher =
1933            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1934
1935        let peer_old = B512::random();
1936        let caps_old = Arc::new(Capabilities::new(vec![]));
1937        fetcher.new_active_peer(NewPeerInfo {
1938            peer_id: peer_old,
1939            best_hash: B256::random(),
1940            best_number: 100,
1941            capabilities: caps_old,
1942            timeout: Arc::new(AtomicU64::new(10)),
1943            range_info: None,
1944            supports_snap: false,
1945        });
1946
1947        let (tx, rx) = oneshot::channel();
1948        fetcher
1949            .download_requests_tx
1950            .send(DownloadRequest::GetBlockAccessLists {
1951                request: vec![],
1952                response: tx,
1953                priority: Priority::Normal,
1954                requirement: BalRequirement::Optional,
1955            })
1956            .unwrap();
1957
1958        let waker = noop_waker();
1959        let mut cx = Context::from_waker(&waker);
1960
1961        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
1962        assert!(fetcher.queued_requests.is_empty());
1963        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
1964    }
1965
1966    #[tokio::test]
1967    async fn test_optional_bal_request_waits_for_busy_eth71_peer() {
1968        use futures::task::noop_waker;
1969        use std::task::{Context, Poll};
1970
1971        let manager = PeersManager::new(PeersConfig::default());
1972        let mut fetcher =
1973            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1974
1975        let peer_71 = B512::random();
1976        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1977        fetcher.new_active_peer(NewPeerInfo {
1978            peer_id: peer_71,
1979            best_hash: B256::random(),
1980            best_number: 100,
1981            capabilities: caps_71,
1982            timeout: Arc::new(AtomicU64::new(10)),
1983            range_info: None,
1984            supports_snap: false,
1985        });
1986        fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
1987
1988        let (tx, _rx) = oneshot::channel();
1989        fetcher
1990            .download_requests_tx
1991            .send(DownloadRequest::GetBlockAccessLists {
1992                request: vec![],
1993                response: tx,
1994                priority: Priority::Normal,
1995                requirement: BalRequirement::Optional,
1996            })
1997            .unwrap();
1998
1999        let waker = noop_waker();
2000        let mut cx = Context::from_waker(&waker);
2001
2002        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2003        assert_eq!(fetcher.queued_requests.len(), 1);
2004    }
2005
2006    #[tokio::test]
2007    async fn test_queued_optional_bal_request_rejected_after_eth71_disconnect() {
2008        use futures::task::noop_waker;
2009        use std::task::{Context, Poll};
2010
2011        let manager = PeersManager::new(PeersConfig::default());
2012        let mut fetcher =
2013            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2014
2015        let peer_old = B512::random();
2016        let caps_old = Arc::new(Capabilities::new(vec![]));
2017        fetcher.new_active_peer(NewPeerInfo {
2018            peer_id: peer_old,
2019            best_hash: B256::random(),
2020            best_number: 100,
2021            capabilities: caps_old,
2022            timeout: Arc::new(AtomicU64::new(10)),
2023            range_info: None,
2024            supports_snap: false,
2025        });
2026
2027        let peer_71 = B512::random();
2028        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
2029        fetcher.new_active_peer(NewPeerInfo {
2030            peer_id: peer_71,
2031            best_hash: B256::random(),
2032            best_number: 100,
2033            capabilities: caps_71,
2034            timeout: Arc::new(AtomicU64::new(10)),
2035            range_info: None,
2036            supports_snap: false,
2037        });
2038        fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
2039
2040        let (tx, rx) = oneshot::channel();
2041        fetcher
2042            .download_requests_tx
2043            .send(DownloadRequest::GetBlockAccessLists {
2044                request: vec![],
2045                response: tx,
2046                priority: Priority::Normal,
2047                requirement: BalRequirement::Optional,
2048            })
2049            .unwrap();
2050
2051        let waker = noop_waker();
2052        let mut cx = Context::from_waker(&waker);
2053
2054        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2055        assert_eq!(fetcher.queued_requests.len(), 1);
2056
2057        fetcher.on_session_closed(&peer_71);
2058
2059        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2060        assert!(fetcher.queued_requests.is_empty());
2061        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
2062    }
2063
2064    #[tokio::test]
2065    async fn test_next_best_peer_snap_no_support() {
2066        let manager = PeersManager::new(PeersConfig::default());
2067        let mut fetcher =
2068            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2069
2070        let peer = B512::random();
2071        fetcher.new_active_peer(NewPeerInfo {
2072            peer_id: peer,
2073            best_hash: B256::random(),
2074            best_number: 100,
2075            capabilities: Arc::new(Capabilities::new(vec![])),
2076            timeout: Arc::new(AtomicU64::new(10)),
2077            range_info: None,
2078            supports_snap: false,
2079        });
2080
2081        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::SupportsSnap), None);
2082    }
2083
2084    #[tokio::test]
2085    async fn test_next_best_peer_snap_filters_correctly() {
2086        let manager = PeersManager::new(PeersConfig::default());
2087        let mut fetcher =
2088            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2089
2090        let peer_no_snap = B512::random();
2091        let peer_with_snap = B512::random();
2092
2093        fetcher.new_active_peer(NewPeerInfo {
2094            peer_id: peer_no_snap,
2095            best_hash: B256::random(),
2096            best_number: 100,
2097            capabilities: Arc::new(Capabilities::new(vec![])),
2098            timeout: Arc::new(AtomicU64::new(5)),
2099            range_info: None,
2100            supports_snap: false,
2101        });
2102        fetcher.new_active_peer(NewPeerInfo {
2103            peer_id: peer_with_snap,
2104            best_hash: B256::random(),
2105            best_number: 100,
2106            capabilities: Arc::new(Capabilities::new(vec![])),
2107            timeout: Arc::new(AtomicU64::new(50)),
2108            range_info: None,
2109            supports_snap: true,
2110        });
2111
2112        // Even though peer_no_snap has a lower timeout, it must NOT be selected.
2113        assert_eq!(
2114            fetcher.next_best_peer(BestPeerRequirements::SupportsSnap),
2115            Some(peer_with_snap)
2116        );
2117    }
2118
2119    #[tokio::test]
2120    async fn test_snap_request_rejected_without_snap_peer() {
2121        use futures::task::noop_waker;
2122        use std::task::{Context, Poll};
2123
2124        let manager = PeersManager::new(PeersConfig::default());
2125        let mut fetcher =
2126            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2127
2128        // Only an eth-only peer is connected.
2129        fetcher.new_active_peer(NewPeerInfo {
2130            peer_id: B512::random(),
2131            best_hash: B256::random(),
2132            best_number: 100,
2133            capabilities: Arc::new(Capabilities::new(vec![])),
2134            timeout: Arc::new(AtomicU64::new(10)),
2135            range_info: None,
2136            supports_snap: false,
2137        });
2138
2139        let (tx, rx) = oneshot::channel();
2140        fetcher
2141            .download_requests_tx
2142            .send(DownloadRequest::GetSnap {
2143                request: SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
2144                    request_id: 0,
2145                    root_hash: B256::ZERO,
2146                    starting_hash: B256::ZERO,
2147                    limit_hash: B256::ZERO,
2148                    response_bytes: 0,
2149                }),
2150                response: tx,
2151                priority: Priority::Normal,
2152            })
2153            .unwrap();
2154
2155        let waker = noop_waker();
2156        let mut cx = Context::from_waker(&waker);
2157
2158        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2159        assert!(fetcher.queued_requests.is_empty());
2160        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
2161    }
2162
2163    #[test_case(SnapResponse::AccountRange(AccountRangeMessage {
2164        request_id: 1, accounts: vec![], proof: vec![Bytes::from_static(&[0xc0])],
2165    }); "account proof")]
2166    #[test_case(SnapResponse::StorageRanges(StorageRangesMessage {
2167        request_id: 1, slots: vec![], proof: vec![Bytes::from_static(&[0xc0])],
2168    }); "storage proof")]
2169    #[test_case(SnapResponse::ByteCodes(ByteCodesMessage {
2170        request_id: 1, codes: vec![Bytes::new()],
2171    }); "empty bytecode")]
2172    #[test_case(SnapResponse::BlockAccessLists(BlockAccessListsMessage {
2173        request_id: 1,
2174        block_access_lists: BlockAccessLists(vec![Some(Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]))]),
2175    }); "present empty BAL")]
2176    #[tokio::test]
2177    async fn test_snap_response_triggers_followup(resp: SnapResponse) {
2178        let manager = PeersManager::new(PeersConfig::default());
2179        let mut fetcher =
2180            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2181
2182        let peer_id = B512::random();
2183        fetcher.new_active_peer(NewPeerInfo {
2184            peer_id,
2185            best_hash: B256::random(),
2186            best_number: 100,
2187            capabilities: Arc::new(Capabilities::new(vec![])),
2188            timeout: Arc::new(AtomicU64::new(10)),
2189            range_info: None,
2190            supports_snap: true,
2191        });
2192
2193        // Queue a followup snap request for the same peer.
2194        let (followup_tx, _followup_rx) = oneshot::channel();
2195        fetcher.queued_requests.push_back(DownloadRequest::GetSnap {
2196            request: SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
2197                request_id: 0,
2198                root_hash: B256::ZERO,
2199                starting_hash: B256::ZERO,
2200                limit_hash: B256::ZERO,
2201                response_bytes: 0,
2202            }),
2203            response: followup_tx,
2204            priority: Priority::Normal,
2205        });
2206
2207        let (tx, mut rx) = oneshot::channel();
2208        fetcher.inflight_snap_requests.insert(peer_id, Request { request: (), response: tx });
2209        fetcher.peers.get_mut(&peer_id).unwrap().state = PeerState::GetSnap;
2210
2211        let outcome = fetcher.on_snap_response(peer_id, Ok(resp));
2212
2213        assert!(matches!(outcome, Some(BlockResponseOutcome::Request(pid, _)) if pid == peer_id));
2214        assert!(rx.try_recv().is_ok());
2215    }
2216
2217    #[tokio::test]
2218    async fn test_queued_snap_request_rejected_after_last_peer_disconnects() {
2219        use futures::task::noop_waker;
2220        use std::task::{Context, Poll};
2221
2222        let manager = PeersManager::new(PeersConfig::default());
2223        let mut fetcher =
2224            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2225
2226        // The only connected peer supports snap but is busy, so the request gets queued.
2227        let peer = B512::random();
2228        fetcher.new_active_peer(NewPeerInfo {
2229            peer_id: peer,
2230            best_hash: B256::random(),
2231            best_number: 100,
2232            capabilities: Arc::new(Capabilities::new(vec![])),
2233            timeout: Arc::new(AtomicU64::new(10)),
2234            range_info: None,
2235            supports_snap: true,
2236        });
2237        fetcher.peers.get_mut(&peer).expect("peer exists").state = PeerState::GetBlockHeaders;
2238
2239        let (tx, rx) = oneshot::channel();
2240        fetcher
2241            .download_requests_tx
2242            .send(DownloadRequest::GetSnap {
2243                request: SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
2244                    request_id: 0,
2245                    root_hash: B256::ZERO,
2246                    starting_hash: B256::ZERO,
2247                    limit_hash: B256::ZERO,
2248                    response_bytes: 0,
2249                }),
2250                response: tx,
2251                priority: Priority::Normal,
2252            })
2253            .unwrap();
2254
2255        let waker = noop_waker();
2256        let mut cx = Context::from_waker(&waker);
2257
2258        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2259        assert_eq!(fetcher.queued_requests.len(), 1);
2260
2261        // The only peer disconnects, leaving `self.peers` empty. The still-queued request must
2262        // resolve immediately instead of waiting for a peer that can never come back.
2263        fetcher.on_session_closed(&peer);
2264
2265        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2266        assert!(fetcher.queued_requests.is_empty());
2267        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
2268    }
2269
2270    #[tokio::test]
2271    async fn unavailable_snap_responses_yield_to_another_peer() {
2272        let responses = [
2273            SnapResponse::BlockAccessLists(BlockAccessListsMessage {
2274                request_id: 1,
2275                block_access_lists: BlockAccessLists(vec![None, None]),
2276            }),
2277            SnapResponse::AccountRange(AccountRangeMessage {
2278                request_id: 1,
2279                accounts: vec![],
2280                proof: vec![],
2281            }),
2282            SnapResponse::StorageRanges(StorageRangesMessage {
2283                request_id: 1,
2284                slots: vec![],
2285                proof: vec![],
2286            }),
2287            SnapResponse::ByteCodes(ByteCodesMessage { request_id: 1, codes: vec![] }),
2288            SnapResponse::BlockAccessLists(BlockAccessListsMessage {
2289                request_id: 1,
2290                block_access_lists: BlockAccessLists(vec![]),
2291            }),
2292            SnapResponse::BlockAccessLists(BlockAccessListsMessage {
2293                request_id: 1,
2294                block_access_lists: BlockAccessLists(vec![
2295                    None,
2296                    Some(Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE])),
2297                ]),
2298            }),
2299        ];
2300        for response in responses {
2301            let manager = PeersManager::new(PeersConfig::default());
2302            let mut fetcher =
2303                StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2304            let fast = B512::random();
2305            let other = B512::random();
2306            for (peer_id, timeout) in [(fast, 10), (other, 50)] {
2307                fetcher.new_active_peer(NewPeerInfo {
2308                    peer_id,
2309                    best_hash: B256::random(),
2310                    best_number: 100,
2311                    capabilities: Arc::new(Capabilities::new(vec![])),
2312                    timeout: Arc::new(AtomicU64::new(timeout)),
2313                    range_info: None,
2314                    supports_snap: true,
2315                });
2316            }
2317            assert_eq!(fetcher.next_best_peer(BestPeerRequirements::SupportsSnap), Some(fast));
2318            let (tx, rx) = oneshot::channel();
2319            fetcher.inflight_snap_requests.insert(fast, Request { request: (), response: tx });
2320            fetcher.peers.get_mut(&fast).unwrap().state = PeerState::GetSnap;
2321            let (followup_tx, _followup_rx) = oneshot::channel();
2322            fetcher.queued_requests.push_back(DownloadRequest::GetSnap {
2323                request: SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
2324                    request_id: 2,
2325                    root_hash: B256::random(),
2326                    starting_hash: B256::ZERO,
2327                    limit_hash: B256::repeat_byte(0xff),
2328                    response_bytes: 1024,
2329                }),
2330                response: followup_tx,
2331                priority: Priority::Normal,
2332            });
2333
2334            assert_eq!(fetcher.on_snap_response(fast, Ok(response.clone())), None);
2335            assert_eq!(rx.await.unwrap().unwrap().1, response);
2336            assert_eq!(
2337                fetcher.next_best_peer(BestPeerRequirements::SupportsSnap),
2338                Some(other),
2339                "an unavailable response must not monopolize requests: {response:?}"
2340            );
2341            assert!(matches!(
2342                fetcher.poll_action(),
2343                PollAction::Ready(FetchAction::BlockRequest { peer_id, .. }) if peer_id == other
2344            ));
2345        }
2346    }
2347}