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(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        let is_likely_bad_response = res.is_err();
489
490        if let Some(resp) = self.inflight_snap_requests.remove(&peer_id) {
491            let _ = resp.response.send(res.map(|r| (peer_id, r).into()));
492        }
493        if let Some(peer) = self.peers.get_mut(&peer_id) {
494            peer.last_response_likely_bad = is_likely_bad_response;
495
496            if peer.state.on_request_finished() && !is_likely_bad_response {
497                return self.followup_request(peer_id)
498            }
499        }
500        None
501    }
502
503    /// Returns a new [`FetchClient`] that can send requests to this type.
504    pub(crate) fn client(&self) -> FetchClient<N> {
505        FetchClient {
506            request_tx: self.download_requests_tx.clone(),
507            peers_handle: self.peers_handle.clone(),
508            num_active_peers: Arc::clone(&self.num_active_peers),
509        }
510    }
511}
512
513/// The outcome of [`StateFetcher::poll_action`]
514enum PollAction {
515    Ready(FetchAction),
516    NoRequests,
517    NoPeersAvailable,
518}
519
520/// Everything [`StateFetcher::new_active_peer`] needs to register a newly connected peer.
521#[derive(Debug)]
522pub(crate) struct NewPeerInfo {
523    /// The remote peer's identifier.
524    pub(crate) peer_id: PeerId,
525    /// Best known hash that the peer has.
526    pub(crate) best_hash: B256,
527    /// The best block number of the peer.
528    pub(crate) best_number: u64,
529    /// Capabilities announced by the peer.
530    pub(crate) capabilities: Arc<Capabilities>,
531    /// The current timeout value to use for the peer.
532    pub(crate) timeout: Arc<AtomicU64>,
533    /// The range info for the peer.
534    pub(crate) range_info: Option<BlockRangeInfo>,
535    /// Whether the connection negotiated `snap/2` and can serve [`DownloadRequest::GetSnap`].
536    pub(crate) supports_snap: bool,
537}
538
539/// Represents a connected peer
540#[derive(Debug)]
541struct Peer {
542    /// The state this peer currently resides in.
543    state: PeerState,
544    /// Best known hash that the peer has
545    best_hash: B256,
546    /// Tracks the best number of the peer.
547    best_number: u64,
548    /// Capabilities announced by the peer.
549    #[allow(dead_code)]
550    capabilities: Arc<Capabilities>,
551    /// Tracks the current timeout value we use for the peer.
552    timeout: Arc<AtomicU64>,
553    /// Tracks whether the peer has recently responded with a likely bad response.
554    ///
555    /// This is used to de-rank the peer if there are other peers available.
556    /// This exists because empty responses may not be penalized (e.g. when blocks near the tip are
557    /// downloaded), but we still want to avoid requesting from the same peer again if it has the
558    /// lowest timeout.
559    last_response_likely_bad: bool,
560    /// Tracks the range info for the peer.
561    range_info: Option<BlockRangeInfo>,
562    /// Whether the connection negotiated `snap/2` and can serve [`DownloadRequest::GetSnap`].
563    supports_snap: bool,
564}
565
566impl Peer {
567    fn timeout(&self) -> u64 {
568        self.timeout.load(Ordering::Relaxed)
569    }
570
571    /// Returns the earliest block number available from the peer.
572    fn earliest(&self) -> u64 {
573        self.range_info.as_ref().map_or(0, |info| info.earliest())
574    }
575
576    /// Returns true if the peer has the full history available.
577    fn has_full_history(&self) -> bool {
578        self.earliest() == 0
579    }
580
581    fn range(&self) -> Option<RangeInclusive<u64>> {
582        self.range_info.as_ref().map(|info| info.range())
583    }
584
585    /// Returns whether this peer can serve requests with the given hard requirements.
586    fn satisfies(&self, requirement: &BestPeerRequirements) -> bool {
587        match requirement {
588            BestPeerRequirements::EthVersion(ver) => self.capabilities.supports_eth_at_least(ver),
589            BestPeerRequirements::SupportsSnap => self.supports_snap,
590            BestPeerRequirements::None |
591            BestPeerRequirements::FullBlock |
592            BestPeerRequirements::FullBlockRange(_) => true,
593        }
594    }
595
596    /// Returns true if this peer has a better range than the other peer for serving the requested
597    /// range.
598    ///
599    /// A peer has a "better range" if:
600    /// 1. It can fully cover the requested range while the other cannot
601    /// 2. None can fully cover the range, but this peer has lower start value
602    /// 3. If a peer doesn't announce a range we assume it has full history, but check the other's
603    ///    range and treat that as better if it can cover the range
604    fn has_better_range(&self, other: &Self, range: &RangeInclusive<u64>) -> bool {
605        let self_range = self.range();
606        let other_range = other.range();
607
608        match (self_range, other_range) {
609            (Some(self_r), Some(other_r)) => {
610                // Check if each peer can fully cover the requested range
611                let self_covers = self_r.contains(range.start()) && self_r.contains(range.end());
612                let other_covers = other_r.contains(range.start()) && other_r.contains(range.end());
613
614                #[expect(clippy::match_same_arms)]
615                match (self_covers, other_covers) {
616                    (true, false) => true,  // Only self covers the range
617                    (false, true) => false, // Only other covers the range
618                    (true, true) => false,  // Both cover
619                    (false, false) => {
620                        // neither covers - prefer if peer has lower (better) start range
621                        self_r.start() < other_r.start()
622                    }
623                }
624            }
625            (Some(self_r), None) => {
626                // Self has range info, other doesn't (treated as full history with unknown latest)
627                // Self is better only if it covers the range
628                self_r.contains(range.start()) && self_r.contains(range.end())
629            }
630            (None, Some(other_r)) => {
631                // Self has no range info (full history), other has range info
632                // Self is better only if other doesn't cover the range
633                !(other_r.contains(range.start()) && other_r.contains(range.end()))
634            }
635            (None, None) => false, // Neither has range info - no one is better
636        }
637    }
638
639    /// Returns true if this peer is better than the other peer based on the given requirements.
640    fn is_better(&self, other: &Self, requirement: &BestPeerRequirements) -> bool {
641        match requirement {
642            BestPeerRequirements::FullBlockRange(range) => self.has_better_range(other, range),
643            BestPeerRequirements::FullBlock => self.has_full_history() && !other.has_full_history(),
644            // Version/capability-based filtering happens in `next_best_peer`, so by the time we
645            // get here both peers already satisfy the requirement.
646            BestPeerRequirements::None |
647            BestPeerRequirements::EthVersion(_) |
648            BestPeerRequirements::SupportsSnap => false,
649        }
650    }
651}
652
653/// Tracks the state of an individual peer
654#[derive(Debug)]
655enum PeerState {
656    /// Peer is currently not handling requests and is available.
657    Idle,
658    /// Peer is handling a `GetBlockHeaders` request.
659    GetBlockHeaders,
660    /// Peer is handling a `GetBlockBodies` request.
661    GetBlockBodies,
662    /// Peer is handling a `GetBlockAccessLists` request.
663    GetBlockAccessLists,
664    /// Peer is handling a `GetReceipts` request.
665    GetReceipts,
666    /// Peer is handling a `snap/2` request.
667    GetSnap,
668    /// Peer session is about to close
669    Closing,
670}
671
672// === impl PeerState ===
673
674impl PeerState {
675    /// Returns true if the peer is currently idle.
676    const fn is_idle(&self) -> bool {
677        matches!(self, Self::Idle)
678    }
679
680    /// Resets the state on a received response.
681    ///
682    /// If the state was already marked as `Closing` do nothing.
683    ///
684    /// Returns `true` if the peer is ready for another request.
685    const fn on_request_finished(&mut self) -> bool {
686        if !matches!(self, Self::Closing) {
687            *self = Self::Idle;
688            return true
689        }
690        false
691    }
692}
693
694/// A request that waits for a response from the network, so it can send it back through the
695/// response channel.
696#[derive(Debug)]
697struct Request<Req, Resp> {
698    /// The issued request object
699    // TODO: this can be attached to the response in error case
700    request: Req,
701    response: oneshot::Sender<Resp>,
702}
703
704/// Requests that can be sent to the Syncer from a [`FetchClient`]
705#[derive(Debug)]
706#[expect(clippy::enum_variant_names)]
707pub(crate) enum DownloadRequest<N: NetworkPrimitives> {
708    /// Download the requested headers and send response through channel
709    GetBlockHeaders {
710        request: HeadersRequest,
711        response: oneshot::Sender<PeerRequestResult<Vec<N::BlockHeader>>>,
712        priority: Priority,
713    },
714    /// Download the requested bodies and send response through channel
715    GetBlockBodies {
716        request: Vec<B256>,
717        response: oneshot::Sender<PeerRequestResult<Vec<N::BlockBody>>>,
718        priority: Priority,
719        range_hint: Option<RangeInclusive<u64>>,
720    },
721    /// Download the requested access lists and send response through channel
722    GetBlockAccessLists {
723        request: Vec<B256>,
724        response: oneshot::Sender<PeerRequestResult<BlockAccessLists>>,
725        priority: Priority,
726        requirement: BalRequirement,
727    },
728    /// Download receipts for the given block hashes and send response through channel
729    GetReceipts {
730        request: Vec<B256>,
731        response: oneshot::Sender<PeerRequestResult<ReceiptsResponse<N::Receipt>>>,
732        priority: Priority,
733    },
734    /// Send a `snap/2` request and send response through channel
735    GetSnap {
736        request: SnapProtocolMessage,
737        response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
738        priority: Priority,
739    },
740}
741
742// === impl DownloadRequest ===
743
744impl<N: NetworkPrimitives> DownloadRequest<N> {
745    /// Returns the corresponding state for a peer that handles the request.
746    const fn peer_state(&self) -> PeerState {
747        match self {
748            Self::GetBlockHeaders { .. } => PeerState::GetBlockHeaders,
749            Self::GetBlockBodies { .. } => PeerState::GetBlockBodies,
750            Self::GetBlockAccessLists { .. } => PeerState::GetBlockAccessLists,
751            Self::GetReceipts { .. } => PeerState::GetReceipts,
752            Self::GetSnap { .. } => PeerState::GetSnap,
753        }
754    }
755
756    /// Returns the requested priority of this request
757    const fn get_priority(&self) -> &Priority {
758        match self {
759            Self::GetBlockHeaders { priority, .. } |
760            Self::GetBlockBodies { priority, .. } |
761            Self::GetBlockAccessLists { priority, .. } |
762            Self::GetReceipts { priority, .. } |
763            Self::GetSnap { priority, .. } => priority,
764        }
765    }
766
767    /// Returns `true` if this request is normal priority.
768    const fn is_normal_priority(&self) -> bool {
769        self.get_priority().is_normal()
770    }
771
772    /// Returns `true` if this is an optional BAL request.
773    const fn is_optional_bal(&self) -> bool {
774        matches!(self, Self::GetBlockAccessLists { requirement: BalRequirement::Optional, .. })
775    }
776
777    /// Returns `true` if this is a `snap/2` request.
778    const fn is_snap(&self) -> bool {
779        matches!(self, Self::GetSnap { .. })
780    }
781
782    /// Sends an error response to the waiting caller.
783    fn send_err_response(self, err: RequestError) {
784        let _ = match self {
785            Self::GetBlockHeaders { response, .. } => response.send(Err(err)).ok(),
786            Self::GetBlockBodies { response, .. } => response.send(Err(err)).ok(),
787            Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
788            Self::GetReceipts { response, .. } => response.send(Err(err)).ok(),
789            Self::GetSnap { response, .. } => response.send(Err(err)).ok(),
790        };
791    }
792
793    /// Returns the best peer requirements for this request.
794    fn best_peer_requirements(&self) -> BestPeerRequirements {
795        match self {
796            Self::GetBlockHeaders { .. } => BestPeerRequirements::None,
797            Self::GetBlockAccessLists { .. } => BestPeerRequirements::EthVersion(EthVersion::Eth71),
798            Self::GetBlockBodies { range_hint, .. } => {
799                if let Some(range) = range_hint {
800                    BestPeerRequirements::FullBlockRange(range.clone())
801                } else {
802                    BestPeerRequirements::FullBlock
803                }
804            }
805            Self::GetReceipts { .. } => BestPeerRequirements::FullBlock,
806            Self::GetSnap { .. } => BestPeerRequirements::SupportsSnap,
807        }
808    }
809}
810
811/// An action the syncer can emit.
812pub(crate) enum FetchAction {
813    /// Dispatch an eth request to the given peer.
814    BlockRequest {
815        /// The targeted recipient for the request
816        peer_id: PeerId,
817        /// The request to send
818        request: BlockRequest,
819    },
820}
821
822/// Outcome of a processed response.
823///
824/// Returned after processing a response.
825#[derive(Debug, PartialEq, Eq)]
826pub(crate) enum BlockResponseOutcome {
827    /// Continue with another request to the peer.
828    Request(PeerId, BlockRequest),
829    /// How to handle a bad response and the reputation change to apply, if any.
830    BadResponse(PeerId, ReputationChangeKind),
831}
832
833/// Additional requirements for how to rank peers during selection.
834enum BestPeerRequirements {
835    /// No additional requirements
836    None,
837    /// Peer must have this block range available.
838    FullBlockRange(RangeInclusive<u64>),
839    /// Peer must have full range.
840    FullBlock,
841    /// Peer must support at least this eth protocol version.
842    EthVersion(EthVersion),
843    /// Peer must have negotiated `snap/2`.
844    SupportsSnap,
845}
846
847#[cfg(test)]
848mod tests {
849    use super::*;
850    use crate::{peers::PeersManager, PeersConfig};
851    use alloy_consensus::Header;
852    use alloy_primitives::B512;
853    use reth_eth_wire::Capability;
854    use reth_eth_wire_types::snap::{AccountRangeMessage, GetAccountRangeMessage};
855    use std::future::poll_fn;
856
857    #[tokio::test(flavor = "multi_thread")]
858    async fn test_poll_fetcher() {
859        let manager = PeersManager::new(PeersConfig::default());
860        let mut fetcher =
861            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
862
863        poll_fn(move |cx| {
864            assert!(fetcher.poll(cx).is_pending());
865            let (tx, _rx) = oneshot::channel();
866            fetcher.queued_requests.push_back(DownloadRequest::GetBlockBodies {
867                request: vec![],
868                response: tx,
869                priority: Priority::default(),
870                range_hint: None,
871            });
872            assert!(fetcher.poll(cx).is_pending());
873
874            Poll::Ready(())
875        })
876        .await;
877    }
878
879    #[tokio::test]
880    async fn test_peer_rotation() {
881        let manager = PeersManager::new(PeersConfig::default());
882        let mut fetcher =
883            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
884        // Add a few random peers
885        let peer1 = B512::random();
886        let peer2 = B512::random();
887        let capabilities = Arc::new(Capabilities::from(vec![]));
888        fetcher.new_active_peer(NewPeerInfo {
889            peer_id: peer1,
890            best_hash: B256::random(),
891            best_number: 1,
892            capabilities: Arc::clone(&capabilities),
893            timeout: Arc::new(AtomicU64::new(1)),
894            range_info: None,
895            supports_snap: false,
896        });
897        fetcher.new_active_peer(NewPeerInfo {
898            peer_id: peer2,
899            best_hash: B256::random(),
900            best_number: 2,
901            capabilities: Arc::clone(&capabilities),
902            timeout: Arc::new(AtomicU64::new(1)),
903            range_info: None,
904            supports_snap: false,
905        });
906
907        let first_peer = fetcher.next_best_peer(BestPeerRequirements::None).unwrap();
908        assert!(first_peer == peer1 || first_peer == peer2);
909        // Pending disconnect for first_peer
910        fetcher.on_pending_disconnect(&first_peer);
911        // first_peer now isn't idle, so we should get other peer
912        let second_peer = fetcher.next_best_peer(BestPeerRequirements::None).unwrap();
913        assert!(first_peer == peer1 || first_peer == peer2);
914        assert_ne!(first_peer, second_peer);
915        // without idle peers, returns None
916        fetcher.on_pending_disconnect(&second_peer);
917        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), None);
918    }
919
920    #[tokio::test]
921    async fn test_peer_prioritization() {
922        let manager = PeersManager::new(PeersConfig::default());
923        let mut fetcher =
924            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
925        // Add a few random peers
926        let peer1 = B512::random();
927        let peer2 = B512::random();
928        let peer3 = B512::random();
929
930        let peer2_timeout = Arc::new(AtomicU64::new(300));
931
932        let capabilities = Arc::new(Capabilities::from(vec![]));
933        fetcher.new_active_peer(NewPeerInfo {
934            peer_id: peer1,
935            best_hash: B256::random(),
936            best_number: 1,
937            capabilities: Arc::clone(&capabilities),
938            timeout: Arc::new(AtomicU64::new(30)),
939            range_info: None,
940            supports_snap: false,
941        });
942        fetcher.new_active_peer(NewPeerInfo {
943            peer_id: peer2,
944            best_hash: B256::random(),
945            best_number: 2,
946            capabilities: Arc::clone(&capabilities),
947            timeout: Arc::clone(&peer2_timeout),
948            range_info: None,
949            supports_snap: false,
950        });
951        fetcher.new_active_peer(NewPeerInfo {
952            peer_id: peer3,
953            best_hash: B256::random(),
954            best_number: 3,
955            capabilities: Arc::clone(&capabilities),
956            timeout: Arc::new(AtomicU64::new(50)),
957            range_info: None,
958            supports_snap: false,
959        });
960
961        // Must always get peer1 (lowest timeout)
962        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer1));
963        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer1));
964        // peer2's timeout changes below peer1's
965        peer2_timeout.store(10, Ordering::Relaxed);
966        // Then we get peer 2 always (now lowest)
967        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer2));
968        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::None), Some(peer2));
969    }
970
971    #[tokio::test]
972    async fn test_on_block_headers_response() {
973        let manager = PeersManager::new(PeersConfig::default());
974        let mut fetcher =
975            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
976        let peer_id = B512::random();
977
978        assert_eq!(fetcher.on_block_headers_response(peer_id, Ok(vec![Header::default()])), None);
979
980        assert_eq!(
981            fetcher.on_block_headers_response(peer_id, Err(RequestError::Timeout)),
982            Some(BlockResponseOutcome::BadResponse(peer_id, ReputationChangeKind::Timeout))
983        );
984        assert_eq!(
985            fetcher.on_block_headers_response(peer_id, Err(RequestError::BadResponse)),
986            None
987        );
988        assert_eq!(
989            fetcher.on_block_headers_response(peer_id, Err(RequestError::ChannelClosed)),
990            None
991        );
992        assert_eq!(
993            fetcher.on_block_headers_response(peer_id, Err(RequestError::ConnectionDropped)),
994            None
995        );
996        assert_eq!(
997            fetcher.on_block_headers_response(peer_id, Err(RequestError::UnsupportedCapability)),
998            None
999        );
1000    }
1001
1002    #[tokio::test]
1003    async fn test_header_response_outcome() {
1004        let manager = PeersManager::new(PeersConfig::default());
1005        let mut fetcher =
1006            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1007        let peer_id = B512::random();
1008
1009        let request_pair = || {
1010            let (tx, _rx) = oneshot::channel();
1011            let req = Request {
1012                request: HeadersRequest {
1013                    start: 0u64.into(),
1014                    limit: 1,
1015                    direction: Default::default(),
1016                },
1017                response: tx,
1018            };
1019            let header = Header { number: 0, ..Default::default() };
1020            (req, header)
1021        };
1022
1023        fetcher.new_active_peer(NewPeerInfo {
1024            peer_id,
1025            best_hash: Default::default(),
1026            best_number: Default::default(),
1027            capabilities: Arc::new(Capabilities::from(vec![])),
1028            timeout: Default::default(),
1029            range_info: None,
1030            supports_snap: false,
1031        });
1032
1033        let (req, header) = request_pair();
1034        fetcher.inflight_headers_requests.insert(peer_id, req);
1035
1036        let outcome = fetcher.on_block_headers_response(peer_id, Ok(vec![header]));
1037        assert!(outcome.is_none());
1038        assert!(fetcher.peers[&peer_id].state.is_idle());
1039
1040        let outcome =
1041            fetcher.on_block_headers_response(peer_id, Err(RequestError::Timeout)).unwrap();
1042
1043        assert!(EthResponseValidator::reputation_change_err(&Err::<Vec<Header>, _>(
1044            RequestError::Timeout
1045        ))
1046        .is_some());
1047
1048        match outcome {
1049            BlockResponseOutcome::BadResponse(peer, _) => {
1050                assert_eq!(peer, peer_id)
1051            }
1052            BlockResponseOutcome::Request(_, _) => {
1053                unreachable!()
1054            }
1055        };
1056
1057        assert!(fetcher.peers[&peer_id].state.is_idle());
1058    }
1059
1060    #[test]
1061    fn test_peer_is_better_none_requirement() {
1062        let peer1 = Peer {
1063            state: PeerState::Idle,
1064            best_hash: B256::random(),
1065            best_number: 100,
1066            capabilities: Arc::new(Capabilities::new(vec![])),
1067            timeout: Arc::new(AtomicU64::new(10)),
1068            last_response_likely_bad: false,
1069            range_info: Some(BlockRangeInfo::new(0, 100, B256::random())),
1070            supports_snap: false,
1071        };
1072
1073        let peer2 = Peer {
1074            state: PeerState::Idle,
1075            best_hash: B256::random(),
1076            best_number: 50,
1077            capabilities: Arc::new(Capabilities::new(vec![])),
1078            timeout: Arc::new(AtomicU64::new(20)),
1079            last_response_likely_bad: false,
1080            range_info: None,
1081            supports_snap: false,
1082        };
1083
1084        // With None requirement, is_better should always return false
1085        assert!(!peer1.is_better(&peer2, &BestPeerRequirements::None));
1086        assert!(!peer2.is_better(&peer1, &BestPeerRequirements::None));
1087    }
1088
1089    #[test]
1090    fn test_peer_is_better_full_block_requirement() {
1091        // Peer with full history (earliest = 0)
1092        let peer_full = Peer {
1093            state: PeerState::Idle,
1094            best_hash: B256::random(),
1095            best_number: 100,
1096            capabilities: Arc::new(Capabilities::new(vec![])),
1097            timeout: Arc::new(AtomicU64::new(10)),
1098            last_response_likely_bad: false,
1099            range_info: Some(BlockRangeInfo::new(0, 100, B256::random())),
1100            supports_snap: false,
1101        };
1102
1103        // Peer without full history (earliest = 50)
1104        let peer_partial = Peer {
1105            state: PeerState::Idle,
1106            best_hash: B256::random(),
1107            best_number: 100,
1108            capabilities: Arc::new(Capabilities::new(vec![])),
1109            timeout: Arc::new(AtomicU64::new(10)),
1110            last_response_likely_bad: false,
1111            range_info: Some(BlockRangeInfo::new(50, 100, B256::random())),
1112            supports_snap: false,
1113        };
1114
1115        // Peer without range info (treated as full history)
1116        let peer_no_range = 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: None,
1124            supports_snap: false,
1125        };
1126
1127        // Peer with full history is better than peer without
1128        assert!(peer_full.is_better(&peer_partial, &BestPeerRequirements::FullBlock));
1129        assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlock));
1130
1131        // Peer without range info (full history) is better than partial
1132        assert!(peer_no_range.is_better(&peer_partial, &BestPeerRequirements::FullBlock));
1133        assert!(!peer_partial.is_better(&peer_no_range, &BestPeerRequirements::FullBlock));
1134
1135        // Both have full history - no improvement
1136        assert!(!peer_full.is_better(&peer_no_range, &BestPeerRequirements::FullBlock));
1137        assert!(!peer_no_range.is_better(&peer_full, &BestPeerRequirements::FullBlock));
1138    }
1139
1140    #[test]
1141    fn test_peer_is_better_full_block_range_requirement() {
1142        let range = RangeInclusive::new(40, 60);
1143
1144        // Peer that covers the requested range
1145        let peer_covers = Peer {
1146            state: PeerState::Idle,
1147            best_hash: B256::random(),
1148            best_number: 100,
1149            capabilities: Arc::new(Capabilities::new(vec![])),
1150            timeout: Arc::new(AtomicU64::new(10)),
1151            last_response_likely_bad: false,
1152            range_info: Some(BlockRangeInfo::new(0, 100, B256::random())),
1153            supports_snap: false,
1154        };
1155
1156        // Peer that doesn't cover the range (earliest too high)
1157        let peer_no_cover = Peer {
1158            state: PeerState::Idle,
1159            best_hash: B256::random(),
1160            best_number: 100,
1161            capabilities: Arc::new(Capabilities::new(vec![])),
1162            timeout: Arc::new(AtomicU64::new(10)),
1163            last_response_likely_bad: false,
1164            range_info: Some(BlockRangeInfo::new(70, 100, B256::random())),
1165            supports_snap: false,
1166        };
1167
1168        // Peer that covers the requested range is better than one that doesn't
1169        assert!(peer_covers
1170            .is_better(&peer_no_cover, &BestPeerRequirements::FullBlockRange(range.clone())));
1171        assert!(
1172            !peer_no_cover.is_better(&peer_covers, &BestPeerRequirements::FullBlockRange(range))
1173        );
1174    }
1175
1176    #[test]
1177    fn test_peer_is_better_both_cover_range() {
1178        let range = RangeInclusive::new(30, 50);
1179
1180        // Peer with full history that covers the range
1181        let peer_full = Peer {
1182            state: PeerState::Idle,
1183            best_hash: B256::random(),
1184            best_number: 100,
1185            capabilities: Arc::new(Capabilities::new(vec![])),
1186            timeout: Arc::new(AtomicU64::new(10)),
1187            last_response_likely_bad: false,
1188            range_info: Some(BlockRangeInfo::new(0, 50, B256::random())),
1189            supports_snap: false,
1190        };
1191
1192        // Peer without full history that also covers the range
1193        let peer_partial = Peer {
1194            state: PeerState::Idle,
1195            best_hash: B256::random(),
1196            best_number: 100,
1197            capabilities: Arc::new(Capabilities::new(vec![])),
1198            timeout: Arc::new(AtomicU64::new(10)),
1199            last_response_likely_bad: false,
1200            range_info: Some(BlockRangeInfo::new(30, 50, B256::random())),
1201            supports_snap: false,
1202        };
1203
1204        // When both cover the range, prefer none
1205        assert!(!peer_full
1206            .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone())));
1207        assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range)));
1208    }
1209
1210    #[test]
1211    fn test_peer_is_better_lower_start() {
1212        let range = RangeInclusive::new(30, 60);
1213
1214        // Peer with full history that covers the range
1215        let peer_full = Peer {
1216            state: PeerState::Idle,
1217            best_hash: B256::random(),
1218            best_number: 100,
1219            capabilities: Arc::new(Capabilities::new(vec![])),
1220            timeout: Arc::new(AtomicU64::new(10)),
1221            last_response_likely_bad: false,
1222            range_info: Some(BlockRangeInfo::new(0, 50, B256::random())),
1223            supports_snap: false,
1224        };
1225
1226        // Peer without full history that also covers the range
1227        let peer_partial = Peer {
1228            state: PeerState::Idle,
1229            best_hash: B256::random(),
1230            best_number: 100,
1231            capabilities: Arc::new(Capabilities::new(vec![])),
1232            timeout: Arc::new(AtomicU64::new(10)),
1233            last_response_likely_bad: false,
1234            range_info: Some(BlockRangeInfo::new(30, 50, B256::random())),
1235            supports_snap: false,
1236        };
1237
1238        // When both cover the range, prefer lower start value
1239        assert!(peer_full
1240            .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone())));
1241        assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range)));
1242    }
1243
1244    #[test]
1245    fn test_peer_is_better_neither_covers_range() {
1246        let range = RangeInclusive::new(40, 60);
1247
1248        // Peer with full history that doesn't cover the range (latest too low)
1249        let peer_full = Peer {
1250            state: PeerState::Idle,
1251            best_hash: B256::random(),
1252            best_number: 30,
1253            capabilities: Arc::new(Capabilities::new(vec![])),
1254            timeout: Arc::new(AtomicU64::new(10)),
1255            last_response_likely_bad: false,
1256            range_info: Some(BlockRangeInfo::new(0, 30, B256::random())),
1257            supports_snap: false,
1258        };
1259
1260        // Peer without full history that also doesn't cover the range
1261        let peer_partial = Peer {
1262            state: PeerState::Idle,
1263            best_hash: B256::random(),
1264            best_number: 30,
1265            capabilities: Arc::new(Capabilities::new(vec![])),
1266            timeout: Arc::new(AtomicU64::new(10)),
1267            last_response_likely_bad: false,
1268            range_info: Some(BlockRangeInfo::new(10, 30, B256::random())),
1269            supports_snap: false,
1270        };
1271
1272        // When neither covers the range, prefer full history
1273        assert!(peer_full
1274            .is_better(&peer_partial, &BestPeerRequirements::FullBlockRange(range.clone())));
1275        assert!(!peer_partial.is_better(&peer_full, &BestPeerRequirements::FullBlockRange(range)));
1276    }
1277
1278    #[test]
1279    fn test_peer_is_better_no_range_info() {
1280        let range = RangeInclusive::new(40, 60);
1281
1282        // Peer with range info
1283        let peer_with_range = Peer {
1284            state: PeerState::Idle,
1285            best_hash: B256::random(),
1286            best_number: 100,
1287            capabilities: Arc::new(Capabilities::new(vec![])),
1288            timeout: Arc::new(AtomicU64::new(10)),
1289            last_response_likely_bad: false,
1290            range_info: Some(BlockRangeInfo::new(30, 100, B256::random())),
1291            supports_snap: false,
1292        };
1293
1294        // Peer without range info
1295        let peer_no_range = Peer {
1296            state: PeerState::Idle,
1297            best_hash: B256::random(),
1298            best_number: 100,
1299            capabilities: Arc::new(Capabilities::new(vec![])),
1300            timeout: Arc::new(AtomicU64::new(10)),
1301            last_response_likely_bad: false,
1302            range_info: None,
1303            supports_snap: false,
1304        };
1305
1306        // Peer without range info is not better (we prefer peers with known ranges)
1307        assert!(!peer_no_range
1308            .is_better(&peer_with_range, &BestPeerRequirements::FullBlockRange(range.clone())));
1309
1310        // Peer with range info is better than peer without
1311        assert!(
1312            peer_with_range.is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range))
1313        );
1314    }
1315
1316    #[test]
1317    fn test_peer_is_better_one_peer_no_range_covers() {
1318        let range = RangeInclusive::new(40, 60);
1319
1320        // Peer with range info that covers the requested range
1321        let peer_with_range_covers = Peer {
1322            state: PeerState::Idle,
1323            best_hash: B256::random(),
1324            best_number: 100,
1325            capabilities: Arc::new(Capabilities::new(vec![])),
1326            timeout: Arc::new(AtomicU64::new(10)),
1327            last_response_likely_bad: false,
1328            range_info: Some(BlockRangeInfo::new(30, 100, B256::random())),
1329            supports_snap: false,
1330        };
1331
1332        // Peer without range info (treated as full history with unknown latest)
1333        let peer_no_range = Peer {
1334            state: PeerState::Idle,
1335            best_hash: B256::random(),
1336            best_number: 100,
1337            capabilities: Arc::new(Capabilities::new(vec![])),
1338            timeout: Arc::new(AtomicU64::new(10)),
1339            last_response_likely_bad: false,
1340            range_info: None,
1341            supports_snap: false,
1342        };
1343
1344        // Peer with range that covers is better than peer without range info
1345        assert!(peer_with_range_covers
1346            .is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range.clone())));
1347
1348        // Peer without range info is not better when other covers
1349        assert!(!peer_no_range
1350            .is_better(&peer_with_range_covers, &BestPeerRequirements::FullBlockRange(range)));
1351    }
1352
1353    #[test]
1354    fn test_peer_is_better_one_peer_no_range_doesnt_cover() {
1355        let range = RangeInclusive::new(40, 60);
1356
1357        // Peer with range info that does NOT cover the requested range (too high)
1358        let peer_with_range_no_cover = Peer {
1359            state: PeerState::Idle,
1360            best_hash: B256::random(),
1361            best_number: 100,
1362            capabilities: Arc::new(Capabilities::new(vec![])),
1363            timeout: Arc::new(AtomicU64::new(10)),
1364            last_response_likely_bad: false,
1365            range_info: Some(BlockRangeInfo::new(70, 100, B256::random())),
1366            supports_snap: false,
1367        };
1368
1369        // Peer without range info (treated as full history)
1370        let peer_no_range = Peer {
1371            state: PeerState::Idle,
1372            best_hash: B256::random(),
1373            best_number: 100,
1374            capabilities: Arc::new(Capabilities::new(vec![])),
1375            timeout: Arc::new(AtomicU64::new(10)),
1376            last_response_likely_bad: false,
1377            range_info: None,
1378            supports_snap: false,
1379        };
1380
1381        // Peer with range that doesn't cover is not better
1382        assert!(!peer_with_range_no_cover
1383            .is_better(&peer_no_range, &BestPeerRequirements::FullBlockRange(range.clone())));
1384
1385        // Peer without range info (full history) is better when other doesn't cover
1386        assert!(peer_no_range
1387            .is_better(&peer_with_range_no_cover, &BestPeerRequirements::FullBlockRange(range)));
1388    }
1389
1390    #[test]
1391    fn test_peer_is_better_edge_cases() {
1392        // Test exact range boundaries
1393        let range = RangeInclusive::new(50, 100);
1394
1395        // Peer that exactly covers the range
1396        let peer_exact = Peer {
1397            state: PeerState::Idle,
1398            best_hash: B256::random(),
1399            best_number: 100,
1400            capabilities: Arc::new(Capabilities::new(vec![])),
1401            timeout: Arc::new(AtomicU64::new(10)),
1402            last_response_likely_bad: false,
1403            range_info: Some(BlockRangeInfo::new(50, 100, B256::random())),
1404            supports_snap: false,
1405        };
1406
1407        // Peer that's one block short at the start
1408        let peer_short_start = Peer {
1409            state: PeerState::Idle,
1410            best_hash: B256::random(),
1411            best_number: 100,
1412            capabilities: Arc::new(Capabilities::new(vec![])),
1413            timeout: Arc::new(AtomicU64::new(10)),
1414            last_response_likely_bad: false,
1415            range_info: Some(BlockRangeInfo::new(51, 100, B256::random())),
1416            supports_snap: false,
1417        };
1418
1419        // Peer that's one block short at the end
1420        let peer_short_end = Peer {
1421            state: PeerState::Idle,
1422            best_hash: B256::random(),
1423            best_number: 100,
1424            capabilities: Arc::new(Capabilities::new(vec![])),
1425            timeout: Arc::new(AtomicU64::new(10)),
1426            last_response_likely_bad: false,
1427            range_info: Some(BlockRangeInfo::new(50, 99, B256::random())),
1428            supports_snap: false,
1429        };
1430
1431        // Exact coverage is better than short coverage
1432        assert!(peer_exact
1433            .is_better(&peer_short_start, &BestPeerRequirements::FullBlockRange(range.clone())));
1434        assert!(peer_exact
1435            .is_better(&peer_short_end, &BestPeerRequirements::FullBlockRange(range.clone())));
1436
1437        // Short coverage is not better than exact coverage
1438        assert!(!peer_short_start
1439            .is_better(&peer_exact, &BestPeerRequirements::FullBlockRange(range.clone())));
1440        assert!(
1441            !peer_short_end.is_better(&peer_exact, &BestPeerRequirements::FullBlockRange(range))
1442        );
1443    }
1444
1445    /// Creates a `StateFetcher` with a single idle peer and returns both.
1446    fn fetcher_with_peer() -> (StateFetcher<EthNetworkPrimitives>, PeerId) {
1447        let manager = PeersManager::new(PeersConfig::default());
1448        let mut fetcher =
1449            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1450        let peer_id = B512::random();
1451
1452        fetcher.new_active_peer(NewPeerInfo {
1453            peer_id,
1454            best_hash: Default::default(),
1455            best_number: Default::default(),
1456            capabilities: Arc::new(Capabilities::from(vec![])),
1457            timeout: Default::default(),
1458            range_info: None,
1459            supports_snap: false,
1460        });
1461        (fetcher, peer_id)
1462    }
1463
1464    /// Inserts an inflight receipts request into the fetcher and returns the
1465    /// `oneshot::Receiver` that the final response will be sent through.
1466    fn insert_inflight_receipts(
1467        fetcher: &mut StateFetcher<EthNetworkPrimitives>,
1468        peer_id: PeerId,
1469    ) -> oneshot::Receiver<PeerRequestResult<ReceiptsResponse<reth_ethereum_primitives::Receipt>>>
1470    {
1471        let (tx, rx) = oneshot::channel();
1472        fetcher.inflight_receipts_requests.insert(peer_id, Request { request: (), response: tx });
1473        fetcher.peers.get_mut(&peer_id).unwrap().state = PeerState::GetReceipts;
1474        rx
1475    }
1476
1477    // ---- Receipts: basic dispatch ----
1478
1479    #[tokio::test]
1480    async fn test_poll_dispatches_receipts_to_peer() {
1481        let (mut fetcher, peer_id) = fetcher_with_peer();
1482
1483        poll_fn(move |cx| {
1484            let (tx, _rx) = oneshot::channel();
1485            fetcher.queued_requests.push_back(DownloadRequest::GetReceipts {
1486                request: vec![B256::ZERO],
1487                response: tx,
1488                priority: Priority::default(),
1489            });
1490
1491            let Poll::Ready(FetchAction::BlockRequest { peer_id: dispatched_peer, request }) =
1492                fetcher.poll(cx)
1493            else {
1494                panic!("expected Ready(BlockRequest)");
1495            };
1496            assert_eq!(dispatched_peer, peer_id);
1497            assert!(matches!(request, BlockRequest::GetReceipts(_)));
1498
1499            // Peer should now be in GetReceipts state
1500            assert!(matches!(fetcher.peers[&peer_id].state, PeerState::GetReceipts));
1501            // Inflight request should be tracked
1502            assert!(fetcher.inflight_receipts_requests.contains_key(&peer_id));
1503
1504            Poll::Ready(())
1505        })
1506        .await;
1507    }
1508
1509    // ---- Receipts: response handling ----
1510
1511    #[tokio::test]
1512    async fn test_receipts_complete_response_resolves_and_idles_peer() {
1513        let (mut fetcher, peer_id) = fetcher_with_peer();
1514
1515        let rx = insert_inflight_receipts(&mut fetcher, peer_id);
1516
1517        let resp = ReceiptsResponse::new(vec![vec![]]);
1518        let outcome = fetcher.on_receipts_response(peer_id, Ok(resp));
1519
1520        // No queued requests, so no followup
1521        assert!(outcome.is_none());
1522        // Peer back to idle
1523        assert!(fetcher.peers[&peer_id].state.is_idle());
1524        // Inflight cleaned up
1525        assert!(!fetcher.inflight_receipts_requests.contains_key(&peer_id));
1526
1527        // Caller receives the response
1528        let result = rx.await.unwrap().unwrap();
1529        assert_eq!(result.1.receipts.len(), 1);
1530    }
1531
1532    #[tokio::test]
1533    async fn test_receipts_empty_response_marks_peer_bad() {
1534        let (mut fetcher, peer_id) = fetcher_with_peer();
1535        let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
1536
1537        let resp = ReceiptsResponse::new(vec![]);
1538        let _ = fetcher.on_receipts_response(peer_id, Ok(resp));
1539
1540        assert!(fetcher.peers[&peer_id].last_response_likely_bad);
1541    }
1542
1543    #[tokio::test]
1544    async fn test_receipts_error_forwards_and_marks_peer_bad() {
1545        let (mut fetcher, peer_id) = fetcher_with_peer();
1546        let rx = insert_inflight_receipts(&mut fetcher, peer_id);
1547
1548        let _ = fetcher.on_receipts_response(peer_id, Err(RequestError::Timeout));
1549
1550        assert!(fetcher.peers[&peer_id].last_response_likely_bad);
1551        // Error is forwarded to the caller
1552        let result = rx.await.unwrap();
1553        assert_eq!(result.unwrap_err(), RequestError::Timeout);
1554    }
1555
1556    #[tokio::test]
1557    async fn test_session_closed_cancels_inflight_receipts() {
1558        let (mut fetcher, peer_id) = fetcher_with_peer();
1559        let rx = insert_inflight_receipts(&mut fetcher, peer_id);
1560
1561        fetcher.on_session_closed(&peer_id);
1562
1563        assert!(!fetcher.peers.contains_key(&peer_id));
1564        assert!(!fetcher.inflight_receipts_requests.contains_key(&peer_id));
1565
1566        let result = rx.await.unwrap();
1567        assert_eq!(result.unwrap_err(), RequestError::ConnectionDropped);
1568    }
1569
1570    #[tokio::test]
1571    async fn test_receipts_response_triggers_followup() {
1572        let (mut fetcher, peer_id) = fetcher_with_peer();
1573
1574        // Queue a bodies request as a followup candidate
1575        let (followup_tx, _followup_rx) = oneshot::channel();
1576        fetcher.queued_requests.push_back(DownloadRequest::GetBlockBodies {
1577            request: vec![B256::random()],
1578            response: followup_tx,
1579            priority: Priority::default(),
1580            range_hint: None,
1581        });
1582
1583        let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
1584
1585        let resp = ReceiptsResponse::new(vec![vec![]]);
1586        let outcome = fetcher.on_receipts_response(peer_id, Ok(resp));
1587
1588        assert!(matches!(outcome, Some(BlockResponseOutcome::Request(pid, _)) if pid == peer_id));
1589    }
1590
1591    #[tokio::test]
1592    async fn test_followup_skips_request_peer_cannot_serve() {
1593        let (mut fetcher, peer_id) = fetcher_with_peer();
1594
1595        let peer_71 = B512::random();
1596        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1597        fetcher.new_active_peer(NewPeerInfo {
1598            peer_id: peer_71,
1599            best_hash: B256::random(),
1600            best_number: 100,
1601            capabilities: caps_71,
1602            timeout: Arc::new(AtomicU64::new(10)),
1603            range_info: None,
1604            supports_snap: false,
1605        });
1606        fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
1607
1608        let (followup_tx, _followup_rx) = oneshot::channel();
1609        fetcher.queued_requests.push_back(DownloadRequest::GetBlockAccessLists {
1610            request: vec![B256::random()],
1611            response: followup_tx,
1612            priority: Priority::Normal,
1613            requirement: BalRequirement::Optional,
1614        });
1615
1616        let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
1617
1618        let resp = ReceiptsResponse::new(vec![vec![]]);
1619        assert!(fetcher.on_receipts_response(peer_id, Ok(resp)).is_none());
1620        assert!(fetcher.peers[&peer_id].state.is_idle());
1621        assert!(!fetcher.inflight_bals_requests.contains_key(&peer_id));
1622        assert!(matches!(
1623            fetcher.queued_requests.front(),
1624            Some(DownloadRequest::GetBlockAccessLists {
1625                requirement: BalRequirement::Optional,
1626                ..
1627            })
1628        ));
1629    }
1630
1631    #[tokio::test]
1632    async fn test_followup_uses_first_satisfiable_request() {
1633        let (mut fetcher, peer_id) = fetcher_with_peer();
1634
1635        let peer_71 = B512::random();
1636        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1637        fetcher.new_active_peer(NewPeerInfo {
1638            peer_id: peer_71,
1639            best_hash: B256::random(),
1640            best_number: 100,
1641            capabilities: caps_71,
1642            timeout: Arc::new(AtomicU64::new(10)),
1643            range_info: None,
1644            supports_snap: false,
1645        });
1646        fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
1647
1648        let (bal_tx, _bal_rx) = oneshot::channel();
1649        fetcher.queued_requests.push_back(DownloadRequest::GetBlockAccessLists {
1650            request: vec![B256::random()],
1651            response: bal_tx,
1652            priority: Priority::Normal,
1653            requirement: BalRequirement::Optional,
1654        });
1655
1656        let (bodies_tx, _bodies_rx) = oneshot::channel();
1657        fetcher.queued_requests.push_back(DownloadRequest::GetBlockBodies {
1658            request: vec![B256::random()],
1659            response: bodies_tx,
1660            priority: Priority::Normal,
1661            range_hint: None,
1662        });
1663
1664        let _rx = insert_inflight_receipts(&mut fetcher, peer_id);
1665
1666        let resp = ReceiptsResponse::new(vec![vec![]]);
1667        let outcome = fetcher.on_receipts_response(peer_id, Ok(resp));
1668
1669        assert!(matches!(
1670            outcome,
1671            Some(BlockResponseOutcome::Request(pid, BlockRequest::GetBlockBodies(_))) if pid == peer_id
1672        ));
1673        assert!(fetcher.inflight_bodies_requests.contains_key(&peer_id));
1674        assert!(matches!(fetcher.peers[&peer_id].state, PeerState::GetBlockBodies));
1675        assert_eq!(fetcher.queued_requests.len(), 1);
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_prepare_block_request_creates_inflight_receipts() {
1687        let (mut fetcher, peer_id) = fetcher_with_peer();
1688        let hashes = vec![B256::with_last_byte(1), B256::with_last_byte(2)];
1689
1690        let (tx, _rx) = oneshot::channel();
1691        let req = DownloadRequest::GetReceipts {
1692            request: hashes.clone(),
1693            response: tx,
1694            priority: Priority::default(),
1695        };
1696
1697        let block_request = fetcher.prepare_block_request(peer_id, req);
1698
1699        // Returns a GetReceipts block request with the same hashes
1700        match block_request {
1701            BlockRequest::GetReceipts(ref get) => {
1702                assert_eq!(get.0, hashes);
1703            }
1704            other => panic!("expected GetReceipts, got {other:?}"),
1705        }
1706
1707        // Peer state transitions to GetReceipts
1708        assert!(matches!(fetcher.peers[&peer_id].state, PeerState::GetReceipts));
1709
1710        // Inflight request is tracked
1711        assert!(fetcher.inflight_receipts_requests.contains_key(&peer_id));
1712    }
1713    #[tokio::test]
1714    async fn test_next_best_peer_eth71_no_support() {
1715        let manager = PeersManager::new(PeersConfig::default());
1716        let mut fetcher =
1717            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1718
1719        let peer = B512::random();
1720
1721        // Capabilities WITHOUT eth71
1722        let capabilities = Arc::new(Capabilities::new(vec![]));
1723
1724        fetcher.new_active_peer(NewPeerInfo {
1725            peer_id: peer,
1726            best_hash: B256::random(),
1727            best_number: 100,
1728            capabilities,
1729            timeout: Arc::new(AtomicU64::new(10)),
1730            range_info: None,
1731            supports_snap: false,
1732        });
1733
1734        // Should return None because peer doesn't support eth71
1735        assert_eq!(
1736            fetcher.next_best_peer(BestPeerRequirements::EthVersion(EthVersion::Eth71)),
1737            None
1738        );
1739    }
1740
1741    #[tokio::test]
1742    async fn test_next_best_peer_eth71_supported() {
1743        let manager = PeersManager::new(PeersConfig::default());
1744        let mut fetcher =
1745            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1746
1747        let peer = B512::random();
1748
1749        // Build capability list that includes Eth71
1750        let capabilities = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1751
1752        fetcher.new_active_peer(NewPeerInfo {
1753            peer_id: peer,
1754            best_hash: B256::random(),
1755            best_number: 100,
1756            capabilities,
1757            timeout: Arc::new(AtomicU64::new(10)),
1758            range_info: None,
1759            supports_snap: false,
1760        });
1761
1762        assert_eq!(
1763            fetcher.next_best_peer(BestPeerRequirements::EthVersion(EthVersion::Eth71)),
1764            Some(peer)
1765        );
1766    }
1767
1768    #[tokio::test]
1769    async fn test_next_best_peer_eth71_filters_correctly() {
1770        let manager = PeersManager::new(PeersConfig::default());
1771        let mut fetcher =
1772            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1773
1774        let peer_no_71 = B512::random();
1775        let peer_with_71 = B512::random();
1776
1777        // Peer without eth71
1778        let caps_old = Arc::new(Capabilities::new(vec![]));
1779
1780        // Peer with eth71
1781        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1782
1783        fetcher.new_active_peer(NewPeerInfo {
1784            peer_id: peer_no_71,
1785            best_hash: B256::random(),
1786            best_number: 100,
1787            capabilities: caps_old,
1788            timeout: Arc::new(AtomicU64::new(5)),
1789            range_info: None,
1790            supports_snap: false,
1791        });
1792
1793        fetcher.new_active_peer(NewPeerInfo {
1794            peer_id: peer_with_71,
1795            best_hash: B256::random(),
1796            best_number: 100,
1797            capabilities: caps_71,
1798            timeout: Arc::new(AtomicU64::new(50)),
1799            range_info: None,
1800            supports_snap: false,
1801        });
1802
1803        // Even though peer_no_71 has lower timeout,
1804        // it must NOT be selected.
1805        assert_eq!(
1806            fetcher.next_best_peer(BestPeerRequirements::EthVersion(EthVersion::Eth71)),
1807            Some(peer_with_71)
1808        );
1809    }
1810
1811    #[tokio::test]
1812    async fn test_wakes_when_eth71_peer_connects() {
1813        use futures::task::noop_waker;
1814        use std::task::{Context, Poll};
1815
1816        let manager = PeersManager::new(PeersConfig::default());
1817        let mut fetcher =
1818            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1819
1820        // Queue Eth71-required request
1821        let (tx, _rx) = oneshot::channel();
1822        fetcher.queued_requests.push_back(DownloadRequest::GetBlockAccessLists {
1823            request: vec![],
1824            response: tx,
1825            priority: Priority::Normal,
1826            requirement: BalRequirement::Mandatory,
1827        });
1828
1829        let waker = noop_waker();
1830        let mut cx = Context::from_waker(&waker);
1831
1832        // No peers -> must be Pending
1833        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
1834
1835        // Add peer WITHOUT Eth71 support
1836        let peer_old = B512::random();
1837        let caps_old = Arc::new(Capabilities::new(vec![]));
1838
1839        fetcher.new_active_peer(NewPeerInfo {
1840            peer_id: peer_old,
1841            best_hash: B256::random(),
1842            best_number: 100,
1843            capabilities: caps_old,
1844            timeout: Arc::new(AtomicU64::new(10)),
1845            range_info: None,
1846            supports_snap: false,
1847        });
1848
1849        // Still Pending
1850        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
1851
1852        // Add peer WITH Eth71 support
1853        let peer_71 = B512::random();
1854        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1855
1856        fetcher.new_active_peer(NewPeerInfo {
1857            peer_id: peer_71,
1858            best_hash: B256::random(),
1859            best_number: 100,
1860            capabilities: caps_71,
1861            timeout: Arc::new(AtomicU64::new(10)),
1862            range_info: None,
1863            supports_snap: false,
1864        });
1865
1866        // Now we must get Ready(BlockRequest)
1867        if let Poll::Ready(FetchAction::BlockRequest { peer_id, .. }) = fetcher.poll(&mut cx) {
1868            assert_eq!(peer_id, peer_71);
1869        }
1870    }
1871
1872    #[tokio::test]
1873    async fn test_optional_bal_request_rejected_without_eth71_peer() {
1874        use futures::task::noop_waker;
1875        use std::task::{Context, Poll};
1876
1877        let manager = PeersManager::new(PeersConfig::default());
1878        let mut fetcher =
1879            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1880
1881        let peer_old = B512::random();
1882        let caps_old = Arc::new(Capabilities::new(vec![]));
1883        fetcher.new_active_peer(NewPeerInfo {
1884            peer_id: peer_old,
1885            best_hash: B256::random(),
1886            best_number: 100,
1887            capabilities: caps_old,
1888            timeout: Arc::new(AtomicU64::new(10)),
1889            range_info: None,
1890            supports_snap: false,
1891        });
1892
1893        let (tx, rx) = oneshot::channel();
1894        fetcher
1895            .download_requests_tx
1896            .send(DownloadRequest::GetBlockAccessLists {
1897                request: vec![],
1898                response: tx,
1899                priority: Priority::Normal,
1900                requirement: BalRequirement::Optional,
1901            })
1902            .unwrap();
1903
1904        let waker = noop_waker();
1905        let mut cx = Context::from_waker(&waker);
1906
1907        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
1908        assert!(fetcher.queued_requests.is_empty());
1909        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
1910    }
1911
1912    #[tokio::test]
1913    async fn test_optional_bal_request_waits_for_busy_eth71_peer() {
1914        use futures::task::noop_waker;
1915        use std::task::{Context, Poll};
1916
1917        let manager = PeersManager::new(PeersConfig::default());
1918        let mut fetcher =
1919            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1920
1921        let peer_71 = B512::random();
1922        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1923        fetcher.new_active_peer(NewPeerInfo {
1924            peer_id: peer_71,
1925            best_hash: B256::random(),
1926            best_number: 100,
1927            capabilities: caps_71,
1928            timeout: Arc::new(AtomicU64::new(10)),
1929            range_info: None,
1930            supports_snap: false,
1931        });
1932        fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
1933
1934        let (tx, _rx) = oneshot::channel();
1935        fetcher
1936            .download_requests_tx
1937            .send(DownloadRequest::GetBlockAccessLists {
1938                request: vec![],
1939                response: tx,
1940                priority: Priority::Normal,
1941                requirement: BalRequirement::Optional,
1942            })
1943            .unwrap();
1944
1945        let waker = noop_waker();
1946        let mut cx = Context::from_waker(&waker);
1947
1948        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
1949        assert_eq!(fetcher.queued_requests.len(), 1);
1950    }
1951
1952    #[tokio::test]
1953    async fn test_queued_optional_bal_request_rejected_after_eth71_disconnect() {
1954        use futures::task::noop_waker;
1955        use std::task::{Context, Poll};
1956
1957        let manager = PeersManager::new(PeersConfig::default());
1958        let mut fetcher =
1959            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
1960
1961        let peer_old = B512::random();
1962        let caps_old = Arc::new(Capabilities::new(vec![]));
1963        fetcher.new_active_peer(NewPeerInfo {
1964            peer_id: peer_old,
1965            best_hash: B256::random(),
1966            best_number: 100,
1967            capabilities: caps_old,
1968            timeout: Arc::new(AtomicU64::new(10)),
1969            range_info: None,
1970            supports_snap: false,
1971        });
1972
1973        let peer_71 = B512::random();
1974        let caps_71 = Arc::new(Capabilities::from(vec![Capability::new("eth".into(), 71)]));
1975        fetcher.new_active_peer(NewPeerInfo {
1976            peer_id: peer_71,
1977            best_hash: B256::random(),
1978            best_number: 100,
1979            capabilities: caps_71,
1980            timeout: Arc::new(AtomicU64::new(10)),
1981            range_info: None,
1982            supports_snap: false,
1983        });
1984        fetcher.peers.get_mut(&peer_71).expect("peer exists").state = PeerState::GetBlockHeaders;
1985
1986        let (tx, rx) = oneshot::channel();
1987        fetcher
1988            .download_requests_tx
1989            .send(DownloadRequest::GetBlockAccessLists {
1990                request: vec![],
1991                response: tx,
1992                priority: Priority::Normal,
1993                requirement: BalRequirement::Optional,
1994            })
1995            .unwrap();
1996
1997        let waker = noop_waker();
1998        let mut cx = Context::from_waker(&waker);
1999
2000        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2001        assert_eq!(fetcher.queued_requests.len(), 1);
2002
2003        fetcher.on_session_closed(&peer_71);
2004
2005        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2006        assert!(fetcher.queued_requests.is_empty());
2007        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
2008    }
2009
2010    #[tokio::test]
2011    async fn test_next_best_peer_snap_no_support() {
2012        let manager = PeersManager::new(PeersConfig::default());
2013        let mut fetcher =
2014            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2015
2016        let peer = B512::random();
2017        fetcher.new_active_peer(NewPeerInfo {
2018            peer_id: peer,
2019            best_hash: B256::random(),
2020            best_number: 100,
2021            capabilities: Arc::new(Capabilities::new(vec![])),
2022            timeout: Arc::new(AtomicU64::new(10)),
2023            range_info: None,
2024            supports_snap: false,
2025        });
2026
2027        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::SupportsSnap), None);
2028    }
2029
2030    #[tokio::test]
2031    async fn test_next_best_peer_snap_supported() {
2032        let manager = PeersManager::new(PeersConfig::default());
2033        let mut fetcher =
2034            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2035
2036        let peer = B512::random();
2037        fetcher.new_active_peer(NewPeerInfo {
2038            peer_id: peer,
2039            best_hash: B256::random(),
2040            best_number: 100,
2041            capabilities: Arc::new(Capabilities::new(vec![])),
2042            timeout: Arc::new(AtomicU64::new(10)),
2043            range_info: None,
2044            supports_snap: true,
2045        });
2046
2047        assert_eq!(fetcher.next_best_peer(BestPeerRequirements::SupportsSnap), Some(peer));
2048    }
2049
2050    #[tokio::test]
2051    async fn test_next_best_peer_snap_filters_correctly() {
2052        let manager = PeersManager::new(PeersConfig::default());
2053        let mut fetcher =
2054            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2055
2056        let peer_no_snap = B512::random();
2057        let peer_with_snap = B512::random();
2058
2059        fetcher.new_active_peer(NewPeerInfo {
2060            peer_id: peer_no_snap,
2061            best_hash: B256::random(),
2062            best_number: 100,
2063            capabilities: Arc::new(Capabilities::new(vec![])),
2064            timeout: Arc::new(AtomicU64::new(5)),
2065            range_info: None,
2066            supports_snap: false,
2067        });
2068        fetcher.new_active_peer(NewPeerInfo {
2069            peer_id: peer_with_snap,
2070            best_hash: B256::random(),
2071            best_number: 100,
2072            capabilities: Arc::new(Capabilities::new(vec![])),
2073            timeout: Arc::new(AtomicU64::new(50)),
2074            range_info: None,
2075            supports_snap: true,
2076        });
2077
2078        // Even though peer_no_snap has a lower timeout, it must NOT be selected.
2079        assert_eq!(
2080            fetcher.next_best_peer(BestPeerRequirements::SupportsSnap),
2081            Some(peer_with_snap)
2082        );
2083    }
2084
2085    #[tokio::test]
2086    async fn test_snap_request_rejected_without_snap_peer() {
2087        use futures::task::noop_waker;
2088        use std::task::{Context, Poll};
2089
2090        let manager = PeersManager::new(PeersConfig::default());
2091        let mut fetcher =
2092            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2093
2094        // Only an eth-only peer is connected.
2095        fetcher.new_active_peer(NewPeerInfo {
2096            peer_id: B512::random(),
2097            best_hash: B256::random(),
2098            best_number: 100,
2099            capabilities: Arc::new(Capabilities::new(vec![])),
2100            timeout: Arc::new(AtomicU64::new(10)),
2101            range_info: None,
2102            supports_snap: false,
2103        });
2104
2105        let (tx, rx) = oneshot::channel();
2106        fetcher
2107            .download_requests_tx
2108            .send(DownloadRequest::GetSnap {
2109                request: SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
2110                    request_id: 0,
2111                    root_hash: B256::ZERO,
2112                    starting_hash: B256::ZERO,
2113                    limit_hash: B256::ZERO,
2114                    response_bytes: 0,
2115                }),
2116                response: tx,
2117                priority: Priority::Normal,
2118            })
2119            .unwrap();
2120
2121        let waker = noop_waker();
2122        let mut cx = Context::from_waker(&waker);
2123
2124        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2125        assert!(fetcher.queued_requests.is_empty());
2126        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
2127    }
2128
2129    #[tokio::test]
2130    async fn test_snap_response_triggers_followup() {
2131        let manager = PeersManager::new(PeersConfig::default());
2132        let mut fetcher =
2133            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2134
2135        let peer_id = B512::random();
2136        fetcher.new_active_peer(NewPeerInfo {
2137            peer_id,
2138            best_hash: B256::random(),
2139            best_number: 100,
2140            capabilities: Arc::new(Capabilities::new(vec![])),
2141            timeout: Arc::new(AtomicU64::new(10)),
2142            range_info: None,
2143            supports_snap: true,
2144        });
2145
2146        // Queue a followup snap request for the same peer.
2147        let (followup_tx, _followup_rx) = oneshot::channel();
2148        fetcher.queued_requests.push_back(DownloadRequest::GetSnap {
2149            request: SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
2150                request_id: 0,
2151                root_hash: B256::ZERO,
2152                starting_hash: B256::ZERO,
2153                limit_hash: B256::ZERO,
2154                response_bytes: 0,
2155            }),
2156            response: followup_tx,
2157            priority: Priority::Normal,
2158        });
2159
2160        let (tx, mut rx) = oneshot::channel();
2161        fetcher.inflight_snap_requests.insert(peer_id, Request { request: (), response: tx });
2162        fetcher.peers.get_mut(&peer_id).unwrap().state = PeerState::GetSnap;
2163
2164        let resp = SnapResponse::AccountRange(AccountRangeMessage {
2165            request_id: 1,
2166            accounts: vec![],
2167            proof: vec![],
2168        });
2169        let outcome = fetcher.on_snap_response(peer_id, Ok(resp));
2170
2171        assert!(matches!(outcome, Some(BlockResponseOutcome::Request(pid, _)) if pid == peer_id));
2172        assert!(rx.try_recv().is_ok());
2173    }
2174
2175    #[tokio::test]
2176    async fn test_queued_snap_request_rejected_after_last_peer_disconnects() {
2177        use futures::task::noop_waker;
2178        use std::task::{Context, Poll};
2179
2180        let manager = PeersManager::new(PeersConfig::default());
2181        let mut fetcher =
2182            StateFetcher::<EthNetworkPrimitives>::new(manager.handle(), Default::default());
2183
2184        // The only connected peer supports snap but is busy, so the request gets queued.
2185        let peer = B512::random();
2186        fetcher.new_active_peer(NewPeerInfo {
2187            peer_id: peer,
2188            best_hash: B256::random(),
2189            best_number: 100,
2190            capabilities: Arc::new(Capabilities::new(vec![])),
2191            timeout: Arc::new(AtomicU64::new(10)),
2192            range_info: None,
2193            supports_snap: true,
2194        });
2195        fetcher.peers.get_mut(&peer).expect("peer exists").state = PeerState::GetBlockHeaders;
2196
2197        let (tx, rx) = oneshot::channel();
2198        fetcher
2199            .download_requests_tx
2200            .send(DownloadRequest::GetSnap {
2201                request: SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
2202                    request_id: 0,
2203                    root_hash: B256::ZERO,
2204                    starting_hash: B256::ZERO,
2205                    limit_hash: B256::ZERO,
2206                    response_bytes: 0,
2207                }),
2208                response: tx,
2209                priority: Priority::Normal,
2210            })
2211            .unwrap();
2212
2213        let waker = noop_waker();
2214        let mut cx = Context::from_waker(&waker);
2215
2216        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2217        assert_eq!(fetcher.queued_requests.len(), 1);
2218
2219        // The only peer disconnects, leaving `self.peers` empty. The still-queued request must
2220        // resolve immediately instead of waiting for a peer that can never come back.
2221        fetcher.on_session_closed(&peer);
2222
2223        assert!(matches!(fetcher.poll(&mut cx), Poll::Pending));
2224        assert!(fetcher.queued_requests.is_empty());
2225        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
2226    }
2227}