Skip to main content

reth_network/transactions/
fetcher.rs

1//! Fetches transactions that peers announced with `NewPooledTransactionHashes`.
2//!
3//! The [`TransactionFetcher`] tracks every announced hash that is not known yet, together with the
4//! peers that announced it, and turns those announcements into `GetPooledTransactions` requests.
5//!
6//! # Model
7//!
8//! Every tracked hash is in exactly one of two states:
9//!
10//! - _pending_: waiting for one of the peers that announced it, its _candidates_, to become idle
11//! - _fetching_: part of exactly one inflight request
12//!
13//! A hash remembers the peers that announced it as candidates. Each peer keeps a FIFO queue of
14//! hashes to request, and the first peers to announce a hash get it queued right away. Later
15//! announcers are only remembered, so that a hash is not given up on when the first announcers
16//! fail to deliver it, without costing a queue entry per announcement. A request for an idle peer
17//! is built by draining its queue in the order the announcements were processed, skipping hashes
18//! that are being fetched from another peer or are not tracked anymore, until the request is
19//! full. Packing a request is therefore proportional to the request size and independent of the
20//! total number of pending hashes. FIFO refers to the fetcher's input order; callers may
21//! reorder or deduplicate hashes within a wire announcement.
22//!
23//! When a request resolves, delivered hashes are dropped from tracking. Undelivered hashes go back
24//! to pending and are queued for their remaining candidates, most recent announcers first, or are
25//! dropped if none remain. The responding peer is dropped as a candidate for every undelivered
26//! hash that precedes the last delivered hash of the request, treating it as skipped, and for
27//! all requested hashes if the response was empty or the request failed.
28//! Undelivered hashes after the last delivered hash keep the peer as a candidate only if it
29//! delivered at least half of the request, treating that tail as potentially truncated. This is
30//! a retry heuristic: transaction counts do not establish whether the response reached a byte
31//! limit. Requiring half the request limits repeated low-progress retries. A dropped peer's later
32//! announcements of the hash are ignored while it is tracked, so it cannot take retries from
33//! candidates that were not asked yet.
34//!
35//! Request timeouts are enforced by the peer's session, which resolves the request with
36//! [`RequestError::Timeout`], so the fetcher does not run any timers. A first timeout is retried
37//! once when the responding peer is the only remaining source; otherwise another source is tried.
38//!
39//! # Bounds
40//!
41//! - a configurable number of inflight requests per peer, one by default, and a global inflight
42//!   request limit
43//! - a per peer limit on the number of tracked hashes it is a candidate for, so a single peer
44//!   cannot flood the fetcher with announcements
45//! - a global limit on the number of tracked hashes, at which the peer tracking the most hashes
46//!   gives up its oldest pending hash, so a group of peers flooding the fetcher evicts its own
47//!   hashes rather than everyone else's
48//! - a fixed number of candidates per hash and a separate fetch-attempt limit, so remembering
49//!   recent fallback sources does not allow unlimited retries
50
51use super::{
52    announcement::{AnnouncedTransaction, TransactionMetadata},
53    config::TransactionFetcherConfig,
54    constants::{
55        tx_fetcher::{
56            AVERAGE_BYTE_SIZE_TX_ENCODED, MAX_COUNT_CANDIDATE_PEERS_PER_HASH,
57            MAX_COUNT_EAGER_CANDIDATE_PEERS_PER_HASH, MAX_FETCH_ATTEMPTS_PER_HASH,
58        },
59        SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
60        SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST,
61    },
62    PeerMetadata,
63};
64use crate::metrics::TransactionFetcherMetrics;
65use alloy_consensus::transaction::PooledTransaction;
66use alloy_primitives::{
67    map::{B256Map, B256Set, Entry, FbBuildHasher, HashMap},
68    TxHash,
69};
70use futures::{stream::FuturesUnordered, Future, FutureExt, Stream, StreamExt};
71use reth_eth_wire::{EthVersion, GetPooledTransactions, PooledTransactions};
72use reth_eth_wire_types::{EthNetworkPrimitives, NetworkPrimitives};
73use reth_network_api::PeerRequest;
74use reth_network_p2p::error::{RequestError, RequestResult};
75use reth_network_peers::PeerId;
76use reth_primitives_traits::SignedTransaction;
77use smallvec::SmallVec;
78use std::{
79    collections::{BinaryHeap, VecDeque},
80    pin::Pin,
81    sync::Arc,
82    task::{ready, Context, Poll},
83};
84use tokio::sync::{mpsc::error::TrySendError, oneshot};
85use tracing::trace;
86
87/// Maximum live entries inspected in a global fallback eviction search.
88const MAX_EVICTION_ATTEMPTS: usize = 8;
89
90/// Fetches transactions that peers announced but that are not in the pool yet.
91///
92/// Announcements are recorded with [`Self::on_announcement`], requests are sent with
93/// [`Self::dispatch`] and resolved requests are yielded as [`FetchEvent`]s by the [`Stream`]
94/// implementation. See the [module docs](self) for how requests are scheduled.
95#[derive(Debug)]
96pub struct TransactionFetcher<N: NetworkPrimitives = EthNetworkPrimitives> {
97    /// All tracked hashes with their candidate peers and fetch state.
98    hashes: B256Map<TxEntry>,
99    /// Tracked hashes in the order they were added, used to evict the oldest pending hash once
100    /// the fetcher is at capacity and the peer tracking the most hashes has none pending. Entries
101    /// are removed lazily, so it may contain hashes that are not tracked anymore. Fetching hashes
102    /// skipped by an eviction search are moved to the back.
103    order: VecDeque<(TxHash, u64)>,
104    /// Fetch state of all peers that announced tracked hashes.
105    peers: HashMap<PeerKey, PeerState>,
106    /// Maps peer ids to their compact key.
107    peer_keys: HashMap<PeerId, PeerKey, FbBuildHasher<64>>,
108    /// Key assigned to the next new peer. Keys are never reused, so the candidates of a hash can
109    /// safely refer to peers that disconnected in the meantime.
110    next_peer_key: u32,
111    /// Distinguishes overlapping requests, including repeat requests for the same hash.
112    next_request_id: u64,
113    /// Distinguishes a re-announcement from stale queue entries for an earlier tracked hash.
114    next_generation: u64,
115    /// Idle peers with queued hashes, in the order they became ready.
116    ready: VecDeque<PeerKey>,
117    /// All inflight `GetPooledTransactions` requests.
118    inflight: FuturesUnordered<InflightRequest<N::PooledTransaction>>,
119    /// Number of tracked hashes that are part of an inflight request.
120    num_fetching: usize,
121    /// Reused when verifying responses, so no sets are allocated per response.
122    scratch_requested: B256Set,
123    /// Reused when verifying responses, so no sets are allocated per response.
124    scratch_delivered: B256Set,
125    /// Reused when processing announcements, so no vector is allocated per announcement.
126    scratch_queue: Vec<(TxHash, u64)>,
127    /// Configured limits.
128    config: TransactionFetcherConfig,
129    metrics: TransactionFetcherMetrics,
130}
131
132impl<N: NetworkPrimitives> TransactionFetcher<N> {
133    /// Creates a new fetcher with the given config.
134    pub fn new(config: TransactionFetcherConfig) -> Self {
135        let metrics = TransactionFetcherMetrics::default();
136        metrics.capacity_inflight_requests.increment(config.max_inflight_requests as u64);
137
138        Self {
139            hashes: Default::default(),
140            order: Default::default(),
141            peers: Default::default(),
142            peer_keys: Default::default(),
143            next_peer_key: 0,
144            next_request_id: 0,
145            next_generation: 0,
146            ready: Default::default(),
147            inflight: Default::default(),
148            num_fetching: 0,
149            scratch_requested: Default::default(),
150            scratch_delivered: Default::default(),
151            scratch_queue: Default::default(),
152            config,
153            metrics,
154        }
155    }
156
157    /// Returns the fetcher's config.
158    pub const fn config(&self) -> &TransactionFetcherConfig {
159        &self.config
160    }
161
162    /// Returns the number of tracked hashes, pending and fetching.
163    pub fn num_hashes(&self) -> usize {
164        self.hashes.len()
165    }
166
167    /// Returns the number of hashes that are waiting for an idle candidate peer.
168    pub fn num_pending_hashes(&self) -> usize {
169        self.hashes.len().saturating_sub(self.num_fetching)
170    }
171
172    /// Returns the number of hashes that are part of an inflight request.
173    pub const fn num_fetching_hashes(&self) -> usize {
174        self.num_fetching
175    }
176
177    /// Returns the number of inflight requests.
178    pub fn num_inflight_requests(&self) -> usize {
179        self.inflight.len()
180    }
181
182    /// Returns `true` if there is no inflight request to the peer.
183    pub fn is_idle(&self, peer_id: &PeerId) -> bool {
184        self.peer_keys
185            .get(peer_id)
186            .and_then(|key| self.peers.get(key))
187            .is_none_or(|peer| peer.inflight == 0)
188    }
189
190    /// Records hashes announced by the peer and queues them for fetching.
191    ///
192    /// The metadata of a hash is the announced transaction type and size, if the announcement
193    /// carried it. The caller is expected to have filtered out hashes that are already known.
194    ///
195    /// Requests are only sent by [`Self::dispatch`].
196    pub fn on_announcement(
197        &mut self,
198        peer_id: PeerId,
199        announcement: impl IntoIterator<Item = AnnouncedTransaction>,
200    ) {
201        let key = self.peer_key(peer_id);
202        let max_per_peer = self.config.max_announced_hashes_per_peer as usize;
203        let max_total = self.config.max_capacity_cache_txns_pending_fetch as usize;
204
205        let mut dropped_peer_limit = 0u64;
206        let mut evicted_at_capacity = 0u64;
207        let mut dropped_at_capacity = 0u64;
208
209        // The peer's count of tracked hashes and the hashes to queue for it are kept locally and
210        // applied once at the end, so every announced hash costs a single map lookup.
211        let Some(peer) = self.peers.get(&key) else { return };
212        let mut tracked = peer.tracked;
213        let mut queue = std::mem::take(&mut self.scratch_queue);
214        queue.clear();
215        // Entries at the front of `queue` already evicted or moved into the peer queue.
216        let mut queue_start = 0;
217        // Other peers can only lose tracked hashes during this announcement. Refresh their
218        // heap entries lazily instead of scanning every peer for each hash at capacity.
219        let mut eviction = None;
220        // Gossip commonly replaces the same fallback peer for every hash in a batch. Apply
221        // that peer's accounting once, flushing before eviction needs up-to-date counts.
222        let mut retired_candidates = DeferredCandidateRemovals::default();
223
224        for AnnouncedTransaction { hash, metadata } in announcement {
225            let size = announced_size(metadata);
226            let at_capacity = self.hashes.len() >= max_total;
227
228            // whether the hash is queued for the peer right away
229            let (eager, generation) = match self.hashes.entry(hash) {
230                Entry::Occupied(mut occupied) => {
231                    let entry = occupied.get_mut();
232                    if let Some(candidate) = entry.candidate_mut(key) {
233                        // announced before by this peer, keep the latest size
234                        candidate.set_size(size);
235                        continue
236                    }
237                    if entry.failed.contains(&key) {
238                        continue
239                    }
240                    if tracked >= max_per_peer {
241                        dropped_peer_limit += 1;
242                        continue
243                    }
244                    if entry.candidates.len() == MAX_COUNT_CANDIDATE_PEERS_PER_HASH {
245                        // Keep the first eager sources and the most recent fallback sources.
246                        // A fixed first-N list lets early co-announcers exclude every later source.
247                        let replace = (MAX_COUNT_EAGER_CANDIDATE_PEERS_PER_HASH..
248                            entry.candidates.len())
249                            .find(|&idx| {
250                                entry.fetching_by.is_none_or(|(fetching, _)| {
251                                    entry.candidates[idx].peer != fetching
252                                })
253                            })
254                            .expect("only one candidate can be fetching");
255                        let removed = entry.candidates.remove(replace);
256                        retired_candidates.record_removed(removed.peer, &mut self.peers);
257                    }
258                    // The first announcers get the hash queued right away, later ones are only
259                    // remembered and asked once the earlier ones failed to deliver.
260                    let eager = entry.candidates.len() < MAX_COUNT_EAGER_CANDIDATE_PEERS_PER_HASH ||
261                        (entry.fetching_by.is_none() &&
262                            !entry.candidates.iter().any(Candidate::is_queued));
263                    let candidate = if eager {
264                        Candidate::queued(key, size)
265                    } else {
266                        Candidate::unqueued(key, size)
267                    };
268                    entry.candidates.push(candidate);
269                    (eager, entry.generation)
270                }
271                Entry::Vacant(vacant) => {
272                    let generation = self.next_generation;
273                    self.next_generation += 1;
274                    if tracked >= max_per_peer {
275                        dropped_peer_limit += 1;
276                        continue
277                    }
278                    if at_capacity {
279                        retired_candidates.flush(&mut self.peers);
280                        // the evicted hash may be one of this peer's, so its count is synced
281                        if let Some(peer) = self.peers.get_mut(&key) {
282                            peer.tracked = tracked;
283                        }
284                        if self.evict_pending(key, &queue, &mut queue_start, &mut eviction) {
285                            evicted_at_capacity += 1;
286                            tracked = self.peers.get(&key).map_or(tracked, |peer| peer.tracked);
287                        } else {
288                            dropped_at_capacity += 1;
289                            continue
290                        }
291                        self.hashes.insert(hash, TxEntry::new(key, size, generation));
292                    } else {
293                        vacant.insert(TxEntry::new(key, size, generation));
294                    }
295                    self.record_order(hash, generation);
296                    (true, generation)
297                }
298            };
299
300            tracked += 1;
301            if eager {
302                queue.push((hash, generation));
303            }
304        }
305
306        retired_candidates.flush(&mut self.peers);
307        let queued = queue.len() - queue_start;
308        if let Some(peer) = self.peers.get_mut(&key) {
309            peer.tracked = tracked;
310            for &(hash, generation) in &queue[queue_start..] {
311                peer.push_queue(
312                    &self.hashes,
313                    key,
314                    (hash, generation),
315                    QueuePosition::Back,
316                    max_per_peer,
317                );
318            }
319        }
320        queue.clear();
321        self.scratch_queue = queue;
322
323        if dropped_peer_limit > 0 {
324            self.metrics.announced_hashes_dropped_peer_limit.increment(dropped_peer_limit);
325        }
326        if evicted_at_capacity > 0 {
327            self.metrics.hashes_evicted_at_capacity.increment(evicted_at_capacity);
328        }
329        if dropped_at_capacity > 0 {
330            self.metrics.announced_hashes_dropped_at_capacity.increment(dropped_at_capacity);
331        }
332
333        if queued > 0 {
334            trace!(target: "net::tx",
335                peer_id=format!("{peer_id:#}"),
336                queued,
337                dropped_peer_limit,
338                evicted_at_capacity,
339                dropped_at_capacity,
340                "queued announced hashes"
341            );
342        }
343        if queued > 0 || queue_start > 0 {
344            self.mark_ready(key, QueuePosition::Back);
345        }
346    }
347
348    /// Sends `GetPooledTransactions` requests to idle peers that have queued hashes.
349    ///
350    /// Stops when the global inflight request limit is reached. `max_hashes_per_request` caps
351    /// each request independently of hashes inflight to other peers, so stalled peers cannot
352    /// shrink requests to responsive peers. Zero sends nothing. The caller must enforce its
353    /// import backpressure when consuming responses; decoded responses remain bounded by
354    /// the configured inflight request limit.
355    ///
356    /// Returns the number of requests sent. New requests are only polled by the next call to
357    /// [`Stream::poll_next`], so the caller must poll the fetcher again if any were sent.
358    pub fn dispatch(
359        &mut self,
360        peers: &HashMap<PeerId, PeerMetadata<N>, FbBuildHasher<64>>,
361        max_hashes_per_request: usize,
362    ) -> usize {
363        if max_hashes_per_request == 0 {
364            return 0
365        }
366        let max_inflight = self.config.max_inflight_requests as usize;
367        let mut sent = 0;
368        // peers whose session channel is full, they get another chance on the next dispatch
369        let mut retry = SmallVec::<[PeerKey; 4]>::new();
370
371        while self.inflight.len() < max_inflight {
372            let Some(key) = self.ready.pop_front() else { break };
373            let Some(peer) = self.peers.get_mut(&key) else { continue };
374            peer.ready = false;
375            if peer.inflight >= self.config.max_inflight_requests_per_peer {
376                continue
377            }
378            let peer_id = peer.peer_id;
379            // the session is gone if the manager doesn't know the peer anymore
380            let Some(session) = peers.get(&peer_id) else { continue };
381
382            let limit = max_hashes_per_request
383                .min(SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST);
384            let request_id = self.next_request_id;
385            self.next_request_id += 1;
386            let hashes = self.pack_request(key, request_id, limit);
387            if hashes.is_empty() {
388                continue
389            }
390
391            let (response, rx) = oneshot::channel();
392            let request = PeerRequest::GetPooledTransactions {
393                request: GetPooledTransactions(hashes.clone()),
394                response,
395            };
396
397            match session.request_tx().try_send(request) {
398                Ok(()) => {
399                    trace!(target: "net::tx",
400                        peer_id=format!("{peer_id:#}"),
401                        hashes=hashes.len(),
402                        "sending `GetPooledTransactions` request to peer's session"
403                    );
404                    if let Some(peer) = self.peers.get_mut(&key) {
405                        peer.inflight += 1;
406                    }
407                    self.inflight.push(InflightRequest {
408                        peer: key,
409                        request_id,
410                        peer_id,
411                        version: session.version(),
412                        client_version: session.client_version.clone(),
413                        hashes,
414                        response: rx,
415                    });
416                    sent += 1;
417                    // the peer may be allowed more than one inflight request
418                    self.mark_ready(key, QueuePosition::Back);
419                }
420                Err(err) => {
421                    self.unpack_request(key, request_id, hashes);
422                    match err {
423                        TrySendError::Full(_) => {
424                            self.metrics.egress_peer_channel_full.increment(1);
425                            retry.push(key);
426                        }
427                        TrySendError::Closed(_) => self.on_peer_disconnected(&peer_id),
428                    }
429                }
430            }
431        }
432
433        for key in retry {
434            self.mark_ready(key, QueuePosition::Back);
435        }
436
437        sent
438    }
439
440    /// Stops tracking the given hashes because the transactions were received, e.g. over
441    /// broadcast.
442    ///
443    /// Hashes that are part of an inflight request are simply not rescheduled when that request
444    /// resolves.
445    pub fn on_transactions_received<'a>(&mut self, hashes: impl IntoIterator<Item = &'a TxHash>) {
446        for hash in hashes {
447            self.remove_hash(hash);
448        }
449    }
450
451    /// Removes the peer as a candidate for all hashes it announced and drops pending hashes that
452    /// have no candidate left.
453    ///
454    /// An inflight request to the peer resolves with an error once its session is gone, which
455    /// reschedules the requested hashes.
456    pub fn on_peer_disconnected(&mut self, peer_id: &PeerId) {
457        let Some(key) = self.peer_keys.remove(peer_id) else { return };
458        if self.peers.remove(&key).is_none() {
459            return
460        }
461
462        // Every tracked hash is visited, since the peer is a candidate of hashes that are not in
463        // its queue as well. Disconnects are rare compared to announcements, so this is cheaper
464        // overall than checking for gone peers whenever candidates are counted.
465        let mut dropped = 0u64;
466        let mut requeue = Vec::new();
467        self.hashes.retain(|hash, entry| {
468            let before = entry.candidates.len();
469            entry.candidates.retain(|candidate| candidate.peer != key);
470            if entry.candidates.len() == before || entry.fetching_by.is_some() {
471                return true
472            }
473            if entry.candidates.is_empty() {
474                dropped += 1;
475                return false
476            }
477            // the hash must stay queued for at least one candidate
478            if !entry.candidates.iter().any(|candidate| candidate.is_queued()) {
479                requeue.push((entry.generation, *hash, entry.unqueued_candidates()));
480            }
481            true
482        });
483
484        let mut requeued = SmallVec::<[PeerKey; MAX_COUNT_CANDIDATE_PEERS_PER_HASH]>::new();
485        // Requeue at the front in reverse announcement order.
486        requeue.sort_unstable_by_key(|(generation, _, _)| std::cmp::Reverse(*generation));
487        for (_, hash, targets) in requeue {
488            self.requeue(hash, targets, &mut requeued);
489        }
490
491        if dropped > 0 {
492            self.metrics.hashes_dropped_no_candidate_peers.increment(dropped);
493        }
494        // retried hashes go to the most recent announcers first
495        for key in requeued {
496            self.mark_ready(key, QueuePosition::Front);
497        }
498    }
499
500    /// Updates the fetcher's gauges.
501    pub fn update_metrics(&self) {
502        self.metrics.inflight_transaction_requests.set(self.inflight.len() as f64);
503        self.metrics.hashes_inflight_transaction_requests.set(self.num_fetching as f64);
504        self.metrics.hashes_pending_fetch.set(self.num_pending_hashes() as f64);
505    }
506
507    /// Returns the key of the peer, registering the peer if it isn't known yet.
508    fn peer_key(&mut self, peer_id: PeerId) -> PeerKey {
509        *self.peer_keys.entry(peer_id).or_insert_with(|| {
510            let key = PeerKey(self.next_peer_key);
511            self.next_peer_key += 1;
512            self.peers.insert(key, PeerState::new(peer_id));
513            key
514        })
515    }
516
517    /// Marks the peer as ready for a request if it is idle and has hashes queued, either behind
518    /// the other ready peers or ahead of them.
519    fn mark_ready(&mut self, key: PeerKey, position: QueuePosition) {
520        if let Some(peer) = self.peers.get_mut(&key) &&
521            !peer.ready &&
522            !peer.queue.is_empty() &&
523            peer.inflight < self.config.max_inflight_requests_per_peer
524        {
525            peer.ready = true;
526            match position {
527                QueuePosition::Front => self.ready.push_front(key),
528                QueuePosition::Back => self.ready.push_back(key),
529            }
530        }
531    }
532
533    /// Queues the hash for the peer, at the back or the front of its queue, and marks it as
534    /// queued in the hash's candidate entry.
535    fn enqueue(&mut self, key: PeerKey, hash: TxHash, position: QueuePosition) {
536        let max_per_peer = self.config.max_announced_hashes_per_peer as usize;
537        let Some(peer) = self.peers.get_mut(&key) else { return };
538        let Some(entry) = self.hashes.get_mut(&hash) else { return };
539        let generation = entry.generation;
540        if let Some(candidate) = entry.candidate_mut(key) {
541            candidate.set_queued(true);
542        }
543        peer.push_queue(&self.hashes, key, (hash, generation), position, max_per_peer);
544    }
545
546    /// Records a newly tracked hash in the eviction order.
547    ///
548    /// Stale lifetimes are removed lazily. Once the order reaches twice the capacity it is
549    /// compacted in place, retaining only the current generation of every tracked hash.
550    fn record_order(&mut self, hash: TxHash, generation: u64) {
551        let max_len = 2 * self.config.max_capacity_cache_txns_pending_fetch as usize;
552        if self.order.len() >= max_len {
553            self.order.retain(|(hash, generation)| {
554                self.hashes.get(hash).is_some_and(|entry| entry.generation == *generation)
555            });
556        }
557        self.order.push_back((hash, generation));
558    }
559
560    /// Evicts a pending hash to make room for one announced by `announcer` and returns whether
561    /// one was found.
562    ///
563    /// Prefer the oldest exclusively announced pending hash of the peer tracking the most
564    /// hashes, so a group of peers flooding the fetcher gives up its own hashes when possible. The
565    /// announcer gives way when it tracks as many hashes as the busiest peer. If the
566    /// chosen peer has no exclusively announced pending hash, the oldest pending hash overall
567    /// is evicted. Preferring exclusive hashes protects other peers' work when possible, while
568    /// the fallback prevents co-announcements from making the entire cache unevictable.
569    ///
570    /// `queued` are the hashes of the current announcement that are not in the announcer's queue
571    /// yet; `queued_start` marks entries already evicted or moved into the peer queue.
572    fn evict_pending(
573        &mut self,
574        announcer: PeerKey,
575        queued: &[(TxHash, u64)],
576        queued_start: &mut usize,
577        eviction: &mut Option<EvictionState>,
578    ) -> bool {
579        let eviction = eviction.get_or_insert_with(|| EvictionState {
580            peers: self
581                .peers
582                .iter()
583                .filter(|(key, peer)| **key != announcer && peer.tracked > 0)
584                .map(|(key, peer)| (peer.tracked, *key))
585                .collect(),
586            exclusive: Default::default(),
587        });
588        let heap = &mut eviction.peers;
589        let (mut tracked, mut key) = loop {
590            let Some(&(previous, key)) = heap.peek() else { break (0, announcer) };
591            let current = self.peers.get(&key).map_or(0, |peer| peer.tracked);
592            if current == previous {
593                break (current, key)
594            }
595            heap.pop();
596            if current > 0 {
597                heap.push((current, key));
598            }
599        };
600        if let Some(peer) = self.peers.get(&announcer) &&
601            peer.tracked >= tracked
602        {
603            key = announcer;
604            tracked = peer.tracked;
605        }
606        if tracked == 0 {
607            return self.evict_oldest_pending()
608        }
609        if self.evict_oldest_pending_of(key, &mut eviction.exclusive) {
610            return true
611        }
612        if key == announcer {
613            while *queued_start < queued.len() {
614                let (hash, generation) = queued[*queued_start];
615                *queued_start += 1;
616                let Some(entry) =
617                    self.hashes.get(&hash).filter(|entry| entry.generation == generation)
618                else {
619                    continue
620                };
621                if entry.candidates.len() == 1 && entry.fetching_by.is_none() {
622                    self.remove_hash(&hash);
623                    return true
624                }
625                // Preserve shared entries skipped by the cursor in this announcement.
626                self.enqueue(key, hash, QueuePosition::Back);
627            }
628        }
629        self.evict_oldest_pending()
630    }
631
632    /// Evicts the oldest pending hash announced only by this peer, if one exists.
633    ///
634    /// Scan each selected peer's queue once per announcement. A bounded prefix can hide
635    /// exclusive victims behind shared hashes; caching avoids rescanning that prefix for every
636    /// eviction. Newly tracked hashes from the current announcement are handled by its cursor.
637    fn evict_oldest_pending_of(
638        &mut self,
639        key: PeerKey,
640        exclusive: &mut HashMap<PeerKey, VecDeque<(TxHash, u64)>>,
641    ) -> bool {
642        let candidates = exclusive.entry(key).or_insert_with(|| {
643            self.peers
644                .get(&key)
645                .into_iter()
646                .flat_map(|peer| &peer.queue)
647                .filter(|(hash, generation)| {
648                    self.hashes.get(hash).is_some_and(|entry| {
649                        entry.generation == *generation &&
650                            entry.candidates.len() == 1 &&
651                            entry.has_candidate(key) &&
652                            entry.fetching_by.is_none()
653                    })
654                })
655                .copied()
656                .collect()
657        });
658        while let Some((hash, generation)) = candidates.pop_front() {
659            if self.hashes.get(&hash).is_some_and(|entry| {
660                entry.generation == generation &&
661                    entry.candidates.len() == 1 &&
662                    entry.has_candidate(key) &&
663                    entry.fetching_by.is_none()
664            }) {
665                self.remove_hash(&hash);
666                return true
667            }
668        }
669        false
670    }
671
672    /// Evicts the oldest pending hash, including shared hashes, as a last resort.
673    ///
674    /// Shared hashes cannot be exempt from eviction: co-announcements could pin every slot.
675    /// Returns `false` if only fetching hashes were found within the search budget. Fetching
676    /// hashes are moved to the back of the order, so the next search continues past them.
677    fn evict_oldest_pending(&mut self) -> bool {
678        let mut attempts = 0;
679        while attempts < MAX_EVICTION_ATTEMPTS {
680            let Some((hash, generation)) = self.order.pop_front() else { break };
681            let Some(entry) = self.hashes.get(&hash).filter(|entry| entry.generation == generation)
682            else {
683                continue
684            };
685            attempts += 1;
686            if entry.fetching_by.is_some() {
687                self.order.push_back((hash, generation));
688                continue
689            }
690            self.remove_hash(&hash);
691            return true
692        }
693        false
694    }
695
696    /// Drains the peer's queue into a request, in announcement order, until the request holds
697    /// `max_hashes` hashes or the expected response size reaches the configured soft limit. A
698    /// transaction that on its own exceeds the size limit is requested alone.
699    ///
700    /// The returned hashes are marked as being fetched by the peer.
701    fn pack_request(&mut self, key: PeerKey, request_id: u64, max_hashes: usize) -> Vec<TxHash> {
702        let max_bytes =
703            self.config.soft_limit_byte_size_pooled_transactions_response_on_pack_request;
704        let Some(peer) = self.peers.get_mut(&key) else { return Vec::new() };
705
706        let mut hashes = Vec::with_capacity(peer.queue.len().min(max_hashes));
707        let mut bytes = 0usize;
708
709        while let Some((hash, generation)) = peer.queue.pop_front() {
710            // skip hashes that were delivered in the meantime, or that this peer is no longer a
711            // candidate for
712            let Some(entry) =
713                self.hashes.get_mut(&hash).filter(|entry| entry.generation == generation)
714            else {
715                continue
716            };
717            let Some(candidate) = entry.candidate_mut(key) else { continue };
718            candidate.set_queued(false);
719            let size = candidate.request_size();
720
721            // hashes that are being fetched elsewhere are queued again if that fetch fails
722            if entry.fetching_by.is_some() {
723                continue
724            }
725
726            if !hashes.is_empty() && bytes.saturating_add(size) > max_bytes {
727                if let Some(candidate) = entry.candidate_mut(key) {
728                    candidate.set_queued(true);
729                }
730                peer.queue.push_front((hash, generation));
731                break
732            }
733
734            entry.fetching_by = Some((key, request_id));
735            entry.attempts += 1;
736            self.num_fetching += 1;
737            bytes = bytes.saturating_add(size);
738            hashes.push(hash);
739
740            if hashes.len() >= max_hashes {
741                break
742            }
743        }
744
745        hashes
746    }
747
748    /// Reverts [`Self::pack_request`] for a request that could not be sent: the hashes are pending
749    /// again and are queued at the front of the peer's queue in their original order.
750    fn unpack_request(&mut self, key: PeerKey, request_id: u64, hashes: Vec<TxHash>) {
751        for hash in hashes.into_iter().rev() {
752            if let Some(entry) = self.hashes.get_mut(&hash) &&
753                entry.fetching_by == Some((key, request_id))
754            {
755                entry.fetching_by = None;
756                entry.attempts -= 1;
757                self.num_fetching -= 1;
758                self.enqueue(key, hash, QueuePosition::Front);
759            }
760        }
761    }
762
763    /// Queues a pending hash for the given candidates, the ones that don't have it queued, and
764    /// records those peers in `requeued`.
765    fn requeue(
766        &mut self,
767        hash: TxHash,
768        targets: SmallVec<[PeerKey; MAX_COUNT_CANDIDATE_PEERS_PER_HASH]>,
769        requeued: &mut SmallVec<[PeerKey; MAX_COUNT_CANDIDATE_PEERS_PER_HASH]>,
770    ) {
771        for key in targets {
772            self.enqueue(key, hash, QueuePosition::Front);
773            if !requeued.contains(&key) {
774                requeued.push(key);
775            }
776        }
777    }
778
779    /// Stops tracking the hash.
780    fn remove_hash(&mut self, hash: &TxHash) {
781        let Some(entry) = self.hashes.remove(hash) else { return };
782        if entry.fetching_by.is_some() {
783            self.num_fetching -= 1;
784        }
785        for candidate in &entry.candidates {
786            if let Some(peer) = self.peers.get_mut(&candidate.peer) {
787                peer.tracked = peer.tracked.saturating_sub(1);
788            }
789        }
790    }
791
792    /// Processes a resolved request and returns the corresponding event.
793    fn on_resolved(
794        &mut self,
795        resolved: ResolvedRequest<N::PooledTransaction>,
796    ) -> FetchEvent<N::PooledTransaction> {
797        let ResolvedRequest {
798            peer: key,
799            request_id,
800            peer_id,
801            version,
802            client_version,
803            hashes: requested,
804            result,
805        } = resolved;
806
807        if let Some(peer) = self.peers.get_mut(&key) {
808            peer.inflight = peer.inflight.saturating_sub(1);
809        }
810
811        let mut delivered = std::mem::take(&mut self.scratch_delivered);
812        delivered.clear();
813
814        let outcome = match result {
815            Ok(mut transactions) => {
816                let mut requested_set = std::mem::take(&mut self.scratch_requested);
817                requested_set.clear();
818                requested_set.extend(requested.iter().copied());
819                let unsolicited =
820                    verify_response(&mut transactions, &requested_set, &mut delivered);
821                self.scratch_requested = requested_set;
822                Ok((transactions, unsolicited))
823            }
824            Err(error) => Err(error),
825        };
826
827        let timed_out = matches!(&outcome, Err(RequestError::Timeout));
828        self.on_delivery(key, request_id, &requested, &delivered, timed_out);
829        self.scratch_delivered = delivered;
830        self.mark_ready(key, QueuePosition::Back);
831
832        match outcome {
833            Ok((transactions, unsolicited)) => {
834                if unsolicited > 0 {
835                    self.metrics.unsolicited_transactions.increment(unsolicited as u64);
836                    trace!(target: "net::tx",
837                        peer_id=format!("{peer_id:#}"),
838                        unsolicited,
839                        "received transactions in `PooledTransactions` response that weren't requested"
840                    );
841                }
842                if !transactions.is_empty() {
843                    self.metrics.fetched_transactions.increment(transactions.len() as u64);
844                    FetchEvent::TransactionsFetched {
845                        peer_id,
846                        version,
847                        client_version,
848                        transactions,
849                        report_peer: unsolicited > 0,
850                    }
851                } else if unsolicited > 0 {
852                    // the peer only sent transactions we didn't ask for
853                    FetchEvent::FetchError { peer_id, error: RequestError::BadResponse }
854                } else {
855                    trace!(target: "net::tx",
856                        peer_id=format!("{peer_id:#}"),
857                        requested=requested.len(),
858                        "received empty `PooledTransactions` response, peer failed to serve hashes it announced"
859                    );
860                    FetchEvent::EmptyResponse { peer_id }
861                }
862            }
863            Err(error) => FetchEvent::FetchError { peer_id, error },
864        }
865    }
866
867    /// Settles the requested hashes of a resolved request: delivered hashes are dropped and
868    /// undelivered hashes are rescheduled for their remaining candidates.
869    fn on_delivery(
870        &mut self,
871        key: PeerKey,
872        request_id: u64,
873        requested: &[TxHash],
874        delivered: &B256Set,
875        timed_out: bool,
876    ) {
877        // Position right after the last delivered hash. Drop the responder as a candidate for
878        // missing hashes before it. Keep it for the undelivered tail only if at least half of
879        // the requested hashes were delivered, treating that tail as potentially truncated.
880        // This is a retry heuristic: transaction counts do not establish whether the response
881        // reached a byte limit. Requiring half the request limits repeated low-progress retries.
882        let cutoff = if 2 * delivered.len() >= requested.len() {
883            requested
884                .iter()
885                .rposition(|hash| delivered.contains(hash))
886                .map_or(requested.len(), |idx| idx + 1)
887        } else {
888            requested.len()
889        };
890
891        let mut dropped = 0u64;
892        let mut requeued = SmallVec::<[PeerKey; MAX_COUNT_CANDIDATE_PEERS_PER_HASH]>::new();
893
894        // iterate in reverse so that queueing at the front of a queue preserves the request order
895        for (idx, hash) in requested.iter().enumerate().rev() {
896            if delivered.contains(hash) {
897                self.remove_hash(hash);
898                continue
899            }
900
901            // the hash was received elsewhere in the meantime, or was even announced and assigned
902            // to another peer again
903            let Some(entry) = self.hashes.get_mut(hash) else { continue };
904            if entry.fetching_by != Some((key, request_id)) {
905                continue
906            }
907            entry.fetching_by = None;
908            self.num_fetching -= 1;
909
910            // A short adaptive session timeout can expire before the first response from the
911            // only source arrives. Allow one retry; prefer another source whenever available.
912            let retry_only_source = timed_out && entry.attempts == 1 && entry.candidates.len() == 1;
913            let peers = &mut self.peers;
914            let failed = &mut entry.failed;
915            entry.candidates.retain(|candidate| {
916                // disconnected peers are pruned as well
917                let Some(peer) = peers.get_mut(&candidate.peer) else { return false };
918                if idx < cutoff && candidate.peer == key && !retry_only_source {
919                    peer.tracked = peer.tracked.saturating_sub(1);
920                    failed.push(key);
921                    return false
922                }
923                true
924            });
925
926            if entry.candidates.is_empty() || entry.attempts >= MAX_FETCH_ATTEMPTS_PER_HASH {
927                // Rotating fallback candidates must not allow unlimited retries for one hash.
928                self.remove_hash(hash);
929                dropped += 1;
930                continue
931            }
932
933            let targets = entry.unqueued_candidates();
934            self.requeue(*hash, targets, &mut requeued);
935        }
936
937        if dropped > 0 {
938            self.metrics.hashes_dropped_no_candidate_peers.increment(dropped);
939            trace!(target: "net::tx", dropped, "dropped hashes with no candidates or exhausted fetch attempts");
940        }
941
942        // retried hashes go to the most recent announcers first
943        for key in requeued {
944            self.mark_ready(key, QueuePosition::Front);
945        }
946    }
947}
948
949#[cfg(test)]
950impl<N: NetworkPrimitives> TransactionFetcher<N> {
951    /// Returns the connected peers that are candidates for the hash, in announcement order.
952    pub(super) fn candidate_peers(&self, hash: &TxHash) -> Vec<PeerId> {
953        self.hashes
954            .get(hash)
955            .map(|entry| {
956                entry
957                    .candidates
958                    .iter()
959                    .filter_map(|candidate| self.peers.get(&candidate.peer))
960                    .map(|peer| peer.peer_id)
961                    .collect()
962            })
963            .unwrap_or_default()
964    }
965
966    /// Returns the peer the hash is currently fetched from, if any.
967    fn fetching_peer(&self, hash: &TxHash) -> Option<PeerId> {
968        let (key, _) = self.hashes.get(hash)?.fetching_by?;
969        self.peers.get(&key).map(|peer| peer.peer_id)
970    }
971
972    /// Returns the hashes queued for the peer, oldest first. May include hashes that are not
973    /// tracked anymore or are being fetched elsewhere.
974    pub(super) fn queued_hashes(&self, peer_id: &PeerId) -> Vec<TxHash> {
975        self.peer_keys
976            .get(peer_id)
977            .and_then(|key| self.peers.get(key))
978            .map(|peer| peer.queue.iter().map(|(hash, _)| *hash).collect())
979            .unwrap_or_default()
980    }
981
982    /// Returns the number of peers the fetcher tracks.
983    fn num_peers(&self) -> usize {
984        self.peers.len()
985    }
986
987    /// Panics if the internal bookkeeping is inconsistent.
988    fn assert_invariants(&self) {
989        let fetching = self.hashes.values().filter(|entry| entry.fetching_by.is_some()).count();
990        assert_eq!(fetching, self.num_fetching, "fetching counter out of sync");
991
992        let ordered = self
993            .order
994            .iter()
995            .filter(|(hash, generation)| {
996                self.hashes.get(hash).is_some_and(|entry| entry.generation == *generation)
997            })
998            .map(|(hash, _)| *hash)
999            .collect::<B256Set>();
1000        assert!(
1001            self.order.len() <= 2 * self.config.max_capacity_cache_txns_pending_fetch as usize,
1002            "eviction order grew beyond its bound"
1003        );
1004
1005        for (hash, entry) in &self.hashes {
1006            assert!(
1007                !entry.candidates.is_empty() || entry.fetching_by.is_some(),
1008                "{hash} is pending without candidates"
1009            );
1010            assert!(
1011                entry.candidates.len() <= MAX_COUNT_CANDIDATE_PEERS_PER_HASH,
1012                "{hash} has too many candidates"
1013            );
1014            let mut unique = entry.candidates.iter().map(|c| c.peer).collect::<Vec<_>>();
1015            unique.sort_unstable();
1016            unique.dedup();
1017            assert_eq!(unique.len(), entry.candidates.len(), "{hash} has duplicate candidates");
1018            assert!(ordered.contains(hash), "{hash} is missing from the eviction order");
1019            if let Some((peer, request_id)) = entry.fetching_by {
1020                assert!(
1021                    self.inflight.iter().any(|request| request.peer == peer &&
1022                        request.request_id == request_id &&
1023                        request.hashes.contains(hash)),
1024                    "fetching hash has no matching request"
1025                );
1026            }
1027            assert!(entry.attempts <= MAX_FETCH_ATTEMPTS_PER_HASH);
1028        }
1029
1030        // a hash flagged as queued is in the peer's queue, and a pending hash is queued for at
1031        // least one connected candidate, otherwise it could starve
1032        let queued = self
1033            .peers
1034            .iter()
1035            .map(|(key, peer)| {
1036                assert!(
1037                    peer.queue.len() <= 2 * self.config.max_announced_hashes_per_peer as usize,
1038                    "queue of {:#} grew beyond its bound",
1039                    peer.peer_id
1040                );
1041                (
1042                    *key,
1043                    peer.queue
1044                        .iter()
1045                        .filter(|(hash, generation)| {
1046                            self.hashes
1047                                .get(hash)
1048                                .is_some_and(|entry| entry.generation == *generation)
1049                        })
1050                        .map(|(hash, _)| *hash)
1051                        .collect::<B256Set>(),
1052                )
1053            })
1054            .collect::<HashMap<_, _>>();
1055        for (hash, entry) in &self.hashes {
1056            let mut fetchable = entry.fetching_by.is_some();
1057            for candidate in &entry.candidates {
1058                let queue =
1059                    queued.get(&candidate.peer).expect("candidate refers to a disconnected peer");
1060                if candidate.is_queued() {
1061                    assert!(
1062                        queue.contains(hash),
1063                        "{hash} is flagged but not queued for {:?}",
1064                        candidate.peer
1065                    );
1066                    fetchable = true;
1067                }
1068            }
1069            assert!(fetchable, "pending {hash} is not queued for any connected candidate");
1070        }
1071
1072        for (key, peer) in &self.peers {
1073            assert_eq!(
1074                peer.inflight as usize,
1075                self.inflight.iter().filter(|request| request.peer == *key).count()
1076            );
1077            assert!(peer.inflight <= self.config.max_inflight_requests_per_peer);
1078            if peer.inflight < self.config.max_inflight_requests_per_peer &&
1079                peer.queue.iter().any(|(hash, generation)| {
1080                    self.hashes.get(hash).is_some_and(|entry| {
1081                        entry.generation == *generation &&
1082                            entry.fetching_by.is_none() &&
1083                            entry
1084                                .candidates
1085                                .iter()
1086                                .any(|candidate| candidate.peer == *key && candidate.is_queued())
1087                    })
1088                })
1089            {
1090                assert!(peer.ready, "idle peer has queued work but is not ready");
1091            }
1092            let tracked = self.hashes.values().filter(|entry| entry.has_candidate(*key)).count();
1093            assert_eq!(tracked, peer.tracked, "tracked counter of {:#} out of sync", peer.peer_id);
1094            assert!(
1095                peer.tracked <= self.config.max_announced_hashes_per_peer as usize,
1096                "{:#} exceeds the per peer limit",
1097                peer.peer_id
1098            );
1099            assert_eq!(
1100                peer.ready,
1101                self.ready.contains(key),
1102                "ready flag of {:#} out of sync",
1103                peer.peer_id
1104            );
1105            if peer.ready {
1106                assert!(
1107                    !peer.queue.is_empty(),
1108                    "{:#} is ready without queued hashes",
1109                    peer.peer_id
1110                );
1111                assert!(
1112                    peer.inflight < self.config.max_inflight_requests_per_peer,
1113                    "{:#} is ready but busy",
1114                    peer.peer_id
1115                );
1116            }
1117            assert_eq!(
1118                self.peer_keys.get(&peer.peer_id),
1119                Some(key),
1120                "peer key mapping out of sync"
1121            );
1122        }
1123
1124        for (peer_id, key) in &self.peer_keys {
1125            assert!(self.peers.contains_key(key), "{peer_id:#} maps to an unknown key");
1126        }
1127
1128        let inflight = self.peers.values().map(|peer| peer.inflight as usize).sum::<usize>();
1129        assert!(inflight <= self.inflight.len(), "peers claim more inflight requests than exist");
1130    }
1131}
1132
1133impl<N: NetworkPrimitives> Default for TransactionFetcher<N> {
1134    fn default() -> Self {
1135        Self::new(TransactionFetcherConfig::default())
1136    }
1137}
1138
1139impl<N: NetworkPrimitives> Stream for TransactionFetcher<N> {
1140    type Item = FetchEvent<N::PooledTransaction>;
1141
1142    /// Advances all inflight requests and yields the next resolved request as an event.
1143    ///
1144    /// Never terminates, returns [`Poll::Pending`] while no request is inflight.
1145    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1146        // `FuturesUnordered` yields `None` while empty but keeps working once new requests are
1147        // pushed, so this is mapped to pending
1148        match self.inflight.poll_next_unpin(cx) {
1149            Poll::Ready(Some(resolved)) => Poll::Ready(Some(self.on_resolved(resolved))),
1150            Poll::Ready(None) | Poll::Pending => Poll::Pending,
1151        }
1152    }
1153}
1154
1155/// Represents possible events from fetching transactions.
1156#[derive(Debug)]
1157pub enum FetchEvent<T = PooledTransaction> {
1158    /// Triggered when transactions are successfully fetched.
1159    TransactionsFetched {
1160        /// The ID of the peer from which transactions were fetched.
1161        peer_id: PeerId,
1162        /// The negotiated protocol of the session that served the response.
1163        version: EthVersion,
1164        /// The client version of the session that served the response.
1165        client_version: Arc<str>,
1166        /// The transactions that were fetched, if available.
1167        transactions: PooledTransactions<T>,
1168        /// Whether the peer should be penalized for sending unsolicited transactions or for
1169        /// misbehavior.
1170        report_peer: bool,
1171    },
1172    /// Triggered when there is an error in fetching transactions.
1173    FetchError {
1174        /// The ID of the peer from which an attempt to fetch transactions resulted in an error.
1175        peer_id: PeerId,
1176        /// The specific error that occurred while fetching.
1177        error: RequestError,
1178    },
1179    /// An empty response was received.
1180    EmptyResponse {
1181        /// The ID of the sender.
1182        peer_id: PeerId,
1183    },
1184}
1185
1186/// Compact identifier of a peer within the fetcher.
1187#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1188struct PeerKey(u32);
1189
1190/// Cached eviction work for one announcement. Other peers can only lose candidates; newly
1191/// tracked hashes from this announcer are handled by the announcement's scratch cursor.
1192#[derive(Debug)]
1193struct EvictionState {
1194    peers: BinaryHeap<(usize, PeerKey)>,
1195    exclusive: HashMap<PeerKey, VecDeque<(TxHash, u64)>>,
1196}
1197
1198/// Position for newly queued hashes or ready peers.
1199#[derive(Debug, Clone, Copy)]
1200enum QueuePosition {
1201    Front,
1202    Back,
1203}
1204
1205/// Batches consecutive removals from the same candidate peer during an announcement.
1206#[derive(Debug, Default)]
1207struct DeferredCandidateRemovals {
1208    pending: Option<(PeerKey, usize)>,
1209}
1210
1211impl DeferredCandidateRemovals {
1212    fn record_removed(&mut self, key: PeerKey, peers: &mut HashMap<PeerKey, PeerState>) {
1213        if let Some((pending_key, count)) = &mut self.pending &&
1214            *pending_key == key
1215        {
1216            *count += 1;
1217        } else {
1218            self.flush(peers);
1219            self.pending = Some((key, 1));
1220        }
1221    }
1222
1223    /// Synchronizes tracked counts before capacity eviction or the end of an announcement.
1224    fn flush(&mut self, peers: &mut HashMap<PeerKey, PeerState>) {
1225        if let Some((key, count)) = self.pending.take() {
1226            peers.get_mut(&key).expect("candidate peer is connected").tracked -= count;
1227        }
1228    }
1229}
1230
1231/// State of a tracked hash.
1232#[derive(Debug)]
1233struct TxEntry {
1234    /// Peers that announced the hash and may be asked for it, oldest first.
1235    candidates: SmallVec<[Candidate; MAX_COUNT_CANDIDATE_PEERS_PER_HASH]>,
1236    /// Identity of this tracking lifetime, also stored in lazy queue entries.
1237    generation: u64,
1238    /// The peer and request currently fetching the transaction, if any.
1239    fetching_by: Option<(PeerKey, u64)>,
1240    /// Sent requests for this entry, bounded independently of candidate replacement.
1241    attempts: usize,
1242    /// Peers dropped as candidates after failing to deliver the hash, whose announcements of it
1243    /// are ignored. Bounded by `attempts`, since each failed request adds at most one peer.
1244    failed: SmallVec<[PeerKey; 2]>,
1245}
1246
1247impl TxEntry {
1248    /// A new entry for a hash that is queued for the announcing peer.
1249    fn new(peer: PeerKey, size: u32, generation: u64) -> Self {
1250        let mut candidates = SmallVec::new();
1251        candidates.push(Candidate::queued(peer, size));
1252        Self { candidates, generation, fetching_by: None, attempts: 0, failed: SmallVec::new() }
1253    }
1254
1255    fn has_candidate(&self, key: PeerKey) -> bool {
1256        self.candidates.iter().any(|candidate| candidate.peer == key)
1257    }
1258
1259    /// Returns the candidates that don't have the hash queued, in the order they announced it.
1260    fn unqueued_candidates(&self) -> SmallVec<[PeerKey; MAX_COUNT_CANDIDATE_PEERS_PER_HASH]> {
1261        self.candidates
1262            .iter()
1263            .filter(|candidate| !candidate.is_queued())
1264            .map(|candidate| candidate.peer)
1265            .collect()
1266    }
1267
1268    fn candidate_mut(&mut self, key: PeerKey) -> Option<&mut Candidate> {
1269        self.candidates.iter_mut().find(|candidate| candidate.peer == key)
1270    }
1271}
1272
1273/// A peer that announced a hash.
1274#[derive(Debug, Clone, Copy)]
1275struct Candidate {
1276    peer: PeerKey,
1277    /// The size the peer announced for the transaction, 0 if unknown, with
1278    /// [`Self::QUEUED`] set while the hash is in the peer's queue.
1279    size_and_queued: u32,
1280}
1281
1282impl Candidate {
1283    /// Marks the hash as queued for the peer. Announced sizes are capped well below this bit.
1284    const QUEUED: u32 = 1 << 31;
1285
1286    /// A candidate that has the hash queued.
1287    const fn queued(peer: PeerKey, size: u32) -> Self {
1288        Self { peer, size_and_queued: size | Self::QUEUED }
1289    }
1290
1291    /// A candidate that doesn't have the hash queued.
1292    const fn unqueued(peer: PeerKey, size: u32) -> Self {
1293        Self { peer, size_and_queued: size }
1294    }
1295
1296    const fn is_queued(&self) -> bool {
1297        self.size_and_queued & Self::QUEUED != 0
1298    }
1299
1300    const fn set_queued(&mut self, queued: bool) {
1301        if queued {
1302            self.size_and_queued |= Self::QUEUED;
1303        } else {
1304            self.size_and_queued &= !Self::QUEUED;
1305        }
1306    }
1307
1308    const fn set_size(&mut self, size: u32) {
1309        self.size_and_queued = (self.size_and_queued & Self::QUEUED) | size;
1310    }
1311
1312    /// Returns the size to account for when packing the hash into a request to this peer.
1313    const fn request_size(&self) -> usize {
1314        let size = self.size_and_queued & !Self::QUEUED;
1315        if size == 0 {
1316            AVERAGE_BYTE_SIZE_TX_ENCODED
1317        } else {
1318            size as usize
1319        }
1320    }
1321}
1322
1323/// Fetch state of a peer.
1324#[derive(Debug)]
1325struct PeerState {
1326    peer_id: PeerId,
1327    /// Hashes the peer announced, oldest first. Entries are removed lazily, so the queue may
1328    /// contain hashes that are not tracked anymore or that the peer is no longer a candidate for.
1329    queue: VecDeque<(TxHash, u64)>,
1330    /// Number of tracked hashes that list this peer as a candidate.
1331    tracked: usize,
1332    /// Number of inflight requests to this peer.
1333    inflight: u8,
1334    /// Whether the peer is queued in the ready list.
1335    ready: bool,
1336}
1337
1338impl PeerState {
1339    const fn new(peer_id: PeerId) -> Self {
1340        Self { peer_id, queue: VecDeque::new(), tracked: 0, inflight: 0, ready: false }
1341    }
1342
1343    /// Queues the hash at the back or the front of the queue.
1344    ///
1345    /// Queues are bounded: once a queue holds twice `max_tracked` hashes, entries the peer is no
1346    /// longer a candidate for and duplicates are removed. A duplicate can occur when a hash is
1347    /// tracked again after it was delivered or evicted, while its old entry still lingers in the
1348    /// queue. Generations prevent those stale entries from selecting a newer tracking lifetime.
1349    fn push_queue(
1350        &mut self,
1351        hashes: &B256Map<TxEntry>,
1352        key: PeerKey,
1353        queued: (TxHash, u64),
1354        position: QueuePosition,
1355        max_tracked: usize,
1356    ) {
1357        if self.queue.len() >= 2 * max_tracked {
1358            let mut seen = B256Set::with_capacity_and_hasher(self.tracked, Default::default());
1359            self.queue.retain(|(hash, generation)| {
1360                hashes.get(hash).is_some_and(|entry| {
1361                    entry.generation == *generation && entry.has_candidate(key)
1362                }) && seen.insert(*hash)
1363            });
1364        }
1365        match position {
1366            QueuePosition::Front => self.queue.push_front(queued),
1367            QueuePosition::Back => self.queue.push_back(queued),
1368        }
1369    }
1370}
1371
1372/// An inflight `GetPooledTransactions` request.
1373#[derive(Debug)]
1374struct InflightRequest<T> {
1375    peer: PeerKey,
1376    request_id: u64,
1377    peer_id: PeerId,
1378    version: EthVersion,
1379    client_version: Arc<str>,
1380    /// The requested hashes, in request order.
1381    hashes: Vec<TxHash>,
1382    response: oneshot::Receiver<RequestResult<PooledTransactions<T>>>,
1383}
1384
1385impl<T> Future for InflightRequest<T> {
1386    type Output = ResolvedRequest<T>;
1387
1388    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1389        let this = self.get_mut();
1390        let result =
1391            ready!(this.response.poll_unpin(cx)).unwrap_or(Err(RequestError::ChannelClosed));
1392        Poll::Ready(ResolvedRequest {
1393            peer: this.peer,
1394            request_id: this.request_id,
1395            peer_id: this.peer_id,
1396            version: this.version,
1397            client_version: this.client_version.clone(),
1398            hashes: std::mem::take(&mut this.hashes),
1399            result,
1400        })
1401    }
1402}
1403
1404/// A resolved `GetPooledTransactions` request.
1405#[derive(Debug)]
1406struct ResolvedRequest<T> {
1407    peer: PeerKey,
1408    request_id: u64,
1409    peer_id: PeerId,
1410    version: EthVersion,
1411    client_version: Arc<str>,
1412    hashes: Vec<TxHash>,
1413    result: RequestResult<PooledTransactions<T>>,
1414}
1415
1416/// Returns the announced transaction size used for request packing, capped at the response soft
1417/// limit, or 0 if the announcement carried no size.
1418fn announced_size(metadata: Option<TransactionMetadata>) -> u32 {
1419    metadata.map_or(0, |metadata| {
1420        metadata.size.min(SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE) as u32
1421    })
1422}
1423
1424/// Filters a response down to the transactions that were requested, dropping duplicates.
1425///
1426/// Records the delivered hashes in `delivered` and returns the number of unsolicited transactions
1427/// that were dropped.
1428fn verify_response<T: SignedTransaction>(
1429    transactions: &mut PooledTransactions<T>,
1430    requested: &B256Set,
1431    delivered: &mut B256Set,
1432) -> usize {
1433    let mut unsolicited = 0;
1434    transactions.0.retain(|tx| {
1435        let hash = *tx.tx_hash();
1436        if !requested.contains(&hash) {
1437            unsolicited += 1;
1438            return false
1439        }
1440        delivered.insert(hash)
1441    });
1442    unsolicited
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447    use super::*;
1448    use crate::test_utils::transactions::new_mock_session_with_capacity;
1449    use alloy_consensus::transaction::Recovered;
1450    use alloy_primitives::B256;
1451    use futures::task::noop_waker_ref;
1452    use rand::{rngs::StdRng, seq::IndexedRandom, Rng, SeedableRng};
1453    use reth_eth_wire::EthVersion;
1454    use reth_ethereum_primitives::{PooledTransactionVariant, TransactionSigned};
1455    use reth_transaction_pool::test_utils::MockTransactionFactory;
1456    use tokio::sync::mpsc;
1457
1458    type Fetcher = TransactionFetcher<EthNetworkPrimitives>;
1459    type ResponseSender =
1460        oneshot::Sender<RequestResult<PooledTransactions<PooledTransactionVariant>>>;
1461
1462    const KIB: usize = 1024;
1463
1464    fn peer(n: u8) -> PeerId {
1465        PeerId::repeat_byte(n)
1466    }
1467
1468    fn hash(n: u64) -> TxHash {
1469        let mut bytes = [0u8; 32];
1470        bytes[24..].copy_from_slice(&n.to_be_bytes());
1471        B256::from(bytes)
1472    }
1473
1474    fn hashes(range: std::ops::Range<u64>) -> Vec<TxHash> {
1475        range.map(hash).collect()
1476    }
1477
1478    fn pooled_txs(count: usize) -> Vec<PooledTransactionVariant> {
1479        let mut factory = MockTransactionFactory::default();
1480        (0..count)
1481            .map(|_| {
1482                let recovered: Recovered<TransactionSigned> =
1483                    factory.create_eip1559().transaction.into();
1484                PooledTransactionVariant::try_from(recovered.into_inner())
1485                    .expect("eip1559 transaction converts to pooled transaction")
1486            })
1487            .collect()
1488    }
1489
1490    fn hashes_of(txs: &[PooledTransactionVariant]) -> Vec<TxHash> {
1491        txs.iter().map(|tx| *tx.tx_hash()).collect()
1492    }
1493
1494    /// A fetcher with mock peer sessions.
1495    struct Rig {
1496        fetcher: Fetcher,
1497        peers: HashMap<PeerId, PeerMetadata<EthNetworkPrimitives>, FbBuildHasher<64>>,
1498        sessions: HashMap<PeerId, mpsc::Receiver<PeerRequest>, FbBuildHasher<64>>,
1499    }
1500
1501    impl Rig {
1502        fn new() -> Self {
1503            Self::with_config(TransactionFetcherConfig::default())
1504        }
1505
1506        fn with_config(config: TransactionFetcherConfig) -> Self {
1507            Self {
1508                fetcher: Fetcher::new(config),
1509                peers: Default::default(),
1510                sessions: Default::default(),
1511            }
1512        }
1513
1514        fn verify(&self) {
1515            self.fetcher.assert_invariants();
1516        }
1517
1518        fn add_peer(&mut self, peer_id: PeerId) {
1519            self.add_peer_with_capacity(peer_id, 8);
1520        }
1521
1522        fn add_peer_with_capacity(&mut self, peer_id: PeerId, capacity: usize) {
1523            let (peer, rx) = new_mock_session_with_capacity(peer_id, EthVersion::Eth68, capacity);
1524            self.peers.insert(peer_id, peer);
1525            self.sessions.insert(peer_id, rx);
1526        }
1527
1528        fn announce(&mut self, peer_id: PeerId, hashes: &[TxHash]) {
1529            self.announce_with_sizes(peer_id, hashes.iter().map(|hash| (*hash, 512)));
1530        }
1531
1532        fn announce_with_sizes(
1533            &mut self,
1534            peer_id: PeerId,
1535            entries: impl IntoIterator<Item = (TxHash, usize)>,
1536        ) {
1537            self.fetcher.on_announcement(
1538                peer_id,
1539                entries.into_iter().map(|(hash, size)| AnnouncedTransaction {
1540                    hash,
1541                    metadata: Some(TransactionMetadata { tx_type: 2, size }),
1542                }),
1543            );
1544            self.verify();
1545        }
1546
1547        fn announce_unsized(&mut self, peer_id: PeerId, hashes: &[TxHash]) {
1548            self.fetcher.on_announcement(
1549                peer_id,
1550                hashes.iter().map(|&hash| AnnouncedTransaction { hash, metadata: None }),
1551            );
1552            self.verify();
1553        }
1554
1555        fn dispatch(&mut self) -> usize {
1556            self.dispatch_with_budget(usize::MAX)
1557        }
1558
1559        fn dispatch_with_budget(&mut self, max_hashes_per_request: usize) -> usize {
1560            let sent = self.fetcher.dispatch(&self.peers, max_hashes_per_request);
1561            self.verify();
1562            sent
1563        }
1564
1565        /// Takes the next request the peer's session received.
1566        fn take_request(&mut self, peer_id: PeerId) -> Option<(Vec<TxHash>, ResponseSender)> {
1567            match self.sessions.get_mut(&peer_id)?.try_recv().ok()? {
1568                PeerRequest::GetPooledTransactions { request, response } => {
1569                    Some((request.0, response))
1570                }
1571                _ => unreachable!("the fetcher only sends `GetPooledTransactions` requests"),
1572            }
1573        }
1574
1575        fn next_event(&mut self) -> Option<FetchEvent<PooledTransactionVariant>> {
1576            let mut cx = Context::from_waker(noop_waker_ref());
1577            let event = match self.fetcher.poll_next_unpin(&mut cx) {
1578                Poll::Ready(event) => event,
1579                Poll::Pending => None,
1580            };
1581            self.verify();
1582            event
1583        }
1584
1585        fn drain_events(&mut self) -> Vec<FetchEvent<PooledTransactionVariant>> {
1586            std::iter::from_fn(|| self.next_event()).collect()
1587        }
1588
1589        fn respond(
1590            &mut self,
1591            peer_id: PeerId,
1592            txs: Vec<PooledTransactionVariant>,
1593        ) -> FetchEvent<PooledTransactionVariant> {
1594            let (_, response) = self.take_request(peer_id).expect("request inflight");
1595            response.send(Ok(PooledTransactions(txs))).unwrap();
1596            self.next_event().expect("response yields an event")
1597        }
1598
1599        fn fail(
1600            &mut self,
1601            peer_id: PeerId,
1602            error: RequestError,
1603        ) -> FetchEvent<PooledTransactionVariant> {
1604            let (_, response) = self.take_request(peer_id).expect("request inflight");
1605            response.send(Err(error)).unwrap();
1606            self.next_event().expect("error yields an event")
1607        }
1608
1609        fn disconnect(&mut self, peer_id: PeerId) {
1610            self.peers.remove(&peer_id);
1611            self.sessions.remove(&peer_id);
1612            self.fetcher.on_peer_disconnected(&peer_id);
1613            self.verify();
1614        }
1615    }
1616
1617    #[test]
1618    fn announced_hashes_are_requested_from_announcing_peer() {
1619        let mut rig = Rig::new();
1620        let peer_a = peer(1);
1621        rig.add_peer(peer_a);
1622        let mut txs = pooled_txs(2);
1623        txs.sort_unstable_by_key(|tx| std::cmp::Reverse(*tx.tx_hash()));
1624        let hashes = hashes_of(&txs);
1625
1626        let mut cx = Context::from_waker(noop_waker_ref());
1627        assert!(rig.fetcher.poll_next_unpin(&mut cx).is_pending());
1628        rig.announce(peer_a, &hashes[..1]);
1629        rig.announce(peer_a, &hashes[1..]);
1630        rig.announce(peer_a, &hashes);
1631        assert_eq!(rig.fetcher.queued_hashes(&peer_a), hashes);
1632        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_a]);
1633        assert_eq!(rig.fetcher.num_pending_hashes(), 2);
1634        assert!(rig.fetcher.is_idle(&peer_a));
1635
1636        assert_eq!(rig.dispatch(), 1);
1637        assert!(!rig.fetcher.is_idle(&peer_a));
1638        assert_eq!(rig.fetcher.num_inflight_requests(), 1);
1639        assert_eq!(rig.fetcher.num_fetching_hashes(), 2);
1640        assert_eq!(rig.fetcher.num_pending_hashes(), 0);
1641        assert_eq!(rig.fetcher.fetching_peer(&hashes[0]), Some(peer_a));
1642
1643        let (requested, response) = rig.take_request(peer_a).unwrap();
1644        assert_eq!(requested, hashes);
1645        response.send(Ok(PooledTransactions(txs))).unwrap();
1646
1647        let FetchEvent::TransactionsFetched { peer_id, transactions, report_peer, .. } =
1648            rig.next_event().unwrap()
1649        else {
1650            panic!("expected fetched transactions")
1651        };
1652        assert_eq!(peer_id, peer_a);
1653        assert_eq!(transactions.len(), 2);
1654        assert!(!report_peer);
1655
1656        assert_eq!(rig.fetcher.num_hashes(), 0);
1657        assert_eq!(rig.fetcher.num_inflight_requests(), 0);
1658        assert!(rig.fetcher.is_idle(&peer_a));
1659        assert!(rig.fetcher.poll_next_unpin(&mut cx).is_pending());
1660    }
1661
1662    #[test]
1663    fn request_is_capped_by_hash_count() {
1664        let mut rig = Rig::new();
1665        let peer_a = peer(1);
1666        rig.add_peer(peer_a);
1667        let hashes = hashes(0..300);
1668
1669        // no size metadata, so the count limit is what bounds the request
1670        rig.announce_unsized(peer_a, &hashes);
1671        assert_eq!(rig.dispatch(), 1);
1672
1673        let (requested, response) = rig.take_request(peer_a).unwrap();
1674        assert_eq!(requested.len(), SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST);
1675        assert_eq!(requested, hashes[..256]);
1676        assert_eq!(rig.fetcher.num_pending_hashes(), 44);
1677
1678        // the peer is busy until the request resolves
1679        assert_eq!(rig.dispatch(), 0);
1680        response.send(Err(RequestError::BadResponse)).unwrap();
1681        rig.next_event().unwrap();
1682        assert_eq!(rig.fetcher.num_pending_hashes(), 44);
1683
1684        assert_eq!(rig.dispatch(), 1);
1685        let (requested, _) = rig.take_request(peer_a).unwrap();
1686        assert_eq!(requested, hashes[256..]);
1687    }
1688
1689    #[test]
1690    fn request_is_capped_by_expected_response_size() {
1691        let mut rig = Rig::new();
1692        let peer_a = peer(1);
1693        rig.add_peer(peer_a);
1694        let hashes = hashes(0..3);
1695
1696        rig.announce_with_sizes(
1697            peer_a,
1698            [(hashes[0], 100 * KIB), (hashes[1], 100 * KIB), (hashes[2], 100)],
1699        );
1700
1701        rig.dispatch();
1702        let (requested, response) = rig.take_request(peer_a).unwrap();
1703        assert_eq!(requested, hashes[..1], "second transaction doesn't fit in 128 KiB");
1704
1705        response.send(Ok(PooledTransactions(vec![]))).unwrap();
1706        rig.next_event().unwrap();
1707        // the empty response dropped the peer as candidate for the first hash
1708        assert_eq!(rig.fetcher.num_hashes(), 2);
1709
1710        rig.dispatch();
1711        let (requested, _) = rig.take_request(peer_a).unwrap();
1712        assert_eq!(requested, hashes[1..]);
1713    }
1714
1715    #[test]
1716    fn announced_size_is_per_peer() {
1717        let mut rig = Rig::new();
1718        let peer_a = peer(1);
1719        let peer_b = peer(2);
1720        rig.add_peer(peer_a);
1721        rig.add_peer(peer_b);
1722        let hashes = hashes(0..2);
1723
1724        // peer_a doesn't know the sizes, peer_b announces a huge first transaction
1725        rig.announce_unsized(peer_a, &hashes);
1726        rig.announce_with_sizes(peer_b, [(hashes[0], 1024 * KIB), (hashes[1], 100)]);
1727
1728        // peer_a's request is packed with the size estimate
1729        rig.dispatch();
1730        let (requested, response) = rig.take_request(peer_a).unwrap();
1731        assert_eq!(requested, hashes);
1732        response.send(Ok(PooledTransactions(vec![]))).unwrap();
1733        rig.next_event().unwrap();
1734
1735        // peer_b's request honors the size it announced
1736        rig.dispatch();
1737        let (requested, _) = rig.take_request(peer_b).unwrap();
1738        assert_eq!(requested, hashes[..1]);
1739    }
1740
1741    #[test]
1742    fn announced_sizes_are_capped() {
1743        let mut rig = Rig::new();
1744        let peer_a = peer(1);
1745        rig.add_peer(peer_a);
1746        let hashes = hashes(0..3);
1747
1748        // absurd sizes must not break the size accounting
1749        rig.announce_with_sizes(
1750            peer_a,
1751            [(hashes[0], usize::MAX - 1), (hashes[1], usize::MAX), (hashes[2], 100)],
1752        );
1753
1754        rig.dispatch();
1755        let (requested, response) = rig.take_request(peer_a).unwrap();
1756        assert_eq!(requested, hashes[..1], "an oversized transaction is requested alone");
1757        response.send(Err(RequestError::BadResponse)).unwrap();
1758        rig.next_event().unwrap();
1759
1760        rig.dispatch();
1761        let (requested, _) = rig.take_request(peer_a).unwrap();
1762        assert_eq!(requested, hashes[1..2]);
1763    }
1764
1765    #[test]
1766    fn hash_is_fetched_from_one_peer_at_a_time() {
1767        let mut rig = Rig::new();
1768        let peer_a = peer(1);
1769        let peer_b = peer(2);
1770        rig.add_peer(peer_a);
1771        rig.add_peer(peer_b);
1772        let txs = pooled_txs(1);
1773        let hashes = hashes_of(&txs);
1774
1775        rig.announce(peer_a, &hashes);
1776        rig.announce(peer_b, &hashes);
1777        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_a, peer_b]);
1778
1779        assert_eq!(rig.dispatch(), 1);
1780        assert_eq!(rig.fetcher.fetching_peer(&hashes[0]), Some(peer_a));
1781        assert!(rig.take_request(peer_b).is_none());
1782        // peer_b's queue entry was consumed without a request
1783        assert!(rig.fetcher.queued_hashes(&peer_b).is_empty());
1784        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_a, peer_b]);
1785
1786        rig.respond(peer_a, txs);
1787        assert_eq!(rig.fetcher.num_hashes(), 0);
1788        assert_eq!(rig.dispatch(), 0);
1789    }
1790
1791    #[test]
1792    fn failed_request_is_retried_with_alternate_peer() {
1793        let mut rig = Rig::new();
1794        let peer_a = peer(1);
1795        let peer_b = peer(2);
1796        rig.add_peer(peer_a);
1797        rig.add_peer(peer_b);
1798        let txs = pooled_txs(2);
1799        let hashes = hashes_of(&txs);
1800
1801        rig.announce(peer_a, &hashes);
1802        rig.announce(peer_b, &hashes);
1803        rig.dispatch();
1804        // peer_b skipped the hashes while they were inflight to peer_a
1805        assert!(rig.fetcher.queued_hashes(&peer_b).is_empty());
1806
1807        let FetchEvent::FetchError { peer_id, error } = rig.fail(peer_a, RequestError::Timeout)
1808        else {
1809            panic!("expected fetch error")
1810        };
1811        assert_eq!(peer_id, peer_a);
1812        assert!(matches!(error, RequestError::Timeout));
1813
1814        // the hashes are pending again for peer_b only and queued in request order
1815        assert_eq!(rig.fetcher.num_pending_hashes(), 2);
1816        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_b]);
1817        assert_eq!(rig.fetcher.queued_hashes(&peer_b), hashes);
1818
1819        assert_eq!(rig.dispatch(), 1);
1820        assert!(rig.take_request(peer_a).is_none());
1821        let event = rig.respond(peer_b, txs);
1822        assert!(
1823            matches!(event, FetchEvent::TransactionsFetched { peer_id, .. } if peer_id == peer_b)
1824        );
1825        assert_eq!(rig.fetcher.num_hashes(), 0);
1826    }
1827
1828    #[test]
1829    fn empty_response_drops_peer_as_candidate() {
1830        let mut rig = Rig::new();
1831        let peer_a = peer(1);
1832        rig.add_peer(peer_a);
1833        let hashes = hashes(0..2);
1834
1835        rig.announce(peer_a, &hashes);
1836        rig.dispatch();
1837        let event = rig.respond(peer_a, vec![]);
1838        assert!(matches!(event, FetchEvent::EmptyResponse { peer_id } if peer_id == peer_a));
1839
1840        // no other peer announced the hashes, so they are dropped
1841        assert_eq!(rig.fetcher.num_hashes(), 0);
1842        assert_eq!(rig.dispatch(), 0);
1843    }
1844
1845    #[test]
1846    fn reannouncing_failed_peers_do_not_overtake_untried_candidates() {
1847        let mut rig = Rig::new();
1848        let (peer_a, peer_b, honest) = (peer(1), peer(2), peer(3));
1849        let txs = pooled_txs(1);
1850        let h = hashes_of(&txs)[0];
1851        rig.add_peer(peer_a);
1852        rig.add_peer(honest);
1853        rig.add_peer(peer_b);
1854
1855        rig.announce(peer_a, &[h]);
1856        rig.dispatch();
1857        rig.announce(honest, &[h]);
1858        rig.dispatch();
1859        rig.announce(peer_b, &[h]);
1860        rig.dispatch();
1861
1862        // the failing peers take turns re-announcing the hash and answering empty
1863        let (mut failing, mut other) = (peer_a, peer_b);
1864        for _ in 0..MAX_FETCH_ATTEMPTS_PER_HASH {
1865            if rig.fetcher.fetching_peer(&h) == Some(honest) {
1866                break
1867            }
1868            rig.respond(failing, vec![]);
1869            rig.dispatch();
1870            rig.announce(failing, &[h]);
1871            rig.dispatch();
1872            std::mem::swap(&mut failing, &mut other);
1873        }
1874
1875        assert_eq!(rig.fetcher.fetching_peer(&h), Some(honest));
1876        assert_eq!(rig.fetcher.candidate_peers(&h), vec![honest]);
1877        let event = rig.respond(honest, txs);
1878        assert!(
1879            matches!(event, FetchEvent::TransactionsFetched { peer_id, .. } if peer_id == honest)
1880        );
1881        assert_eq!(rig.fetcher.num_hashes(), 0);
1882    }
1883
1884    #[test]
1885    fn untried_candidate_is_asked_after_a_single_empty_response() {
1886        let mut rig = Rig::new();
1887        let (peer_a, honest) = (peer(1), peer(2));
1888        let h = hash(1);
1889        rig.add_peer(peer_a);
1890        rig.add_peer(honest);
1891
1892        rig.announce(peer_a, &[h]);
1893        rig.dispatch();
1894        rig.announce(honest, &[h]);
1895        rig.dispatch();
1896        rig.respond(peer_a, vec![]);
1897        rig.dispatch();
1898        assert_eq!(rig.fetcher.fetching_peer(&h), Some(honest));
1899    }
1900
1901    #[test]
1902    fn partial_response_keeps_peer_for_truncated_tail() {
1903        let mut rig = Rig::new();
1904        let peer_a = peer(1);
1905        rig.add_peer(peer_a);
1906        let txs = pooled_txs(4);
1907        let hashes = hashes_of(&txs);
1908
1909        rig.announce(peer_a, &hashes);
1910        rig.dispatch();
1911
1912        // the first half is delivered, the rest looks like truncation
1913        rig.respond(peer_a, txs[..2].to_vec());
1914        assert_eq!(rig.fetcher.num_pending_hashes(), 2);
1915        assert_eq!(rig.fetcher.candidate_peers(&hashes[2]), vec![peer_a]);
1916        assert_eq!(rig.fetcher.queued_hashes(&peer_a), hashes[2..]);
1917
1918        rig.dispatch();
1919        let (requested, _) = rig.take_request(peer_a).unwrap();
1920        assert_eq!(requested, hashes[2..]);
1921    }
1922
1923    #[test]
1924    fn small_partial_response_drops_peer_for_all_undelivered_hashes() {
1925        let mut rig = Rig::new();
1926        let peer_a = peer(1);
1927        let peer_b = peer(2);
1928        rig.add_peer(peer_a);
1929        rig.add_peer(peer_b);
1930        let txs = pooled_txs(10);
1931        let hashes = hashes_of(&txs);
1932
1933        rig.announce(peer_a, &hashes);
1934        rig.announce(peer_b, &hashes[..5]);
1935        rig.dispatch();
1936        assert_eq!(rig.fetcher.fetching_peer(&hashes[0]), Some(peer_a));
1937
1938        // Delivering fewer than half of the requested hashes drops peer_a for the rest,
1939        // limiting repeated low-progress retries regardless of transaction sizes.
1940        rig.respond(peer_a, txs[..1].to_vec());
1941        assert_eq!(rig.fetcher.num_hashes(), 4, "hashes without another candidate are dropped");
1942        for hash in &hashes[1..5] {
1943            assert_eq!(rig.fetcher.candidate_peers(hash), vec![peer_b]);
1944        }
1945        assert!(rig.fetcher.queued_hashes(&peer_a).is_empty());
1946    }
1947
1948    #[test]
1949    fn skipped_hashes_without_alternates_are_dropped() {
1950        let mut rig = Rig::new();
1951        let peer_a = peer(1);
1952        rig.add_peer(peer_a);
1953        let txs = pooled_txs(4);
1954        let hashes = hashes_of(&txs);
1955
1956        rig.announce(peer_a, &hashes);
1957        rig.dispatch();
1958        rig.respond(peer_a, txs[1..3].to_vec());
1959
1960        // the first hash was skipped and has no other candidate, the last one is retried
1961        assert_eq!(rig.fetcher.num_hashes(), 1);
1962        assert_eq!(rig.fetcher.candidate_peers(&hashes[3]), vec![peer_a]);
1963        assert!(rig.fetcher.candidate_peers(&hashes[0]).is_empty());
1964    }
1965
1966    #[test]
1967    fn unsolicited_and_duplicate_transactions_are_filtered_and_reported() {
1968        let mut rig = Rig::new();
1969        let peer_a = peer(1);
1970        rig.add_peer(peer_a);
1971        let txs = pooled_txs(2);
1972        let hashes = hashes_of(&txs);
1973
1974        rig.announce(peer_a, &hashes[..1]);
1975        rig.dispatch();
1976
1977        let FetchEvent::TransactionsFetched { transactions, report_peer, .. } =
1978            rig.respond(peer_a, vec![txs[0].clone(), txs[1].clone(), txs[0].clone()])
1979        else {
1980            panic!("expected fetched transactions")
1981        };
1982        assert!(report_peer);
1983        assert_eq!(transactions.0, txs[..1]);
1984        assert_eq!(rig.fetcher.num_hashes(), 0);
1985    }
1986
1987    #[test]
1988    fn response_with_only_unsolicited_transactions_is_a_bad_response() {
1989        let mut rig = Rig::new();
1990        let peer_a = peer(1);
1991        let peer_b = peer(2);
1992        rig.add_peer(peer_a);
1993        rig.add_peer(peer_b);
1994        let txs = pooled_txs(2);
1995        let hashes = hashes_of(&txs);
1996
1997        rig.announce(peer_a, &hashes[..1]);
1998        rig.announce(peer_b, &hashes[..1]);
1999        rig.dispatch();
2000
2001        let event = rig.respond(peer_a, txs[1..].to_vec());
2002        assert!(matches!(
2003            event,
2004            FetchEvent::FetchError { peer_id, error: RequestError::BadResponse } if peer_id == peer_a
2005        ));
2006        // treated like a failed request, peer_b gets to try
2007        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_b]);
2008        rig.dispatch();
2009        assert!(rig.take_request(peer_b).is_some());
2010    }
2011
2012    #[test]
2013    fn duplicate_transactions_in_response_are_deduplicated() {
2014        let mut rig = Rig::new();
2015        let peer_a = peer(1);
2016        rig.add_peer(peer_a);
2017        let txs = pooled_txs(1);
2018
2019        rig.announce(peer_a, &hashes_of(&txs));
2020        rig.dispatch();
2021
2022        let FetchEvent::TransactionsFetched { transactions, report_peer, .. } =
2023            rig.respond(peer_a, vec![txs[0].clone(), txs[0].clone()])
2024        else {
2025            panic!("expected fetched transactions")
2026        };
2027        assert_eq!(transactions.len(), 1);
2028        assert!(!report_peer);
2029    }
2030
2031    #[test]
2032    fn received_transactions_stop_tracking() {
2033        let mut rig = Rig::new();
2034        let peer_a = peer(1);
2035        rig.add_peer(peer_a);
2036        let txs = pooled_txs(3);
2037        let hashes = hashes_of(&txs);
2038
2039        rig.announce(peer_a, &hashes[..2]);
2040        rig.dispatch();
2041        rig.announce(peer_a, &hashes[2..]);
2042        assert_eq!(rig.fetcher.num_fetching_hashes(), 2);
2043        assert_eq!(rig.fetcher.num_pending_hashes(), 1);
2044
2045        // one inflight and the pending hash arrive over broadcast
2046        rig.fetcher.on_transactions_received([&hashes[0], &hashes[2]]);
2047        rig.fetcher.assert_invariants();
2048        assert_eq!(rig.fetcher.num_hashes(), 1);
2049        assert_eq!(rig.fetcher.num_fetching_hashes(), 1);
2050
2051        // the response doesn't include the hash received over broadcast, which must not be
2052        // rescheduled, but includes the other one
2053        let FetchEvent::TransactionsFetched { transactions, .. } =
2054            rig.respond(peer_a, txs[1..2].to_vec())
2055        else {
2056            panic!("expected fetched transactions")
2057        };
2058        assert_eq!(transactions.len(), 1);
2059        assert_eq!(rig.fetcher.num_hashes(), 0);
2060        assert_eq!(rig.dispatch(), 0);
2061    }
2062
2063    #[test]
2064    fn response_delivering_hash_received_over_broadcast_is_still_returned() {
2065        let mut rig = Rig::new();
2066        let peer_a = peer(1);
2067        rig.add_peer(peer_a);
2068        let txs = pooled_txs(1);
2069        let hashes = hashes_of(&txs);
2070
2071        rig.announce(peer_a, &hashes);
2072        rig.dispatch();
2073        rig.fetcher.on_transactions_received(&hashes);
2074
2075        let FetchEvent::TransactionsFetched { transactions, report_peer, .. } =
2076            rig.respond(peer_a, txs)
2077        else {
2078            panic!("expected fetched transactions")
2079        };
2080        assert_eq!(transactions.len(), 1);
2081        assert!(!report_peer, "the transaction was requested, even if it arrived elsewhere first");
2082        assert_eq!(rig.fetcher.num_hashes(), 0);
2083    }
2084
2085    #[test]
2086    fn peer_disconnect_drops_hashes_without_alternates() {
2087        let mut rig = Rig::new();
2088        let peer_a = peer(1);
2089        let peer_b = peer(2);
2090        rig.add_peer(peer_a);
2091        rig.add_peer(peer_b);
2092        let hashes = hashes(0..2);
2093
2094        rig.announce(peer_a, &hashes);
2095        rig.announce(peer_b, &hashes[1..]);
2096
2097        rig.disconnect(peer_a);
2098        assert_eq!(rig.fetcher.num_hashes(), 1);
2099        assert_eq!(rig.fetcher.candidate_peers(&hashes[1]), vec![peer_b]);
2100        assert!(rig.fetcher.is_idle(&peer_a));
2101
2102        rig.dispatch();
2103        let (requested, _) = rig.take_request(peer_b).unwrap();
2104        assert_eq!(requested, hashes[1..]);
2105    }
2106
2107    #[test]
2108    fn inflight_request_of_disconnected_peer_is_rescheduled() {
2109        let mut rig = Rig::new();
2110        let peer_a = peer(1);
2111        let peer_b = peer(2);
2112        rig.add_peer(peer_a);
2113        rig.add_peer(peer_b);
2114        let txs = pooled_txs(2);
2115        let hashes = hashes_of(&txs);
2116
2117        rig.announce(peer_a, &hashes);
2118        rig.announce(peer_b, &hashes);
2119        rig.dispatch();
2120        assert_eq!(rig.fetcher.fetching_peer(&hashes[0]), Some(peer_a));
2121
2122        // dropping the session drops the pending request, which resolves it with an error
2123        rig.disconnect(peer_a);
2124        assert_eq!(
2125            rig.fetcher.num_fetching_hashes(),
2126            2,
2127            "hashes stay inflight until the request resolves"
2128        );
2129
2130        let event = rig.next_event().unwrap();
2131        assert!(matches!(
2132            event,
2133            FetchEvent::FetchError { peer_id, error: RequestError::ChannelClosed } if peer_id == peer_a
2134        ));
2135        assert_eq!(rig.fetcher.num_pending_hashes(), 2);
2136        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_b]);
2137
2138        rig.dispatch();
2139        let event = rig.respond(peer_b, txs);
2140        assert!(matches!(event, FetchEvent::TransactionsFetched { .. }));
2141        assert_eq!(rig.fetcher.num_hashes(), 0);
2142    }
2143
2144    #[test]
2145    fn per_peer_announcement_limit_is_enforced() {
2146        let config =
2147            TransactionFetcherConfig { max_announced_hashes_per_peer: 3, ..Default::default() };
2148        let mut rig = Rig::with_config(config);
2149        let peer_a = peer(1);
2150        let peer_b = peer(2);
2151        rig.add_peer(peer_a);
2152        rig.add_peer(peer_b);
2153        let txs = pooled_txs(5);
2154        let hashes = hashes_of(&txs);
2155
2156        rig.announce(peer_a, &hashes);
2157        assert_eq!(rig.fetcher.num_hashes(), 3);
2158        assert_eq!(rig.fetcher.queued_hashes(&peer_a), hashes[..3]);
2159
2160        // the limit is per peer, another peer can still announce the dropped hashes
2161        rig.announce(peer_b, &hashes[3..]);
2162        assert_eq!(rig.fetcher.num_hashes(), 5);
2163        assert_eq!(rig.fetcher.candidate_peers(&hashes[4]), vec![peer_b]);
2164
2165        // delivering frees up the budget of the peer
2166        rig.dispatch();
2167        rig.respond(peer_a, txs[..3].to_vec());
2168        rig.announce(peer_a, &hashes[3..]);
2169        assert_eq!(rig.fetcher.candidate_peers(&hashes[3]), vec![peer_b, peer_a]);
2170    }
2171
2172    #[test]
2173    fn global_capacity_evicts_oldest_pending_hash() {
2174        let config = TransactionFetcherConfig {
2175            max_capacity_cache_txns_pending_fetch: 2,
2176            ..Default::default()
2177        };
2178        let mut rig = Rig::with_config(config);
2179        let peer_a = peer(1);
2180        let peer_b = peer(2);
2181        rig.add_peer(peer_a);
2182        rig.add_peer(peer_b);
2183        let txs = pooled_txs(5);
2184        let hashes = hashes_of(&txs);
2185
2186        // the oldest pending hash makes room for a new one
2187        rig.announce(peer_a, &hashes[..3]);
2188        assert_eq!(rig.fetcher.num_hashes(), 2);
2189        assert!(rig.fetcher.candidate_peers(&hashes[0]).is_empty());
2190        assert_eq!(rig.fetcher.candidate_peers(&hashes[2]), vec![peer_a]);
2191
2192        // hashes that are being fetched are not evicted, the announcement is dropped instead
2193        rig.dispatch();
2194        let (requested, response) = rig.take_request(peer_a).unwrap();
2195        assert_eq!(requested, hashes[1..3]);
2196        rig.announce(peer_b, &hashes[3..4]);
2197        assert_eq!(rig.fetcher.num_hashes(), 2);
2198        assert!(rig.fetcher.candidate_peers(&hashes[3]).is_empty());
2199
2200        // the second hash is delivered, the third is pending again. Both peers then track one
2201        // hash and the announcing peer gives way, so its own older hash makes room.
2202        response.send(Ok(PooledTransactions(txs[1..2].to_vec()))).unwrap();
2203        rig.next_event().unwrap();
2204        assert_eq!(rig.fetcher.num_hashes(), 1);
2205        rig.announce(peer_b, &hashes[3..4]);
2206        assert_eq!(rig.fetcher.num_hashes(), 2);
2207        rig.announce(peer_b, &hashes[4..5]);
2208        assert_eq!(rig.fetcher.num_hashes(), 2);
2209        assert_eq!(rig.fetcher.candidate_peers(&hashes[2]), vec![peer_a]);
2210        assert!(rig.fetcher.candidate_peers(&hashes[3]).is_empty());
2211        assert_eq!(rig.fetcher.candidate_peers(&hashes[4]), vec![peer_b]);
2212    }
2213
2214    #[test]
2215    fn later_announcers_are_asked_after_the_first_ones_failed() {
2216        let mut rig = Rig::new();
2217        let batch = hashes(0..3);
2218        let hash = batch[0];
2219        let peers =
2220            (1..=MAX_COUNT_CANDIDATE_PEERS_PER_HASH as u8 + 2).map(peer).collect::<Vec<_>>();
2221        for peer_id in &peers {
2222            rig.add_peer(*peer_id);
2223            rig.announce(*peer_id, &batch);
2224        }
2225
2226        // Keep the eager sources and rotate the oldest fallback sources out for later ones.
2227        let candidates = peers[..MAX_COUNT_EAGER_CANDIDATE_PEERS_PER_HASH]
2228            .iter()
2229            .chain(&peers[MAX_COUNT_EAGER_CANDIDATE_PEERS_PER_HASH + 2..])
2230            .copied()
2231            .collect::<Vec<_>>();
2232        assert_eq!(rig.fetcher.candidate_peers(&hash), candidates);
2233        for (i, peer_id) in candidates.iter().enumerate() {
2234            let eager = i < MAX_COUNT_EAGER_CANDIDATE_PEERS_PER_HASH;
2235            assert_eq!(rig.fetcher.queued_hashes(peer_id).contains(&hash), eager, "peer {i}");
2236        }
2237        assert!(rig.fetcher.queued_hashes(&peers[MAX_COUNT_CANDIDATE_PEERS_PER_HASH]).is_empty());
2238
2239        // the first announcer fetches the hash, retries go to the most recent announcers first
2240        assert_eq!(rig.dispatch(), 1);
2241        assert_eq!(rig.fetcher.fetching_peer(&hash), Some(peers[0]));
2242        rig.fail(peers[0], RequestError::Timeout);
2243        assert_eq!(rig.dispatch(), 1);
2244        assert_eq!(rig.fetcher.fetching_peer(&hash), Some(*candidates.last().unwrap()));
2245
2246        // the hash is given up on once all candidates failed
2247        let mut order = vec![peers[0]];
2248        while let Some(fetching) = rig.fetcher.fetching_peer(&hash) {
2249            rig.fail(fetching, RequestError::Timeout);
2250            order.push(fetching);
2251            rig.dispatch();
2252        }
2253        let expected = std::iter::once(candidates[0])
2254            .chain(candidates[1..].iter().rev().copied())
2255            .collect::<Vec<_>>();
2256        assert_eq!(order, expected);
2257        assert_eq!(rig.fetcher.num_hashes(), 0);
2258    }
2259
2260    #[test]
2261    fn old_request_error_does_not_settle_a_new_request_to_the_same_peer() {
2262        let config =
2263            TransactionFetcherConfig { max_inflight_requests_per_peer: 2, ..Default::default() };
2264        let mut rig = Rig::with_config(config);
2265        let p = peer(1);
2266        let h = hash(1);
2267        rig.add_peer(p);
2268        rig.announce(p, &[h]);
2269        rig.dispatch();
2270        let (_, first) = rig.take_request(p).unwrap();
2271        rig.fetcher.on_transactions_received([&h]);
2272        rig.announce(p, &[h]);
2273        rig.dispatch();
2274        let (_, second) = rig.take_request(p).unwrap();
2275        first.send(Err(RequestError::Timeout)).unwrap();
2276        rig.next_event().unwrap();
2277        assert_eq!(rig.fetcher.num_fetching_hashes(), 1);
2278        assert_eq!(rig.fetcher.fetching_peer(&h), Some(p));
2279        assert_eq!(rig.fetcher.candidate_peers(&h), vec![p]);
2280        second.send(Ok(PooledTransactions(vec![]))).unwrap();
2281        rig.next_event().unwrap();
2282        assert_eq!(rig.fetcher.num_hashes(), 0);
2283    }
2284
2285    #[test]
2286    fn first_timeout_retries_the_only_source_once() {
2287        let mut rig = Rig::new();
2288        let p = peer(1);
2289        let h = hash(1);
2290        rig.add_peer(p);
2291        rig.announce(p, &[h]);
2292        rig.dispatch();
2293        rig.fail(p, RequestError::Timeout);
2294        assert_eq!(rig.fetcher.candidate_peers(&h), vec![p]);
2295        assert_eq!(rig.dispatch(), 1);
2296        rig.fail(p, RequestError::Timeout);
2297        assert_eq!(rig.fetcher.num_hashes(), 0);
2298        assert_eq!(rig.dispatch(), 0);
2299    }
2300
2301    #[test]
2302    fn replacing_candidates_cannot_extend_the_fetch_attempt_budget() {
2303        let mut rig = Rig::new();
2304        let h = hash(1);
2305        for n in 1..=MAX_COUNT_CANDIDATE_PEERS_PER_HASH as u8 {
2306            rig.add_peer(peer(n));
2307            rig.announce(peer(n), &[h]);
2308        }
2309        for n in 0..MAX_FETCH_ATTEMPTS_PER_HASH {
2310            let newcomer = peer((n + MAX_COUNT_CANDIDATE_PEERS_PER_HASH + 1) as u8);
2311            rig.add_peer(newcomer);
2312            rig.announce(newcomer, &[h]);
2313            assert_eq!(rig.dispatch(), 1);
2314            let fetching = rig.fetcher.fetching_peer(&h).unwrap();
2315            rig.fail(fetching, RequestError::Timeout);
2316        }
2317        assert_eq!(rig.fetcher.num_hashes(), 0);
2318    }
2319
2320    #[test]
2321    fn self_eviction_skips_shared_and_stale_announcement_entries() {
2322        let mut rig = Rig::with_config(TransactionFetcherConfig {
2323            max_capacity_cache_txns_pending_fetch: 4,
2324            ..Default::default()
2325        });
2326        let honest = peer(1);
2327        let flooder = peer(2);
2328        for p in [honest, flooder] {
2329            rig.add_peer(p);
2330        }
2331        rig.announce(honest, &hashes(0..2));
2332        rig.announce(flooder, &hashes(10..12));
2333        // The shared cursor entry must not hide newly queued exclusive victims.
2334        rig.announce(flooder, &[hash(0), hash(12), hash(13), hash(14), hash(15), hash(16)]);
2335        assert_eq!(rig.fetcher.candidate_peers(&hash(1)), vec![honest]);
2336        assert_eq!(rig.fetcher.candidate_peers(&hash(0)), vec![honest, flooder]);
2337        assert_eq!(rig.fetcher.num_hashes(), 4);
2338    }
2339
2340    #[test]
2341    fn stale_queue_entries_do_not_hide_exclusive_eviction_victims() {
2342        let mut rig = Rig::with_config(TransactionFetcherConfig {
2343            max_capacity_cache_txns_pending_fetch: 16,
2344            ..Default::default()
2345        });
2346        let honest = peer(1);
2347        let flooder = peer(2);
2348        let newcomer = peer(3);
2349        for p in [honest, flooder, newcomer] {
2350            rig.add_peer(p);
2351        }
2352        rig.announce(honest, &[hash(0)]);
2353        rig.announce(flooder, &hashes(1..16));
2354        rig.fetcher.on_transactions_received(hashes(1..9).iter());
2355        rig.announce(flooder, &hashes(16..24));
2356        rig.announce(newcomer, &[hash(24)]);
2357        assert_eq!(rig.fetcher.candidate_peers(&hash(0)), vec![honest]);
2358        assert!(rig.fetcher.candidate_peers(&hash(9)).is_empty());
2359    }
2360
2361    #[test]
2362    fn capacity_eviction_targets_the_peer_with_the_most_hashes() {
2363        let config = TransactionFetcherConfig {
2364            max_capacity_cache_txns_pending_fetch: 100,
2365            max_announced_hashes_per_peer: 60,
2366            ..Default::default()
2367        };
2368        let mut rig = Rig::with_config(config);
2369        let honest = peer(1);
2370        let flooders = [peer(2), peer(3)];
2371        rig.add_peer(honest);
2372        for peer_id in flooders {
2373            rig.add_peer(peer_id);
2374        }
2375
2376        // the honest peer's hashes are the oldest when the flood fills the fetcher
2377        let honest_hashes = hashes(0..10);
2378        rig.announce(honest, &honest_hashes);
2379        rig.announce(flooders[0], &hashes(100..160));
2380        rig.announce(flooders[1], &hashes(200..260));
2381
2382        assert_eq!(rig.fetcher.num_hashes(), 100);
2383        for hash in &honest_hashes {
2384            assert_eq!(rig.fetcher.candidate_peers(hash), vec![honest], "{hash} was evicted");
2385        }
2386        assert_eq!(rig.fetcher.queued_hashes(&honest), honest_hashes);
2387
2388        // the flooders gave up their oldest hashes
2389        assert!(rig.fetcher.candidate_peers(&hashes(100..101)[0]).is_empty());
2390        assert_eq!(rig.fetcher.candidate_peers(&hashes(259..260)[0]), vec![flooders[1]]);
2391    }
2392
2393    #[test]
2394    fn shared_queue_prefix_does_not_hide_exclusive_eviction_victim() {
2395        let mut rig = Rig::with_config(TransactionFetcherConfig {
2396            max_capacity_cache_txns_pending_fetch: 10,
2397            ..Default::default()
2398        });
2399        let honest = peer(1);
2400        let flooder = peer(2);
2401        let newcomer = peer(3);
2402        for p in [honest, flooder, newcomer] {
2403            rig.add_peer(p);
2404        }
2405        rig.announce(honest, &hashes(0..9));
2406        rig.announce(flooder, &hashes(0..8));
2407        rig.announce(flooder, &[hash(9)]);
2408        rig.announce(newcomer, &[hash(10)]);
2409        for h in hashes(0..8) {
2410            assert_eq!(rig.fetcher.candidate_peers(&h), vec![honest, flooder]);
2411        }
2412        assert!(rig.fetcher.candidate_peers(&hash(9)).is_empty());
2413        assert_eq!(rig.fetcher.candidate_peers(&hash(10)), vec![newcomer]);
2414    }
2415
2416    #[test]
2417    fn capacity_fallback_keeps_the_announcer_ready() {
2418        let mut rig = Rig::with_config(TransactionFetcherConfig {
2419            max_capacity_cache_txns_pending_fetch: 9,
2420            ..Default::default()
2421        });
2422        let stalled = peer(1);
2423        let source = peer(2);
2424        let announcer = peer(3);
2425        for p in [stalled, source, announcer] {
2426            rig.add_peer(p);
2427        }
2428        rig.announce(stalled, &hashes(0..8));
2429        rig.dispatch();
2430        rig.announce(source, &[hash(8)]);
2431        // The global scan sees only fetching hashes. The last new hash is dropped after
2432        // the shared scratch entries have been moved into the announcer's real queue.
2433        rig.announce(announcer, &hashes(0..10));
2434        rig.disconnect(source);
2435        assert_eq!(rig.dispatch(), 1);
2436        assert_eq!(rig.take_request(announcer).unwrap().0, vec![hash(8)]);
2437    }
2438
2439    #[test]
2440    fn capacity_eviction_preserves_coannounced_hashes() {
2441        let config = TransactionFetcherConfig {
2442            max_capacity_cache_txns_pending_fetch: 4,
2443            ..Default::default()
2444        };
2445        let mut rig = Rig::with_config(config);
2446        let honest = peer(1);
2447        let flooder = peer(2);
2448        let newcomer = peer(3);
2449        for peer_id in [honest, flooder, newcomer] {
2450            rig.add_peer(peer_id);
2451        }
2452        let shared = hash(0);
2453        rig.announce(honest, &[shared]);
2454        rig.announce(flooder, &hashes(0..4));
2455        rig.announce(newcomer, &[hash(4)]);
2456        assert_eq!(rig.fetcher.num_hashes(), 4);
2457        assert_eq!(rig.fetcher.candidate_peers(&shared), vec![honest, flooder]);
2458        assert!(rig.fetcher.candidate_peers(&hash(1)).is_empty());
2459        assert_eq!(rig.fetcher.candidate_peers(&hash(4)), vec![newcomer]);
2460        rig.dispatch();
2461        assert_eq!(rig.fetcher.fetching_peer(&shared), Some(honest));
2462        assert_eq!(rig.take_request(flooder).unwrap().0, vec![hash(2), hash(3)]);
2463    }
2464
2465    #[test]
2466    fn capacity_fallback_moves_past_fetching_hashes() {
2467        let mut rig = Rig::with_config(TransactionFetcherConfig {
2468            max_capacity_cache_txns_pending_fetch: 24,
2469            ..Default::default()
2470        });
2471        let [fetching, first, second, coannouncer, honest] = [1, 2, 3, 4, 5].map(peer);
2472        for p in [fetching, first, second, coannouncer, honest] {
2473            rig.add_peer(p);
2474        }
2475        // the oldest hashes are inflight and the busiest peer has no exclusive hashes
2476        rig.announce(fetching, &hashes(0..8));
2477        rig.dispatch();
2478        rig.announce(first, &hashes(8..16));
2479        rig.announce(second, &hashes(16..24));
2480        rig.announce(coannouncer, &hashes(8..24));
2481
2482        // the first search only gets past the fetching hashes, later ones evict pending hashes
2483        rig.announce(honest, &hashes(24..28));
2484        assert!(rig.fetcher.candidate_peers(&hash(24)).is_empty());
2485        for h in hashes(25..28) {
2486            assert_eq!(rig.fetcher.candidate_peers(&h), vec![honest]);
2487        }
2488        for h in hashes(8..11) {
2489            assert!(rig.fetcher.candidate_peers(&h).is_empty());
2490        }
2491        assert_eq!(rig.fetcher.num_fetching_hashes(), 8);
2492        rig.dispatch();
2493        assert_eq!(rig.take_request(honest).unwrap().0, hashes(25..28));
2494    }
2495
2496    #[test]
2497    fn capacity_with_only_shared_hashes_admits_new_announcements() {
2498        let config = TransactionFetcherConfig {
2499            max_capacity_cache_txns_pending_fetch: 2,
2500            ..Default::default()
2501        };
2502        let mut rig = Rig::with_config(config);
2503        for peer_id in [peer(1), peer(2), peer(3)] {
2504            rig.add_peer(peer_id);
2505        }
2506        for peer_id in [peer(1), peer(2)] {
2507            rig.announce(peer_id, &hashes(0..2));
2508        }
2509        rig.announce(peer(3), &[hash(2)]);
2510        assert_eq!(rig.fetcher.num_hashes(), 2);
2511        assert_eq!(rig.fetcher.candidate_peers(&hash(2)), vec![peer(3)]);
2512        assert!(rig.fetcher.candidate_peers(&hash(0)).is_empty());
2513        assert_eq!(rig.fetcher.candidate_peers(&hash(1)), vec![peer(1), peer(2)]);
2514        rig.dispatch();
2515        assert_eq!(rig.fetcher.num_fetching_hashes(), 2);
2516    }
2517
2518    #[test]
2519    fn retracked_hash_is_not_evicted_at_its_stale_queue_position() {
2520        let mut rig = Rig::with_config(TransactionFetcherConfig {
2521            max_capacity_cache_txns_pending_fetch: 3,
2522            ..Default::default()
2523        });
2524        let p = peer(1);
2525        rig.add_peer(p);
2526        rig.announce(p, &hashes(0..3));
2527        rig.fetcher.on_transactions_received([&hash(0)]);
2528        rig.announce(p, &[hash(0)]);
2529        rig.announce(p, &[hash(3)]);
2530        assert_eq!(rig.fetcher.candidate_peers(&hash(0)), vec![p]);
2531        assert!(rig.fetcher.candidate_peers(&hash(1)).is_empty());
2532        rig.dispatch();
2533        assert_eq!(rig.take_request(p).unwrap().0, vec![hash(2), hash(0), hash(3)]);
2534    }
2535
2536    #[test]
2537    fn shared_fallback_ignores_stale_tracking_generations() {
2538        let mut rig = Rig::with_config(TransactionFetcherConfig {
2539            max_capacity_cache_txns_pending_fetch: 3,
2540            ..Default::default()
2541        });
2542        for p in [peer(1), peer(2), peer(3)] {
2543            rig.add_peer(p);
2544        }
2545        for p in [peer(1), peer(2)] {
2546            rig.announce(p, &hashes(0..3));
2547        }
2548        rig.fetcher.on_transactions_received([&hash(0)]);
2549        for p in [peer(1), peer(2)] {
2550            rig.announce(p, &[hash(0)]);
2551        }
2552        rig.announce(peer(3), &[hash(3)]);
2553        assert_eq!(rig.fetcher.candidate_peers(&hash(0)), vec![peer(1), peer(2)]);
2554        assert!(rig.fetcher.candidate_peers(&hash(1)).is_empty());
2555    }
2556
2557    #[test]
2558    fn eviction_order_stays_bounded_when_hashes_are_tracked_again() {
2559        let config = TransactionFetcherConfig {
2560            max_capacity_cache_txns_pending_fetch: 8,
2561            ..Default::default()
2562        };
2563        let mut rig = Rig::with_config(config);
2564        let peer_a = peer(1);
2565        rig.add_peer(peer_a);
2566        let batch = hashes(0..8);
2567
2568        // the same hashes are given up on and announced again over and over, every round leaves
2569        // stale entries in the eviction order behind
2570        for _ in 0..10 {
2571            rig.announce(peer_a, &batch);
2572            assert_eq!(rig.fetcher.num_hashes(), 8);
2573            rig.dispatch();
2574            rig.fail(peer_a, RequestError::BadResponse);
2575            assert_eq!(rig.fetcher.num_hashes(), 0);
2576        }
2577        rig.announce(peer_a, &batch);
2578        assert_eq!(rig.fetcher.num_hashes(), 8);
2579
2580        // a new hash still evicts the oldest one rather than getting dropped
2581        rig.announce(peer_a, &hashes(8..9));
2582        assert_eq!(rig.fetcher.num_hashes(), 8);
2583        assert!(rig.fetcher.candidate_peers(&batch[0]).is_empty());
2584    }
2585
2586    #[test]
2587    fn disconnected_remembered_candidates_are_forgotten() {
2588        let mut rig = Rig::new();
2589        let hash = hash(1);
2590        let peers = (1..=MAX_COUNT_CANDIDATE_PEERS_PER_HASH as u8).map(peer).collect::<Vec<_>>();
2591        for peer_id in &peers {
2592            rig.add_peer(*peer_id);
2593            rig.announce(*peer_id, &[hash]);
2594        }
2595        let (queued, remembered) = peers.split_at(MAX_COUNT_EAGER_CANDIDATE_PEERS_PER_HASH);
2596
2597        // remembered candidates have no queue entry, they are forgotten nonetheless
2598        for peer_id in remembered {
2599            rig.disconnect(*peer_id);
2600        }
2601        assert_eq!(rig.fetcher.candidate_peers(&hash), queued);
2602
2603        // which frees their slots for later announcers
2604        let newcomer = peer(42);
2605        rig.add_peer(newcomer);
2606        rig.announce(newcomer, &[hash]);
2607        assert!(rig.fetcher.candidate_peers(&hash).contains(&newcomer));
2608
2609        // and a hash whose last connected candidate leaves is dropped
2610        for peer_id in queued {
2611            rig.disconnect(*peer_id);
2612        }
2613        rig.disconnect(newcomer);
2614        assert_eq!(rig.fetcher.num_hashes(), 0);
2615    }
2616
2617    #[test]
2618    fn remembered_candidates_take_over_when_queued_ones_disconnect() {
2619        let mut rig = Rig::new();
2620        let hash = hash(1);
2621        let peers =
2622            (1..=MAX_COUNT_EAGER_CANDIDATE_PEERS_PER_HASH as u8 + 1).map(peer).collect::<Vec<_>>();
2623        for peer_id in &peers {
2624            rig.add_peer(*peer_id);
2625            rig.announce(*peer_id, &[hash]);
2626        }
2627        let last = *peers.last().unwrap();
2628        assert!(rig.fetcher.queued_hashes(&last).is_empty());
2629
2630        // all peers that had the hash queued disconnect before fetching it
2631        for peer_id in &peers[..peers.len() - 1] {
2632            rig.disconnect(*peer_id);
2633        }
2634        assert_eq!(rig.fetcher.candidate_peers(&hash), vec![last]);
2635        assert_eq!(rig.fetcher.queued_hashes(&last), vec![hash]);
2636        assert_eq!(rig.dispatch(), 1);
2637        assert_eq!(rig.fetcher.fetching_peer(&hash), Some(last));
2638    }
2639
2640    #[test]
2641    fn global_inflight_limit_defers_ready_peers() {
2642        let config = TransactionFetcherConfig { max_inflight_requests: 1, ..Default::default() };
2643        let mut rig = Rig::with_config(config);
2644        let peer_a = peer(1);
2645        let peer_b = peer(2);
2646        rig.add_peer(peer_a);
2647        rig.add_peer(peer_b);
2648
2649        rig.announce(peer_a, &hashes(0..2));
2650        rig.announce(peer_b, &hashes(2..4));
2651
2652        assert_eq!(rig.dispatch(), 1);
2653        let (_, response) = rig.take_request(peer_a).unwrap();
2654        assert!(rig.take_request(peer_b).is_none());
2655        assert_eq!(rig.dispatch(), 0);
2656
2657        // the deferred peer is served once a request slot frees up
2658        rig.announce(peer_a, &hashes(4..5));
2659        drop(response);
2660        rig.next_event().unwrap();
2661        assert_eq!(rig.dispatch(), 1);
2662        assert!(rig.take_request(peer_b).is_some());
2663        assert!(rig.take_request(peer_a).is_none(), "peer_b was ready first");
2664    }
2665
2666    #[test]
2667    fn dispatch_applies_the_request_limit_independently_to_each_peer() {
2668        let mut rig = Rig::new();
2669        let peer_a = peer(1);
2670        let peer_b = peer(2);
2671        rig.add_peer(peer_a);
2672        rig.add_peer(peer_b);
2673
2674        rig.announce(peer_a, &hashes(0..100));
2675        rig.announce(peer_b, &hashes(100..200));
2676
2677        assert_eq!(rig.dispatch_with_budget(0), 0);
2678
2679        // Each peer can use the full request limit regardless of other inflight requests.
2680        assert_eq!(rig.dispatch_with_budget(40), 2);
2681        let (requested, response_a) = rig.take_request(peer_a).unwrap();
2682        assert_eq!(requested, hashes(0..40));
2683        let (requested, _) = rig.take_request(peer_b).unwrap();
2684        assert_eq!(requested, hashes(100..140));
2685        assert_eq!(rig.fetcher.num_fetching_hashes(), 80);
2686
2687        // A smaller request limit is respected too.
2688        response_a.send(Err(RequestError::BadResponse)).unwrap();
2689        rig.next_event().unwrap();
2690        assert_eq!(rig.dispatch_with_budget(10), 1);
2691        let (requested, _) = rig.take_request(peer_a).unwrap();
2692        assert_eq!(requested, hashes(40..50));
2693    }
2694
2695    #[test]
2696    fn full_session_channel_rolls_back_request() {
2697        let mut rig = Rig::new();
2698        let peer_a = peer(1);
2699        rig.add_peer_with_capacity(peer_a, 1);
2700        let hashes = hashes(0..3);
2701
2702        // occupy the only slot of the session channel
2703        let (blocker, _rx) = oneshot::channel();
2704        rig.peers[&peer_a]
2705            .request_tx()
2706            .try_send(PeerRequest::GetPooledTransactions {
2707                request: GetPooledTransactions(vec![]),
2708                response: blocker,
2709            })
2710            .unwrap();
2711
2712        rig.announce(peer_a, &hashes);
2713        assert_eq!(rig.dispatch(), 0);
2714        assert_eq!(rig.fetcher.num_pending_hashes(), 3);
2715        assert_eq!(rig.fetcher.num_inflight_requests(), 0);
2716        assert!(rig.fetcher.is_idle(&peer_a));
2717        assert_eq!(rig.fetcher.queued_hashes(&peer_a), hashes);
2718
2719        // drain the blocker, the peer is retried on the next dispatch
2720        let (blocked, _) = rig.take_request(peer_a).unwrap();
2721        assert!(blocked.is_empty());
2722        assert_eq!(rig.dispatch(), 1);
2723        let (requested, _) = rig.take_request(peer_a).unwrap();
2724        assert_eq!(requested, hashes);
2725    }
2726
2727    #[test]
2728    fn closed_session_channel_disconnects_peer() {
2729        let mut rig = Rig::new();
2730        let peer_a = peer(1);
2731        let peer_b = peer(2);
2732        rig.add_peer(peer_a);
2733        rig.add_peer(peer_b);
2734        let hashes = hashes(0..2);
2735
2736        rig.announce(peer_a, &hashes);
2737        rig.announce(peer_b, &hashes[..1]);
2738        // the session task is gone, but the manager didn't process the disconnect yet
2739        rig.sessions.remove(&peer_a);
2740
2741        assert_eq!(rig.dispatch(), 1);
2742        assert!(rig.fetcher.candidate_peers(&hashes[0]).contains(&peer_b));
2743        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_b]);
2744        assert_eq!(rig.fetcher.num_hashes(), 1, "hash only announced by the gone peer is dropped");
2745        assert_eq!(rig.fetcher.fetching_peer(&hashes[0]), Some(peer_b));
2746    }
2747
2748    #[test]
2749    fn busy_peer_queue_is_compacted() {
2750        let config =
2751            TransactionFetcherConfig { max_announced_hashes_per_peer: 4, ..Default::default() };
2752        let mut rig = Rig::with_config(config);
2753        let peer_a = peer(1);
2754        let peer_b = peer(2);
2755        rig.add_peer(peer_a);
2756        rig.add_peer(peer_b);
2757
2758        // keep peer_a busy
2759        rig.announce(peer_a, &hashes(0..1));
2760        rig.dispatch();
2761
2762        // hashes announced by the busy peer are delivered by others over and over
2763        for round in 1..20u64 {
2764            let hashes = hashes(round * 10..round * 10 + 4);
2765            rig.announce(peer_a, &hashes);
2766            rig.fetcher.on_transactions_received(&hashes);
2767            rig.fetcher.assert_invariants();
2768        }
2769        assert!(rig.fetcher.queued_hashes(&peer_a).len() <= 8);
2770    }
2771
2772    #[test]
2773    fn late_response_for_reassigned_hash_is_ignored() {
2774        let mut rig = Rig::new();
2775        let peer_a = peer(1);
2776        let peer_b = peer(2);
2777        rig.add_peer(peer_a);
2778        rig.add_peer(peer_b);
2779        let txs = pooled_txs(1);
2780        let hashes = hashes_of(&txs);
2781
2782        rig.announce(peer_a, &hashes);
2783        rig.dispatch();
2784        // arrives over broadcast, gets announced again and is assigned to another peer
2785        rig.fetcher.on_transactions_received(&hashes);
2786        rig.announce(peer_b, &hashes);
2787        rig.dispatch();
2788        assert_eq!(rig.fetcher.fetching_peer(&hashes[0]), Some(peer_b));
2789
2790        // the original request resolves without the hash, which must not touch the new fetch
2791        rig.respond(peer_a, vec![]);
2792        assert_eq!(rig.fetcher.fetching_peer(&hashes[0]), Some(peer_b));
2793        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_b]);
2794
2795        rig.respond(peer_b, txs);
2796        assert_eq!(rig.fetcher.num_hashes(), 0);
2797    }
2798
2799    #[test]
2800    fn random_operations_keep_invariants() {
2801        let mut rng = StdRng::seed_from_u64(0x5eed);
2802        let txs = pooled_txs(150);
2803        let all_hashes = hashes_of(&txs);
2804        let by_hash = txs.iter().map(|tx| (*tx.tx_hash(), tx.clone())).collect::<B256Map<_>>();
2805        // more peers than a hash has candidate slots, so announcers get remembered and rejected
2806        let peer_ids = (1..=24).map(peer).collect::<Vec<_>>();
2807
2808        let config = TransactionFetcherConfig {
2809            max_inflight_requests: 8,
2810            max_inflight_requests_per_peer: 2,
2811            max_capacity_cache_txns_pending_fetch: 120,
2812            max_announced_hashes_per_peer: 40,
2813            ..Default::default()
2814        };
2815        let mut rig = Rig::with_config(config);
2816        for peer_id in &peer_ids {
2817            rig.add_peer_with_capacity(*peer_id, 4);
2818        }
2819        let mut connected = peer_ids.clone();
2820        // requests taken from the sessions that weren't answered yet
2821        let mut outstanding: Vec<(PeerId, Vec<TxHash>, ResponseSender)> = Vec::new();
2822
2823        for _ in 0..5000 {
2824            match rng.random_range(0..100u32) {
2825                0..=39 => {
2826                    let Some(&peer_id) = connected.choose(&mut rng) else { continue };
2827                    let count = rng.random_range(1..=30);
2828                    let entries = all_hashes
2829                        .choose_multiple(&mut rng, count)
2830                        .map(|hash| {
2831                            let size = match rng.random_range(0..10) {
2832                                0 => 0,
2833                                1 => 200 * KIB,
2834                                _ => rng.random_range(100..1500),
2835                            };
2836                            (*hash, size)
2837                        })
2838                        .collect::<Vec<_>>();
2839                    rig.announce_with_sizes(peer_id, entries);
2840                }
2841                40..=59 => {
2842                    let budget =
2843                        if rng.random_bool(0.2) { rng.random_range(0..60) } else { usize::MAX };
2844                    rig.dispatch_with_budget(budget);
2845                    for peer_id in &connected {
2846                        while let Some((requested, response)) = rig.take_request(*peer_id) {
2847                            assert!(!requested.is_empty());
2848                            assert!(
2849                                requested.len() <=
2850                                    SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST
2851                            );
2852                            let unique = requested.iter().copied().collect::<B256Set>();
2853                            assert_eq!(unique.len(), requested.len(), "request has duplicates");
2854                            outstanding.push((*peer_id, requested, response));
2855                        }
2856                    }
2857                }
2858                60..=84 => {
2859                    if outstanding.is_empty() {
2860                        continue
2861                    }
2862                    let (_, requested, response) =
2863                        outstanding.swap_remove(rng.random_range(0..outstanding.len()));
2864                    match rng.random_range(0..10) {
2865                        0..=5 => {
2866                            let mut delivered = requested
2867                                .iter()
2868                                .filter(|_| rng.random_bool(0.7))
2869                                .map(|hash| by_hash[hash].clone())
2870                                .collect::<Vec<_>>();
2871                            if rng.random_bool(0.1) &&
2872                                let Some(duplicate) = delivered.first().cloned()
2873                            {
2874                                delivered.push(duplicate);
2875                            }
2876                            if rng.random_bool(0.1) {
2877                                delivered.push(txs.choose(&mut rng).unwrap().clone());
2878                            }
2879                            let _ = response.send(Ok(PooledTransactions(delivered)));
2880                        }
2881                        6..=7 => {
2882                            let _ = response.send(Ok(PooledTransactions(vec![])));
2883                        }
2884                        8 => {
2885                            let _ = response.send(Err(RequestError::Timeout));
2886                        }
2887                        _ => drop(response),
2888                    }
2889                    rig.drain_events();
2890                }
2891                85..=89 => {
2892                    let count = rng.random_range(1..=10);
2893                    let received =
2894                        all_hashes.choose_multiple(&mut rng, count).copied().collect::<Vec<_>>();
2895                    rig.fetcher.on_transactions_received(&received);
2896                    rig.fetcher.assert_invariants();
2897                }
2898                90..=94 => {
2899                    if connected.len() > 1 {
2900                        let peer_id = connected.swap_remove(rng.random_range(0..connected.len()));
2901                        rig.disconnect(peer_id);
2902                        // drops the response senders of the peer's outstanding requests
2903                        outstanding.retain(|(id, ..)| *id != peer_id);
2904                        rig.drain_events();
2905                    }
2906                }
2907                _ => {
2908                    if let Some(&peer_id) = peer_ids.iter().find(|id| !connected.contains(id)) {
2909                        rig.add_peer_with_capacity(peer_id, 4);
2910                        connected.push(peer_id);
2911                    }
2912                }
2913            }
2914        }
2915
2916        // settle everything that is still inflight
2917        drop(outstanding);
2918        for peer_id in &connected {
2919            rig.disconnect(*peer_id);
2920        }
2921        rig.drain_events();
2922        assert_eq!(rig.fetcher.num_inflight_requests(), 0);
2923        assert_eq!(rig.fetcher.num_fetching_hashes(), 0);
2924        assert_eq!(rig.fetcher.num_hashes(), 0);
2925        assert_eq!(rig.fetcher.num_peers(), 0);
2926    }
2927    #[test]
2928    fn resolved_requests_are_yielded_one_per_poll() {
2929        let mut rig = Rig::new();
2930        let peers = (1..=3).map(peer).collect::<Vec<_>>();
2931        for (i, peer_id) in peers.iter().enumerate() {
2932            rig.add_peer(*peer_id);
2933            rig.announce(*peer_id, &hashes(i as u64..i as u64 + 1));
2934        }
2935        assert_eq!(rig.dispatch(), 3);
2936        for peer_id in &peers {
2937            let (_, response) = rig.take_request(*peer_id).unwrap();
2938            response.send(Err(RequestError::Timeout)).unwrap();
2939        }
2940
2941        let events = rig.drain_events();
2942        assert_eq!(events.len(), 3);
2943        assert!(events.iter().all(|event| matches!(event, FetchEvent::FetchError { .. })));
2944        assert!(rig.next_event().is_none());
2945        assert_eq!(rig.fetcher.num_inflight_requests(), 0);
2946    }
2947
2948    #[test]
2949    fn global_capacity_bounds_tracked_hashes_across_peers() {
2950        let config = TransactionFetcherConfig {
2951            max_capacity_cache_txns_pending_fetch: 40,
2952            ..Default::default()
2953        };
2954        let mut rig = Rig::with_config(config);
2955        let peers = (1..=4).map(peer).collect::<Vec<_>>();
2956        for (i, peer_id) in peers.iter().enumerate() {
2957            rig.add_peer(*peer_id);
2958            rig.announce_unsized(*peer_id, &hashes(i as u64 * 16..(i as u64 + 1) * 16));
2959        }
2960
2961        // 24 of the 64 announced hashes were evicted, spread over the peers so that every
2962        // peer lost its oldest hashes and kept its newest
2963        assert_eq!(rig.fetcher.num_hashes(), 40);
2964        for (i, peer_id) in peers.iter().enumerate() {
2965            let first = i as u64 * 16;
2966            assert!(rig.fetcher.candidate_peers(&hash(first)).is_empty());
2967            assert!(rig.fetcher.candidate_peers(&hash(first + 3)).is_empty());
2968            assert_eq!(rig.fetcher.candidate_peers(&hash(first + 8)), vec![*peer_id]);
2969            assert_eq!(rig.fetcher.candidate_peers(&hash(first + 15)), vec![*peer_id]);
2970        }
2971        rig.fetcher.assert_invariants();
2972
2973        // every peer still has hashes to request, the evicted ones are skipped
2974        assert_eq!(rig.dispatch(), 4);
2975        let mut requested_total = 0;
2976        for peer_id in &peers {
2977            let (requested, _) = rig.take_request(*peer_id).unwrap();
2978            assert!(requested.len() <= 256);
2979            requested_total += requested.len();
2980        }
2981        assert_eq!(requested_total, 40, "every remaining hash is requested");
2982        rig.fetcher.assert_invariants();
2983    }
2984
2985    #[test]
2986    fn delivery_order_does_not_matter() {
2987        let mut rig = Rig::new();
2988        let peer_a = peer(1);
2989        let peer_b = peer(2);
2990        rig.add_peer(peer_a);
2991        rig.add_peer(peer_b);
2992        let txs = pooled_txs(4);
2993        let hashes = hashes_of(&txs);
2994
2995        rig.announce(peer_a, &hashes);
2996        rig.announce(peer_b, &hashes);
2997        rig.dispatch();
2998
2999        // the last and the first hash are delivered in reverse order, the two in between were
3000        // skipped by peer_a
3001        let event = rig.respond(peer_a, vec![txs[3].clone(), txs[0].clone()]);
3002        let FetchEvent::TransactionsFetched { transactions, .. } = event else { panic!() };
3003        assert_eq!(transactions.len(), 2);
3004        assert_eq!(rig.fetcher.num_hashes(), 2);
3005        assert_eq!(rig.fetcher.candidate_peers(&hashes[1]), vec![peer_b]);
3006        assert_eq!(rig.fetcher.candidate_peers(&hashes[2]), vec![peer_b]);
3007        assert_eq!(rig.fetcher.queued_hashes(&peer_b), hashes[1..3]);
3008    }
3009
3010    #[test]
3011    fn partial_delivery_with_concurrent_requests_per_peer() {
3012        let config =
3013            TransactionFetcherConfig { max_inflight_requests_per_peer: 2, ..Default::default() };
3014        let mut rig = Rig::with_config(config);
3015        let peer_a = peer(1);
3016        rig.add_peer(peer_a);
3017        let txs = pooled_txs(300);
3018        let hashes = hashes_of(&txs);
3019
3020        rig.announce(peer_a, &hashes);
3021        assert_eq!(rig.dispatch(), 2);
3022        let (first, response_first) = rig.take_request(peer_a).unwrap();
3023        let (second, response_second) = rig.take_request(peer_a).unwrap();
3024        assert_eq!(first, hashes[..256]);
3025        assert_eq!(second, hashes[256..]);
3026
3027        // only the last hash of the first request is delivered, the rest was skipped and has no
3028        // other candidate
3029        response_first.send(Ok(PooledTransactions(txs[255..256].to_vec()))).unwrap();
3030        rig.next_event().unwrap();
3031        assert_eq!(rig.fetcher.num_hashes(), 44);
3032        assert_eq!(rig.fetcher.num_fetching_hashes(), 44);
3033        assert!(!rig.fetcher.is_idle(&peer_a));
3034
3035        response_second.send(Ok(PooledTransactions(txs[256..].to_vec()))).unwrap();
3036        rig.next_event().unwrap();
3037        assert_eq!(rig.fetcher.num_hashes(), 0);
3038        assert!(rig.fetcher.is_idle(&peer_a));
3039    }
3040
3041    #[test]
3042    fn old_request_resolves_after_peer_reconnected_and_reannounced() {
3043        let mut rig = Rig::new();
3044        let peer_a = peer(1);
3045        rig.add_peer(peer_a);
3046        let txs = pooled_txs(2);
3047        let hashes = hashes_of(&txs);
3048
3049        rig.announce(peer_a, &hashes);
3050        rig.dispatch();
3051        let (_, response) = rig.take_request(peer_a).unwrap();
3052
3053        // the peer reconnects and announces the hashes again while the old request is pending
3054        rig.disconnect(peer_a);
3055        rig.add_peer(peer_a);
3056        rig.announce(peer_a, &hashes);
3057        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_a]);
3058        assert_eq!(rig.fetcher.num_fetching_hashes(), 2);
3059        assert_eq!(rig.dispatch(), 0, "the hashes are still being fetched");
3060
3061        // the old request delivers one hash and skips the other, which is retried with the new
3062        // session
3063        response.send(Ok(PooledTransactions(txs[1..].to_vec()))).unwrap();
3064        rig.next_event().unwrap();
3065        assert_eq!(rig.fetcher.num_hashes(), 1);
3066        assert_eq!(rig.fetcher.candidate_peers(&hashes[0]), vec![peer_a]);
3067        assert_eq!(rig.dispatch(), 1);
3068        let (requested, _) = rig.take_request(peer_a).unwrap();
3069        assert_eq!(requested, hashes[..1]);
3070    }
3071
3072    #[test]
3073    fn requeued_hashes_are_not_queued_twice() {
3074        let mut rig = Rig::new();
3075        let peer_a = peer(1);
3076        let peer_b = peer(2);
3077        rig.add_peer(peer_a);
3078        rig.add_peer(peer_b);
3079        let hashes = hashes(0..3);
3080
3081        // peer_b is busy while both announce the hashes, so they stay in its queue
3082        rig.announce(peer_b, &hashes[..1]);
3083        rig.dispatch();
3084        rig.announce(peer_a, &hashes[1..]);
3085        rig.announce(peer_b, &hashes[1..]);
3086        rig.dispatch();
3087        assert_eq!(rig.fetcher.fetching_peer(&hashes[1]), Some(peer_a));
3088        assert_eq!(rig.fetcher.queued_hashes(&peer_b), hashes[1..]);
3089
3090        rig.fail(peer_a, RequestError::Timeout);
3091        assert_eq!(rig.fetcher.queued_hashes(&peer_b), hashes[1..], "no duplicates");
3092    }
3093}