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