Skip to main content

reth_network/transactions/
fetcher.rs

1//! `TransactionFetcher` is responsible for rate limiting and retry logic for fetching
2//! transactions. Upon receiving an announcement, functionality of the `TransactionFetcher` is
3//! used for filtering out hashes 1) for which the tx is already known and 2) unknown but the hash
4//! is already seen in a previous announcement. The hashes that remain from an announcement are
5//! then packed into a request with respect to the [`EthVersion`] of the announcement. Any hashes
6//! that don't fit into the request, are buffered in the `TransactionFetcher`. If on the other
7//! hand, space remains, hashes that the peer has previously announced are taken out of buffered
8//! hashes to fill the request up. The [`GetPooledTransactions`] request is then sent to the
9//! peer's session, this marks the peer as active with respect to
10//! `MAX_CONCURRENT_TX_REQUESTS_PER_PEER`.
11//!
12//! When a peer buffers hashes in the `TransactionsManager::on_new_pooled_transaction_hashes`
13//! pipeline, it is stored as fallback peer for those hashes. When [`TransactionsManager`] is
14//! polled, it checks if any of fallback peer is idle. If so, it packs a request for that peer,
15//! filling it from the buffered hashes. It does so until there are no more idle peers or until
16//! the hashes buffer is empty.
17//!
18//! If a [`GetPooledTransactions`] request resolves with an error, the hashes in the request are
19//! buffered with respect to `MAX_REQUEST_RETRIES_PER_TX_HASH`. So is the case if the request
20//! resolves with partial success, that is some of the requested hashes are not in the response,
21//! these are then buffered.
22//!
23//! Most healthy peers will send the same hashes in their announcements, as RLPx is a gossip
24//! protocol. This means it's unlikely, that a valid hash, will be buffered for very long
25//! before it's re-tried. Nonetheless, the capacity of the buffered hashes cache must be large
26//! enough to buffer many hashes during network failure, to allow for recovery.
27
28use super::{
29    config::TransactionFetcherConfig,
30    constants::{tx_fetcher::*, SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST},
31    PeerMetadata, PooledTransactions, SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
32};
33use crate::{
34    cache::{LruCache, LruMap},
35    duration_metered_exec,
36    metrics::TransactionFetcherMetrics,
37};
38use alloy_consensus::transaction::PooledTransaction;
39use alloy_primitives::{
40    map::{FbBuildHasher, HashMap},
41    TxHash,
42};
43use derive_more::{Constructor, Deref};
44use futures::{stream::FuturesUnordered, Future, FutureExt, Stream, StreamExt};
45use pin_project::pin_project;
46use reth_eth_wire::{
47    DedupPayload, GetPooledTransactions, HandleMempoolData, HandleVersionedMempoolData,
48    PartiallyValidData, RequestTxHashes, ValidAnnouncementData,
49};
50use reth_eth_wire_types::{EthNetworkPrimitives, NetworkPrimitives};
51use reth_network_api::PeerRequest;
52use reth_network_p2p::error::{RequestError, RequestResult};
53use reth_network_peers::PeerId;
54use reth_primitives_traits::SignedTransaction;
55use schnellru::ByLength;
56use std::{
57    pin::Pin,
58    task::{ready, Context, Poll},
59    time::Duration,
60};
61use tokio::sync::{mpsc::error::TrySendError, oneshot, oneshot::error::RecvError};
62use tracing::trace;
63
64/// The type responsible for fetching missing transactions from peers.
65///
66/// This will keep track of unique transaction hashes that are currently being fetched and submits
67/// new requests on announced hashes.
68#[derive(Debug)]
69#[pin_project]
70pub struct TransactionFetcher<N: NetworkPrimitives = EthNetworkPrimitives> {
71    /// All peers with to which a [`GetPooledTransactions`] request is inflight.
72    pub active_peers: LruMap<PeerId, u8, ByLength, FbBuildHasher<64>>,
73    /// All currently active [`GetPooledTransactions`] requests.
74    ///
75    /// The set of hashes encompassed by these requests are a subset of all hashes in the fetcher.
76    /// It's disjoint from the set of hashes which are awaiting an idle fallback peer in order to
77    /// be fetched.
78    #[pin]
79    pub inflight_requests: FuturesUnordered<GetPooledTxRequestFut<N::PooledTransaction>>,
80    /// Hashes that are awaiting an idle fallback peer so they can be fetched.
81    ///
82    /// This is a subset of all hashes in the fetcher, and is disjoint from the set of hashes for
83    /// which a [`GetPooledTransactions`] request is inflight.
84    pub hashes_pending_fetch: LruCache<TxHash, FbBuildHasher<32>>,
85    /// Tracks all hashes in the transaction fetcher.
86    pub hashes_fetch_inflight_and_pending_fetch:
87        LruMap<TxHash, TxFetchMetadata, ByLength, FbBuildHasher<32>>,
88    /// Info on capacity of the transaction fetcher.
89    pub info: TransactionFetcherInfo,
90    #[doc(hidden)]
91    metrics: TransactionFetcherMetrics,
92}
93
94impl<N: NetworkPrimitives> TransactionFetcher<N> {
95    /// Removes the peer from the active set.
96    pub(crate) fn remove_peer(&mut self, peer_id: &PeerId) {
97        self.active_peers.remove(peer_id);
98    }
99
100    /// Updates metrics.
101    #[inline]
102    pub fn update_metrics(&self) {
103        let metrics = &self.metrics;
104
105        metrics.inflight_transaction_requests.set(self.inflight_requests.len() as f64);
106
107        let hashes_pending_fetch = self.hashes_pending_fetch.len() as f64;
108        let total_hashes = self.hashes_fetch_inflight_and_pending_fetch.len() as f64;
109
110        metrics.hashes_pending_fetch.set(hashes_pending_fetch);
111        metrics.hashes_inflight_transaction_requests.set(total_hashes - hashes_pending_fetch);
112    }
113
114    #[inline]
115    fn update_pending_fetch_cache_search_metrics(&self, durations: TxFetcherSearchDurations) {
116        let metrics = &self.metrics;
117
118        let TxFetcherSearchDurations { find_idle_peer, fill_request } = durations;
119        metrics
120            .duration_find_idle_fallback_peer_for_any_pending_hash
121            .set(find_idle_peer.as_secs_f64());
122        metrics.duration_fill_request_from_hashes_pending_fetch.set(fill_request.as_secs_f64());
123    }
124
125    /// Sets up transaction fetcher with config
126    pub fn with_transaction_fetcher_config(config: &TransactionFetcherConfig) -> Self {
127        let TransactionFetcherConfig {
128            max_inflight_requests,
129            max_capacity_cache_txns_pending_fetch,
130            ..
131        } = *config;
132
133        let info = config.clone().into();
134
135        let metrics = TransactionFetcherMetrics::default();
136        metrics.capacity_inflight_requests.increment(max_inflight_requests as u64);
137
138        Self {
139            active_peers: LruMap::with_hasher(max_inflight_requests, Default::default()),
140            hashes_pending_fetch: LruCache::with_hasher(
141                max_capacity_cache_txns_pending_fetch,
142                Default::default(),
143            ),
144            hashes_fetch_inflight_and_pending_fetch: LruMap::with_hasher(
145                max_inflight_requests + max_capacity_cache_txns_pending_fetch,
146                Default::default(),
147            ),
148            info,
149            metrics,
150            ..Default::default()
151        }
152    }
153
154    /// Removes the specified hashes from inflight tracking.
155    #[inline]
156    pub fn remove_hashes_from_transaction_fetcher<'a, I>(&mut self, hashes: I)
157    where
158        I: IntoIterator<Item = &'a TxHash>,
159    {
160        for hash in hashes {
161            self.hashes_fetch_inflight_and_pending_fetch.remove(hash);
162            self.hashes_pending_fetch.remove(hash);
163        }
164    }
165
166    /// Updates peer's activity status upon a resolved [`GetPooledTxRequest`].
167    fn decrement_inflight_request_count_for(&mut self, peer_id: &PeerId) {
168        let remove = || -> bool {
169            if let Some(inflight_count) = self.active_peers.get(peer_id) {
170                *inflight_count = inflight_count.saturating_sub(1);
171                if *inflight_count == 0 {
172                    return true
173                }
174            }
175            false
176        }();
177
178        if remove {
179            self.active_peers.remove(peer_id);
180        }
181    }
182
183    /// Returns `true` if peer is idle with respect to `self.inflight_requests`.
184    #[inline]
185    pub fn is_idle(&self, peer_id: &PeerId) -> bool {
186        let Some(inflight_count) = self.active_peers.peek(peer_id) else { return true };
187        if *inflight_count < self.info.max_inflight_requests_per_peer {
188            return true
189        }
190        false
191    }
192
193    /// Returns any idle peer for the given hash.
194    pub fn get_idle_peer_for(&self, hash: TxHash) -> Option<&PeerId> {
195        let TxFetchMetadata { fallback_peers, .. } =
196            self.hashes_fetch_inflight_and_pending_fetch.peek(&hash)?;
197
198        fallback_peers.iter().find(|peer_id| self.is_idle(peer_id))
199    }
200
201    /// Returns any idle peer for any hash pending fetch. If one is found, the corresponding
202    /// hash is written to the request buffer that is passed as parameter.
203    ///
204    /// Loops through the hashes pending fetch in lru order until one is found with an idle
205    /// fallback peer, or the budget passed as parameter is depleted, whatever happens first.
206    pub fn find_any_idle_fallback_peer_for_any_pending_hash(
207        &mut self,
208        hashes_to_request: &mut RequestTxHashes,
209        mut budget: Option<usize>, // search fallback peers for max `budget` lru pending hashes
210    ) -> Option<PeerId> {
211        let mut hashes_pending_fetch_iter = self.hashes_pending_fetch.iter();
212
213        let idle_peer = loop {
214            let &hash = hashes_pending_fetch_iter.next()?;
215
216            let idle_peer = self.get_idle_peer_for(hash);
217
218            if idle_peer.is_some() {
219                hashes_to_request.insert(hash);
220                break idle_peer.copied()
221            }
222
223            if let Some(ref mut bud) = budget {
224                *bud = bud.saturating_sub(1);
225                if *bud == 0 {
226                    return None
227                }
228            }
229        };
230        let hash = hashes_to_request.iter().next()?;
231
232        // pop hash that is loaded in request buffer from cache of hashes pending fetch
233        drop(hashes_pending_fetch_iter);
234        _ = self.hashes_pending_fetch.remove(hash);
235
236        idle_peer
237    }
238
239    /// Packages hashes for a [`GetPooledTxRequest`] up to limit. Returns left over hashes. Takes
240    /// a [`RequestTxHashes`] buffer as parameter for filling with hashes to request.
241    ///
242    /// Returns left over hashes.
243    pub fn pack_request(
244        &self,
245        hashes_to_request: &mut RequestTxHashes,
246        hashes_from_announcement: ValidAnnouncementData,
247    ) -> RequestTxHashes {
248        if hashes_from_announcement.msg_version().has_eth68_metadata() {
249            return self.pack_request_eth68(hashes_to_request, hashes_from_announcement)
250        }
251        self.pack_request_eth66(hashes_to_request, hashes_from_announcement)
252    }
253
254    /// Packages hashes for a [`GetPooledTxRequest`] from an
255    /// [`Eth68`](reth_eth_wire::EthVersion::Eth68) announcement up to limit as defined by protocol
256    /// version 68. Takes a [`RequestTxHashes`] buffer as parameter for filling with hashes to
257    /// request.
258    ///
259    /// Returns left over hashes.
260    ///
261    /// Loops through hashes passed as parameter and checks if a hash fits in the expected
262    /// response. If no, it's added to surplus hashes. If yes, it's added to hashes to the request
263    /// and expected response size is accumulated.
264    pub fn pack_request_eth68(
265        &self,
266        hashes_to_request: &mut RequestTxHashes,
267        hashes_from_announcement: impl HandleMempoolData
268            + IntoIterator<Item = (TxHash, Option<(u8, usize)>)>,
269    ) -> RequestTxHashes {
270        let mut acc_size_response = 0;
271
272        let mut hashes_from_announcement_iter = hashes_from_announcement.into_iter();
273
274        if let Some((hash, Some((_ty, size)))) = hashes_from_announcement_iter.next() {
275            hashes_to_request.insert(hash);
276
277            // tx is really big, pack request with single tx
278            if size >= self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request {
279                return hashes_from_announcement_iter.collect()
280            }
281            acc_size_response = size;
282        }
283
284        let mut surplus_hashes = RequestTxHashes::default();
285
286        // folds size based on expected response size  and adds selected hashes to the request
287        // list and the other hashes to the surplus list
288        for (hash, metadata) in hashes_from_announcement_iter.by_ref() {
289            let Some((_ty, size)) = metadata else {
290                unreachable!("this method is called upon reception of an eth68 announcement")
291            };
292
293            let next_acc_size = acc_size_response.checked_add(size).filter(|next_acc_size| {
294                *next_acc_size <=
295                    self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request
296            });
297
298            if let Some(next_acc_size) = next_acc_size {
299                // only update accumulated size of tx response if tx will fit in without exceeding
300                // soft limit
301                acc_size_response = next_acc_size;
302                _ = hashes_to_request.insert(hash)
303            } else {
304                _ = surplus_hashes.insert(hash)
305            }
306
307            let free_space =
308                self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request -
309                    acc_size_response;
310
311            if free_space < MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED {
312                break
313            }
314        }
315
316        surplus_hashes.extend(hashes_from_announcement_iter.map(|(hash, _metadata)| hash));
317
318        surplus_hashes
319    }
320
321    /// Packages hashes for a [`GetPooledTxRequest`] from an
322    /// [`Eth66`](reth_eth_wire::EthVersion::Eth66) announcement up to limit as defined by
323    /// protocol version 66. Takes a [`RequestTxHashes`] buffer as parameter for filling with
324    /// hashes to request.
325    ///
326    /// Returns left over hashes.
327    pub fn pack_request_eth66(
328        &self,
329        hashes_to_request: &mut RequestTxHashes,
330        hashes_from_announcement: ValidAnnouncementData,
331    ) -> RequestTxHashes {
332        let (mut hashes, _version) = hashes_from_announcement.into_request_hashes();
333        if hashes.len() <= SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST {
334            *hashes_to_request = hashes;
335            hashes_to_request.shrink_to_fit();
336
337            RequestTxHashes::default()
338        } else {
339            let surplus_hashes =
340                hashes.retain_count(SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST);
341            *hashes_to_request = hashes;
342            hashes_to_request.shrink_to_fit();
343
344            surplus_hashes
345        }
346    }
347
348    /// Tries to buffer hashes for retry.
349    pub fn try_buffer_hashes_for_retry(
350        &mut self,
351        mut hashes: RequestTxHashes,
352        peer_failed_to_serve: &PeerId,
353    ) {
354        // It could be that the txns have been received over broadcast in the time being. Remove
355        // the peer as fallback peer so it isn't request again for these hashes.
356        hashes.retain(|hash| {
357            if let Some(entry) = self.hashes_fetch_inflight_and_pending_fetch.get(hash) {
358                entry.fallback_peers_mut().remove(peer_failed_to_serve);
359                return true
360            }
361            // tx has been seen over broadcast in the time it took for the request to resolve
362            false
363        });
364
365        self.buffer_hashes(hashes, None)
366    }
367
368    /// Number of hashes pending fetch.
369    pub fn num_pending_hashes(&self) -> usize {
370        self.hashes_pending_fetch.len()
371    }
372
373    /// Number of all transaction hashes in the fetcher.
374    pub fn num_all_hashes(&self) -> usize {
375        self.hashes_fetch_inflight_and_pending_fetch.len()
376    }
377
378    /// Buffers hashes. Note: Only peers that haven't yet tried to request the hashes should be
379    /// passed as `fallback_peer` parameter! For re-buffering hashes on failed request, use
380    /// [`TransactionFetcher::try_buffer_hashes_for_retry`]. Hashes that have been re-requested
381    /// [`DEFAULT_MAX_RETRIES`], are dropped.
382    pub fn buffer_hashes(&mut self, hashes: RequestTxHashes, fallback_peer: Option<PeerId>) {
383        for hash in hashes {
384            // hash could have been evicted from bounded lru map
385            let Some(TxFetchMetadata { retries, fallback_peers, .. }) =
386                self.hashes_fetch_inflight_and_pending_fetch.get(&hash)
387            else {
388                continue
389            };
390
391            if let Some(peer_id) = fallback_peer {
392                // peer has not yet requested hash
393                fallback_peers.insert(peer_id);
394            } else {
395                if *retries >= DEFAULT_MAX_RETRIES {
396                    trace!(target: "net::tx",
397                        %hash,
398                        retries,
399                        "retry limit for `GetPooledTransactions` requests reached for hash, dropping hash"
400                    );
401
402                    self.hashes_fetch_inflight_and_pending_fetch.remove(&hash);
403                    self.hashes_pending_fetch.remove(&hash);
404                    continue
405                }
406                *retries += 1;
407            }
408
409            if let (_, Some(evicted_hash)) = self.hashes_pending_fetch.insert_and_get_evicted(hash)
410            {
411                self.hashes_fetch_inflight_and_pending_fetch.remove(&evicted_hash);
412            }
413        }
414    }
415
416    /// Tries to request hashes pending fetch.
417    ///
418    /// Finds the first buffered hash with a fallback peer that is idle, if any. Fills the rest of
419    /// the request by checking the transactions seen by the peer against the buffer.
420    pub fn on_fetch_pending_hashes(
421        &mut self,
422        peers: &HashMap<PeerId, PeerMetadata<N>, FbBuildHasher<64>>,
423        has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
424    ) -> bool {
425        let mut hashes_to_request = RequestTxHashes::with_capacity(
426            DEFAULT_MARGINAL_COUNT_HASHES_GET_POOLED_TRANSACTIONS_REQUEST,
427        );
428        let mut search_durations = TxFetcherSearchDurations::default();
429
430        // budget to look for an idle peer before giving up
431        let budget_find_idle_fallback_peer = self
432            .search_breadth_budget_find_idle_fallback_peer(&has_capacity_wrt_pending_pool_imports);
433
434        let peer_id = duration_metered_exec!(
435            {
436                let Some(peer_id) = self.find_any_idle_fallback_peer_for_any_pending_hash(
437                    &mut hashes_to_request,
438                    budget_find_idle_fallback_peer,
439                ) else {
440                    // no peers are idle or budget is depleted
441                    return false
442                };
443
444                peer_id
445            },
446            search_durations.find_idle_peer
447        );
448
449        // peer may have disconnected between idle check and here, re-buffer hashes so they
450        // aren't lost from the pending fetch cache
451        let Some(peer) = peers.get(&peer_id) else {
452            self.buffer_hashes(hashes_to_request, None);
453            return false
454        };
455        let conn_eth_version = peer.version;
456
457        // fill the request with more hashes pending fetch that have been announced by the peer.
458        // the search for more hashes is done with respect to the given budget, which determines
459        // how many hashes to loop through before giving up. if no more hashes are found wrt to
460        // the budget, the single hash that was taken out of the cache above is sent in a request.
461        let budget_fill_request = self
462            .search_breadth_budget_find_intersection_pending_hashes_and_hashes_seen_by_peer(
463                &has_capacity_wrt_pending_pool_imports,
464            );
465
466        duration_metered_exec!(
467            {
468                self.fill_request_from_hashes_pending_fetch(
469                    &mut hashes_to_request,
470                    &peer.seen_transactions,
471                    budget_fill_request,
472                )
473            },
474            search_durations.fill_request
475        );
476
477        self.update_pending_fetch_cache_search_metrics(search_durations);
478
479        trace!(target: "net::tx",
480            peer_id=format!("{peer_id:#}"),
481            hashes=?*hashes_to_request,
482            %conn_eth_version,
483            "requesting hashes that were stored pending fetch from peer"
484        );
485
486        // request the buffered missing transactions
487        if let Some(failed_to_request_hashes) =
488            self.request_transactions_from_peer(hashes_to_request, peer)
489        {
490            trace!(target: "net::tx",
491                peer_id=format!("{peer_id:#}"),
492                ?failed_to_request_hashes,
493                %conn_eth_version,
494                "failed sending request to peer's session, buffering hashes"
495            );
496
497            self.buffer_hashes(failed_to_request_hashes, Some(peer_id));
498            return false
499        }
500
501        true
502    }
503
504    /// Filters out hashes that have been seen before. For hashes that have already been seen, the
505    /// peer is added as fallback peer.
506    pub fn filter_unseen_and_pending_hashes(
507        &mut self,
508        new_announced_hashes: &mut ValidAnnouncementData,
509        is_tx_bad_import: impl Fn(&TxHash) -> bool,
510        peer_id: &PeerId,
511        client_version: &str,
512    ) {
513        let mut previously_unseen_hashes_count = 0;
514
515        let msg_version = new_announced_hashes.msg_version();
516
517        // filter out inflight hashes, and register the peer as fallback for all inflight hashes
518        new_announced_hashes.retain(|hash, metadata| {
519
520            // occupied entry
521            if let Some(TxFetchMetadata{ tx_encoded_length: previously_seen_size, ..}) = self.hashes_fetch_inflight_and_pending_fetch.peek_mut(hash) {
522                // update size metadata if available
523                if let Some((_ty, size)) = metadata {
524                    if let Some(prev_size) = previously_seen_size {
525                        // check if this peer is announcing a different size than a previous peer
526                        if size != prev_size {
527                            trace!(target: "net::tx",
528                                peer_id=format!("{peer_id:#}"),
529                                %hash,
530                                size,
531                                previously_seen_size,
532                                %client_version,
533                                "peer announced a different size for tx, this is especially worrying if one size is much bigger..."
534                            );
535                        }
536                    }
537                    // believe the most recent peer to announce tx
538                    *previously_seen_size = Some(*size);
539                }
540
541                // hash has been seen but is not inflight
542                if self.hashes_pending_fetch.remove(hash) {
543                    return true
544                }
545
546                return false
547            }
548
549            // vacant entry
550
551            if is_tx_bad_import(hash) {
552                return false
553            }
554
555            previously_unseen_hashes_count += 1;
556
557            if self
558                .hashes_fetch_inflight_and_pending_fetch
559                .get_or_insert(*hash, || TxFetchMetadata {
560                    retries: 0,
561                    fallback_peers: LruCache::with_hasher(
562                        DEFAULT_MAX_COUNT_FALLBACK_PEERS as u32,
563                        Default::default(),
564                    ),
565                    tx_encoded_length: None,
566                })
567                .is_none()
568            {
569
570                trace!(target: "net::tx",
571                    peer_id=format!("{peer_id:#}"),
572                    %hash,
573                    ?msg_version,
574                    %client_version,
575                    "failed to cache new announced hash from peer in schnellru::LruMap, dropping hash"
576                );
577
578                return false
579            }
580            true
581        });
582
583        trace!(target: "net::tx",
584            peer_id=format!("{peer_id:#}"),
585            previously_unseen_hashes_count=previously_unseen_hashes_count,
586            msg_version=?msg_version,
587            client_version=%client_version,
588            "received previously unseen hashes in announcement from peer"
589        );
590    }
591
592    /// Requests the missing transactions from the previously unseen announced hashes of the peer.
593    /// Returns the requested hashes if the request concurrency limit is reached or if the request
594    /// fails to send over the channel to the peer's session task.
595    ///
596    /// This filters all announced hashes that are already in flight, and requests the missing,
597    /// while marking the given peer as an alternative peer for the hashes that are already in
598    /// flight.
599    pub fn request_transactions_from_peer(
600        &mut self,
601        new_announced_hashes: RequestTxHashes,
602        peer: &PeerMetadata<N>,
603    ) -> Option<RequestTxHashes> {
604        let peer_id: PeerId = peer.request_tx.peer_id;
605        let conn_eth_version = peer.version;
606
607        if self.active_peers.len() >= self.info.max_inflight_requests {
608            trace!(target: "net::tx",
609                peer_id=format!("{peer_id:#}"),
610                hashes=?*new_announced_hashes,
611                %conn_eth_version,
612                max_inflight_transaction_requests=self.info.max_inflight_requests,
613                "limit for concurrent `GetPooledTransactions` requests reached, dropping request for hashes to peer"
614            );
615            return Some(new_announced_hashes)
616        }
617
618        let Some(inflight_count) = self.active_peers.get_or_insert(peer_id, || 0) else {
619            trace!(target: "net::tx",
620                peer_id=format!("{peer_id:#}"),
621                hashes=?*new_announced_hashes,
622                conn_eth_version=%conn_eth_version,
623                "failed to cache active peer in schnellru::LruMap, dropping request to peer"
624            );
625            return Some(new_announced_hashes)
626        };
627
628        if *inflight_count >= self.info.max_inflight_requests_per_peer {
629            trace!(target: "net::tx",
630                peer_id=format!("{peer_id:#}"),
631                hashes=?*new_announced_hashes,
632                %conn_eth_version,
633                max_concurrent_tx_reqs_per_peer=self.info.max_inflight_requests_per_peer,
634                "limit for concurrent `GetPooledTransactions` requests per peer reached"
635            );
636            return Some(new_announced_hashes)
637        }
638
639        #[cfg(debug_assertions)]
640        {
641            for hash in &new_announced_hashes {
642                if self.hashes_pending_fetch.contains(hash) {
643                    tracing::debug!(target: "net::tx", "`{}` should have been taken out of buffer before packing in a request, breaks invariant `@hashes_pending_fetch` and `@inflight_requests`, `@hashes_fetch_inflight_and_pending_fetch` for `{}`: {:?}",
644                        format!("{:?}", new_announced_hashes), // Assuming new_announced_hashes can be debug-printed directly
645                        format!("{:?}", new_announced_hashes),
646                        new_announced_hashes.iter().map(|hash| {
647                            let metadata = self.hashes_fetch_inflight_and_pending_fetch.get(hash);
648                            // Assuming you only need `retries` and `tx_encoded_length` for debugging
649                            (*hash, metadata.map(|m| (m.retries, m.tx_encoded_length)))
650                        }).collect::<Vec<(TxHash, Option<(u8, Option<usize>)>)>>())
651                }
652            }
653        }
654
655        let (response, rx) = oneshot::channel();
656        let req = PeerRequest::GetPooledTransactions {
657            request: GetPooledTransactions(new_announced_hashes.iter().copied().collect()),
658            response,
659        };
660
661        // try to send the request to the peer
662        if let Err(err) = peer.request_tx.try_send(req) {
663            // peer channel is full
664            return match err {
665                TrySendError::Full(_) | TrySendError::Closed(_) => {
666                    self.metrics.egress_peer_channel_full.increment(1);
667                    Some(new_announced_hashes)
668                }
669            }
670        }
671
672        *inflight_count += 1;
673        // stores a new request future for the request
674        self.inflight_requests.push(GetPooledTxRequestFut::new(peer_id, new_announced_hashes, rx));
675
676        None
677    }
678
679    /// Tries to fill request with hashes pending fetch so that the expected [`PooledTransactions`]
680    /// response is full enough. A mutable reference to a list of hashes to request is passed as
681    /// parameter. A budget is passed as parameter, this ensures that the node stops searching
682    /// for more hashes after the budget is depleted. Under bad network conditions, the cache of
683    /// hashes pending fetch may become very full for a while. As the node recovers, the hashes
684    /// pending fetch cache should get smaller. The budget should aim to be big enough to loop
685    /// through all buffered hashes in good network conditions.
686    ///
687    /// The request hashes buffer is filled as if it's an eth68 request, i.e. smartly assemble
688    /// the request based on expected response size. For any hash missing size metadata, it is
689    /// guessed at [`AVERAGE_BYTE_SIZE_TX_ENCODED`].
690    ///
691    /// Loops through hashes pending fetch and does:
692    ///
693    /// 1. Check if a hash pending fetch is seen by peer.
694    /// 2. Optimistically include the hash in the request.
695    /// 3. Accumulate expected total response size.
696    /// 4. Check if acc size and hashes count is at limit, if so stop looping.
697    /// 5. Remove hashes to request from cache of hashes pending fetch.
698    pub fn fill_request_from_hashes_pending_fetch(
699        &mut self,
700        hashes_to_request: &mut RequestTxHashes,
701        seen_hashes: &LruCache<TxHash, FbBuildHasher<32>>,
702        mut budget_fill_request: Option<usize>, // check max `budget` lru pending hashes
703    ) {
704        let Some(hash) = hashes_to_request.iter().next() else { return };
705
706        let mut acc_size_response = self
707            .hashes_fetch_inflight_and_pending_fetch
708            .get(hash)
709            .and_then(|entry| entry.tx_encoded_len())
710            .unwrap_or(AVERAGE_BYTE_SIZE_TX_ENCODED);
711
712        // if request full enough already, we're satisfied, send request for single tx
713        if acc_size_response >=
714            DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE_ON_FETCH_PENDING_HASHES
715        {
716            return
717        }
718
719        // try to fill request by checking if any other hashes pending fetch (in lru order) are
720        // also seen by peer
721        for hash in self.hashes_pending_fetch.iter() {
722            // 1. Check if a hash pending fetch is seen by peer.
723            if !seen_hashes.contains(hash) {
724                continue
725            };
726
727            // 2. Optimistically include the hash in the request.
728            hashes_to_request.insert(*hash);
729
730            // 3. Accumulate expected total response size.
731            let size = self
732                .hashes_fetch_inflight_and_pending_fetch
733                .get(hash)
734                .and_then(|entry| entry.tx_encoded_len())
735                .unwrap_or(AVERAGE_BYTE_SIZE_TX_ENCODED);
736
737            acc_size_response = acc_size_response.saturating_add(size);
738
739            // 4. Check if acc size or hashes count is at limit, if so stop looping.
740            // if expected response is full enough or the number of hashes in the request is
741            // enough, we're satisfied
742            if acc_size_response >=
743                DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE_ON_FETCH_PENDING_HASHES ||
744                hashes_to_request.len() >
745                    DEFAULT_SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST_ON_FETCH_PENDING_HASHES
746            {
747                break
748            }
749
750            if let Some(ref mut bud) = budget_fill_request {
751                *bud -= 1;
752                if *bud == 0 {
753                    break
754                }
755            }
756        }
757
758        // 5. Remove hashes to request from cache of hashes pending fetch.
759        for hash in hashes_to_request.iter() {
760            self.hashes_pending_fetch.remove(hash);
761        }
762    }
763
764    /// Returns `true` if [`TransactionFetcher`] has capacity to request pending hashes. Returns
765    /// `false` if [`TransactionFetcher`] is operating close to full capacity.
766    pub fn has_capacity_for_fetching_pending_hashes(&self) -> bool {
767        let info = &self.info;
768
769        self.has_capacity(info.max_inflight_requests)
770    }
771
772    /// Returns `true` if the number of inflight requests are under a given tolerated max.
773    fn has_capacity(&self, max_inflight_requests: usize) -> bool {
774        self.inflight_requests.len() <= max_inflight_requests
775    }
776
777    /// Returns the limit to enforce when looking for any pending hash with an idle fallback peer.
778    ///
779    /// Returns `Some(limit)` if [`TransactionFetcher`] and the
780    /// [`TransactionPool`](reth_transaction_pool::TransactionPool) are operating close to full
781    /// capacity. Returns `None`, unlimited, if they are not that busy.
782    pub fn search_breadth_budget_find_idle_fallback_peer(
783        &self,
784        has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
785    ) -> Option<usize> {
786        let info = &self.info;
787
788        let tx_fetcher_has_capacity = self.has_capacity(
789            info.max_inflight_requests /
790                DEFAULT_DIVISOR_MAX_COUNT_INFLIGHT_REQUESTS_ON_FIND_IDLE_PEER,
791        );
792        let tx_pool_has_capacity = has_capacity_wrt_pending_pool_imports(
793            DEFAULT_DIVISOR_MAX_COUNT_PENDING_POOL_IMPORTS_ON_FIND_IDLE_PEER,
794        );
795
796        if tx_fetcher_has_capacity && tx_pool_has_capacity {
797            // unlimited search breadth
798            None
799        } else {
800            // limited breadth of search for idle peer
801            let limit = DEFAULT_BUDGET_FIND_IDLE_FALLBACK_PEER;
802
803            trace!(target: "net::tx",
804                inflight_requests=self.inflight_requests.len(),
805                max_inflight_transaction_requests=info.max_inflight_requests,
806                hashes_pending_fetch=self.hashes_pending_fetch.len(),
807                limit,
808                "search breadth limited in search for idle fallback peer for some hash pending fetch"
809            );
810
811            Some(limit)
812        }
813    }
814
815    /// Returns the limit to enforce when looking for the intersection between hashes announced by
816    /// peer and hashes pending fetch.
817    ///
818    /// Returns `Some(limit)` if [`TransactionFetcher`] and the
819    /// [`TransactionPool`](reth_transaction_pool::TransactionPool) are operating close to full
820    /// capacity. Returns `None`, unlimited, if they are not that busy.
821    pub fn search_breadth_budget_find_intersection_pending_hashes_and_hashes_seen_by_peer(
822        &self,
823        has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
824    ) -> Option<usize> {
825        let info = &self.info;
826
827        let tx_fetcher_has_capacity = self.has_capacity(
828            info.max_inflight_requests /
829                DEFAULT_DIVISOR_MAX_COUNT_INFLIGHT_REQUESTS_ON_FIND_INTERSECTION,
830        );
831        let tx_pool_has_capacity = has_capacity_wrt_pending_pool_imports(
832            DEFAULT_DIVISOR_MAX_COUNT_PENDING_POOL_IMPORTS_ON_FIND_INTERSECTION,
833        );
834
835        if tx_fetcher_has_capacity && tx_pool_has_capacity {
836            // unlimited search breadth
837            None
838        } else {
839            // limited breadth of search for idle peer
840            let limit = DEFAULT_BUDGET_FIND_INTERSECTION_ANNOUNCED_BY_PEER_AND_PENDING_FETCH;
841
842            trace!(target: "net::tx",
843                inflight_requests=self.inflight_requests.len(),
844                max_inflight_transaction_requests=self.info.max_inflight_requests,
845                hashes_pending_fetch=self.hashes_pending_fetch.len(),
846                limit=limit,
847                "search breadth limited in search for intersection of hashes announced by peer and hashes pending fetch"
848            );
849
850            Some(limit)
851        }
852    }
853
854    /// Processes a resolved [`GetPooledTransactions`] request. Queues the outcome as a
855    /// [`FetchEvent`], which will then be streamed by
856    /// [`TransactionsManager`](super::TransactionsManager).
857    pub fn on_resolved_get_pooled_transactions_request_fut(
858        &mut self,
859        response: GetPooledTxResponse<N::PooledTransaction>,
860    ) -> FetchEvent<N::PooledTransaction> {
861        // update peer activity, requests for buffered hashes can only be made to idle
862        // fallback peers
863        let GetPooledTxResponse { peer_id, mut requested_hashes, result } = response;
864
865        self.decrement_inflight_request_count_for(&peer_id);
866
867        match result {
868            Ok(Ok(transactions)) => {
869                //
870                // 1. peer has failed to serve any of the hashes it has announced to us that we,
871                // as a follow, have requested
872                //
873                if transactions.is_empty() {
874                    trace!(target: "net::tx",
875                        peer_id=format!("{peer_id:#}"),
876                        requested_hashes_len=requested_hashes.len(),
877                        "received empty `PooledTransactions` response from peer, peer failed to serve hashes it announced"
878                    );
879
880                    return FetchEvent::EmptyResponse { peer_id }
881                }
882
883                //
884                // 2. filter out hashes that we didn't request
885                //
886                let payload = UnverifiedPooledTransactions::new(transactions);
887
888                let unverified_len = payload.len();
889                let (verification_outcome, verified_payload) =
890                    payload.verify(&requested_hashes, &peer_id);
891
892                let unsolicited = unverified_len - verified_payload.len();
893                if unsolicited > 0 {
894                    self.metrics.unsolicited_transactions.increment(unsolicited as u64);
895                }
896
897                let report_peer = if verification_outcome == VerificationOutcome::ReportPeer {
898                    trace!(target: "net::tx",
899                        peer_id=format!("{peer_id:#}"),
900                        unverified_len,
901                        verified_payload_len=verified_payload.len(),
902                        "received `PooledTransactions` response from peer with entries that didn't verify against request, filtered out transactions"
903                    );
904                    true
905                } else {
906                    false
907                };
908
909                // peer has only sent hashes that we didn't request
910                if verified_payload.is_empty() {
911                    return FetchEvent::FetchError { peer_id, error: RequestError::BadResponse }
912                }
913
914                //
915                // 3. stateless validation of payload, e.g. dedup
916                //
917                let unvalidated_payload_len = verified_payload.len();
918
919                let valid_payload = verified_payload.dedup();
920
921                // todo: validate based on announced tx size/type and report peer for sending
922                // invalid response <https://github.com/paradigmxyz/reth/issues/6529>. requires
923                // passing the rlp encoded length down from active session along with the decoded
924                // tx.
925
926                if valid_payload.len() != unvalidated_payload_len {
927                    trace!(target: "net::tx",
928                    peer_id=format!("{peer_id:#}"),
929                    unvalidated_payload_len,
930                    valid_payload_len=valid_payload.len(),
931                    "received `PooledTransactions` response from peer with duplicate entries, filtered them out"
932                    );
933                }
934                // valid payload will have at least one transaction at this point. even if the tx
935                // size/type announced by the peer is different to the actual tx size/type, pass on
936                // to pending pool imports pipeline for validation.
937
938                //
939                // 4. clear received hashes
940                //
941                let requested_hashes_len = requested_hashes.len();
942                let mut fetched = Vec::with_capacity(valid_payload.len());
943                requested_hashes.retain(|requested_hash| {
944                    if valid_payload.contains_key(requested_hash) {
945                        // hash is now known, stop tracking
946                        fetched.push(*requested_hash);
947                        return false
948                    }
949                    true
950                });
951                fetched.shrink_to_fit();
952                self.metrics.fetched_transactions.increment(fetched.len() as u64);
953
954                if fetched.len() < requested_hashes_len {
955                    trace!(target: "net::tx",
956                        peer_id=format!("{peer_id:#}"),
957                        requested_hashes_len=requested_hashes_len,
958                        fetched_len=fetched.len(),
959                        "peer failed to serve hashes it announced"
960                    );
961                }
962
963                //
964                // 5. buffer left over hashes
965                //
966                self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
967
968                let transactions = valid_payload.into_data().into_values().collect();
969
970                FetchEvent::TransactionsFetched { peer_id, transactions, report_peer }
971            }
972            Ok(Err(req_err)) => {
973                self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
974                FetchEvent::FetchError { peer_id, error: req_err }
975            }
976            Err(_) => {
977                self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
978                // request channel closed/dropped
979                FetchEvent::FetchError { peer_id, error: RequestError::ChannelClosed }
980            }
981        }
982    }
983}
984
985impl<N: NetworkPrimitives> Stream for TransactionFetcher<N> {
986    type Item = FetchEvent<N::PooledTransaction>;
987
988    /// Advances all inflight requests and returns the next event.
989    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
990        // `FuturesUnordered` doesn't close when `None` is returned. so just return pending.
991        // <https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=815be2b6c8003303757c3ced135f363e>
992        if self.inflight_requests.is_empty() {
993            return Poll::Pending
994        }
995
996        if let Some(resp) = ready!(self.inflight_requests.poll_next_unpin(cx)) {
997            return Poll::Ready(Some(self.on_resolved_get_pooled_transactions_request_fut(resp)))
998        }
999
1000        Poll::Pending
1001    }
1002}
1003
1004impl<T: NetworkPrimitives> Default for TransactionFetcher<T> {
1005    fn default() -> Self {
1006        Self {
1007            active_peers: LruMap::with_hasher(
1008                DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
1009                Default::default(),
1010            ),
1011            inflight_requests: Default::default(),
1012            hashes_pending_fetch: LruCache::with_hasher(
1013                DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
1014                Default::default(),
1015            ),
1016            hashes_fetch_inflight_and_pending_fetch: LruMap::with_hasher(
1017                DEFAULT_MAX_CAPACITY_CACHE_INFLIGHT_AND_PENDING_FETCH,
1018                Default::default(),
1019            ),
1020            info: TransactionFetcherInfo::default(),
1021            metrics: Default::default(),
1022        }
1023    }
1024}
1025
1026/// Metadata of a transaction hash that is yet to be fetched.
1027#[derive(Debug, Constructor)]
1028pub struct TxFetchMetadata {
1029    /// The number of times a request attempt has been made for the hash.
1030    retries: u8,
1031    /// Peers that have announced the hash, but to which a request attempt has not yet been made.
1032    fallback_peers: LruCache<PeerId, FbBuildHasher<64>>,
1033    /// Size metadata of the transaction if it has been seen in an eth68 announcement.
1034    // todo: store all seen sizes as a `(size, peer_id)` tuple to catch peers that respond with
1035    // another size tx than they announced. alt enter in request (won't catch peers announcing
1036    // wrong size for requests assembled from hashes pending fetch if stored in request fut)
1037    tx_encoded_length: Option<usize>,
1038}
1039
1040impl TxFetchMetadata {
1041    /// Returns a mutable reference to the fallback peers cache for this transaction hash.
1042    pub const fn fallback_peers_mut(&mut self) -> &mut LruCache<PeerId, FbBuildHasher<64>> {
1043        &mut self.fallback_peers
1044    }
1045
1046    /// Returns the size of the transaction, if its hash has been received in any
1047    /// [`Eth68`](reth_eth_wire::EthVersion::Eth68) announcement. If the transaction hash has only
1048    /// been seen in [`Eth66`](reth_eth_wire::EthVersion::Eth66) announcements so far, this will
1049    /// return `None`.
1050    pub const fn tx_encoded_len(&self) -> Option<usize> {
1051        self.tx_encoded_length
1052    }
1053}
1054
1055/// Represents possible events from fetching transactions.
1056#[derive(Debug)]
1057pub enum FetchEvent<T = PooledTransaction> {
1058    /// Triggered when transactions are successfully fetched.
1059    TransactionsFetched {
1060        /// The ID of the peer from which transactions were fetched.
1061        peer_id: PeerId,
1062        /// The transactions that were fetched, if available.
1063        transactions: PooledTransactions<T>,
1064        /// Whether the peer should be penalized for sending unsolicited transactions or for
1065        /// misbehavior.
1066        report_peer: bool,
1067    },
1068    /// Triggered when there is an error in fetching transactions.
1069    FetchError {
1070        /// The ID of the peer from which an attempt to fetch transactions resulted in an error.
1071        peer_id: PeerId,
1072        /// The specific error that occurred while fetching.
1073        error: RequestError,
1074    },
1075    /// An empty response was received.
1076    EmptyResponse {
1077        /// The ID of the sender.
1078        peer_id: PeerId,
1079    },
1080}
1081
1082/// An inflight request for [`PooledTransactions`] from a peer.
1083#[derive(Debug)]
1084pub struct GetPooledTxRequest<T = PooledTransaction> {
1085    peer_id: PeerId,
1086    /// Transaction hashes that were requested, for cleanup purposes
1087    requested_hashes: RequestTxHashes,
1088    response: oneshot::Receiver<RequestResult<PooledTransactions<T>>>,
1089}
1090
1091/// Upon reception of a response, a [`GetPooledTxRequest`] is deconstructed to form a
1092/// [`GetPooledTxResponse`].
1093#[derive(Debug)]
1094pub struct GetPooledTxResponse<T = PooledTransaction> {
1095    peer_id: PeerId,
1096    /// Transaction hashes that were requested, for cleanup purposes, since peer may only return a
1097    /// subset of requested hashes.
1098    requested_hashes: RequestTxHashes,
1099    result: Result<RequestResult<PooledTransactions<T>>, RecvError>,
1100}
1101
1102/// Stores the response receiver made by sending a [`GetPooledTransactions`] request to a peer's
1103/// session.
1104#[must_use = "futures do nothing unless polled"]
1105#[pin_project::pin_project]
1106#[derive(Debug)]
1107pub struct GetPooledTxRequestFut<T = PooledTransaction> {
1108    #[pin]
1109    inner: Option<GetPooledTxRequest<T>>,
1110}
1111
1112impl<T> GetPooledTxRequestFut<T> {
1113    #[inline]
1114    const fn new(
1115        peer_id: PeerId,
1116        requested_hashes: RequestTxHashes,
1117        response: oneshot::Receiver<RequestResult<PooledTransactions<T>>>,
1118    ) -> Self {
1119        Self { inner: Some(GetPooledTxRequest { peer_id, requested_hashes, response }) }
1120    }
1121}
1122
1123impl<T> Future for GetPooledTxRequestFut<T> {
1124    type Output = GetPooledTxResponse<T>;
1125
1126    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1127        let mut req = self.as_mut().project().inner.take().expect("polled after completion");
1128        match req.response.poll_unpin(cx) {
1129            Poll::Ready(result) => Poll::Ready(GetPooledTxResponse {
1130                peer_id: req.peer_id,
1131                requested_hashes: req.requested_hashes,
1132                result,
1133            }),
1134            Poll::Pending => {
1135                self.project().inner.set(Some(req));
1136                Poll::Pending
1137            }
1138        }
1139    }
1140}
1141
1142/// Wrapper of unverified [`PooledTransactions`].
1143#[derive(Debug, Constructor, Deref)]
1144pub struct UnverifiedPooledTransactions<T> {
1145    txns: PooledTransactions<T>,
1146}
1147
1148/// [`PooledTransactions`] that have been successfully verified.
1149#[derive(Debug, Constructor, Deref)]
1150pub struct VerifiedPooledTransactions<T> {
1151    txns: PooledTransactions<T>,
1152}
1153
1154impl<T: SignedTransaction> DedupPayload for VerifiedPooledTransactions<T> {
1155    type Value = T;
1156
1157    fn is_empty(&self) -> bool {
1158        self.txns.is_empty()
1159    }
1160
1161    fn len(&self) -> usize {
1162        self.txns.len()
1163    }
1164
1165    fn dedup(self) -> PartiallyValidData<Self::Value> {
1166        PartiallyValidData::from_raw_data(
1167            self.txns.into_iter().map(|tx| (*tx.tx_hash(), tx)).collect(),
1168            None,
1169        )
1170    }
1171}
1172
1173trait VerifyPooledTransactionsResponse {
1174    type Transaction: SignedTransaction;
1175
1176    fn verify(
1177        self,
1178        requested_hashes: &RequestTxHashes,
1179        peer_id: &PeerId,
1180    ) -> (VerificationOutcome, VerifiedPooledTransactions<Self::Transaction>);
1181}
1182
1183impl<T: SignedTransaction> VerifyPooledTransactionsResponse for UnverifiedPooledTransactions<T> {
1184    type Transaction = T;
1185
1186    fn verify(
1187        self,
1188        requested_hashes: &RequestTxHashes,
1189        _peer_id: &PeerId,
1190    ) -> (VerificationOutcome, VerifiedPooledTransactions<T>) {
1191        let mut verification_outcome = VerificationOutcome::Ok;
1192
1193        let Self { mut txns } = self;
1194
1195        #[cfg(debug_assertions)]
1196        let mut tx_hashes_not_requested: smallvec::SmallVec<[TxHash; 16]> = smallvec::smallvec!();
1197        #[cfg(not(debug_assertions))]
1198        let mut tx_hashes_not_requested_count = 0;
1199
1200        txns.0.retain(|tx| {
1201            if !requested_hashes.contains(tx.tx_hash()) {
1202                verification_outcome = VerificationOutcome::ReportPeer;
1203
1204                #[cfg(debug_assertions)]
1205                tx_hashes_not_requested.push(*tx.tx_hash());
1206                #[cfg(not(debug_assertions))]
1207                {
1208                    tx_hashes_not_requested_count += 1;
1209                }
1210
1211                return false
1212            }
1213            true
1214        });
1215
1216        #[cfg(debug_assertions)]
1217        if !tx_hashes_not_requested.is_empty() {
1218            trace!(target: "net::tx",
1219                peer_id=format!("{_peer_id:#}"),
1220                ?tx_hashes_not_requested,
1221                "transactions in `PooledTransactions` response from peer were not requested"
1222            );
1223        }
1224        #[cfg(not(debug_assertions))]
1225        if tx_hashes_not_requested_count != 0 {
1226            trace!(target: "net::tx",
1227                peer_id=format!("{_peer_id:#}"),
1228                tx_hashes_not_requested_count,
1229                "transactions in `PooledTransactions` response from peer were not requested"
1230            );
1231        }
1232
1233        (verification_outcome, VerifiedPooledTransactions::new(txns))
1234    }
1235}
1236
1237/// Outcome from verifying a [`PooledTransactions`] response. Signals to caller whether to penalize
1238/// the sender of the response or not.
1239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1240pub enum VerificationOutcome {
1241    /// Peer behaves appropriately.
1242    Ok,
1243    /// A penalty should be flagged for the peer. Peer sent a response with unacceptably
1244    /// invalid entries.
1245    ReportPeer,
1246}
1247
1248/// Tracks stats about the [`TransactionFetcher`].
1249#[derive(Debug, Constructor)]
1250pub struct TransactionFetcherInfo {
1251    /// Max inflight [`GetPooledTransactions`] requests.
1252    pub max_inflight_requests: usize,
1253    /// Max inflight [`GetPooledTransactions`] requests per peer.
1254    pub max_inflight_requests_per_peer: u8,
1255    /// Soft limit for the byte size of the expected [`PooledTransactions`] response, upon packing
1256    /// a [`GetPooledTransactions`] request with hashes (by default less than 2 MiB worth of
1257    /// transactions is requested).
1258    pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
1259    /// Soft limit for the byte size of a [`PooledTransactions`] response, upon assembling the
1260    /// response. Spec'd at 2 MiB, but can be adjusted for research purpose.
1261    pub soft_limit_byte_size_pooled_transactions_response: usize,
1262    /// Max capacity of the cache of transaction hashes, for transactions that weren't yet fetched.
1263    /// A transaction is pending fetch if its hash didn't fit into a [`GetPooledTransactions`] yet,
1264    /// or it wasn't returned upon request to peers.
1265    pub max_capacity_cache_txns_pending_fetch: u32,
1266}
1267
1268impl Default for TransactionFetcherInfo {
1269    fn default() -> Self {
1270        Self::new(
1271            DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS as usize,
1272            DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
1273            DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
1274            SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
1275            DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
1276        )
1277    }
1278}
1279
1280impl From<TransactionFetcherConfig> for TransactionFetcherInfo {
1281    fn from(config: TransactionFetcherConfig) -> Self {
1282        let TransactionFetcherConfig {
1283            max_inflight_requests,
1284            max_inflight_requests_per_peer,
1285            soft_limit_byte_size_pooled_transactions_response,
1286            soft_limit_byte_size_pooled_transactions_response_on_pack_request,
1287            max_capacity_cache_txns_pending_fetch,
1288        } = config;
1289
1290        Self::new(
1291            max_inflight_requests as usize,
1292            max_inflight_requests_per_peer,
1293            soft_limit_byte_size_pooled_transactions_response_on_pack_request,
1294            soft_limit_byte_size_pooled_transactions_response,
1295            max_capacity_cache_txns_pending_fetch,
1296        )
1297    }
1298}
1299
1300#[derive(Debug, Default)]
1301struct TxFetcherSearchDurations {
1302    find_idle_peer: Duration,
1303    fill_request: Duration,
1304}
1305
1306#[cfg(test)]
1307mod test {
1308    use super::*;
1309    use crate::test_utils::transactions::{buffer_hash_to_tx_fetcher, new_mock_session};
1310    use alloy_primitives::{
1311        hex,
1312        map::{B256Map, B256Set, HashMap},
1313        B256,
1314    };
1315    use alloy_rlp::Decodable;
1316    use derive_more::IntoIterator;
1317    use reth_eth_wire_types::EthVersion;
1318    use reth_ethereum_primitives::TransactionSigned;
1319    use std::str::FromStr;
1320
1321    #[derive(IntoIterator)]
1322    struct TestValidAnnouncementData(Vec<(TxHash, Option<(u8, usize)>)>);
1323
1324    impl HandleMempoolData for TestValidAnnouncementData {
1325        fn is_empty(&self) -> bool {
1326            self.0.is_empty()
1327        }
1328
1329        fn len(&self) -> usize {
1330            self.0.len()
1331        }
1332
1333        fn retain_by_hash(&mut self, mut f: impl FnMut(&TxHash) -> bool) {
1334            self.0.retain(|(hash, _)| f(hash))
1335        }
1336    }
1337
1338    impl HandleVersionedMempoolData for TestValidAnnouncementData {
1339        fn msg_version(&self) -> EthVersion {
1340            EthVersion::Eth68
1341        }
1342    }
1343
1344    #[test]
1345    fn pack_eth68_request() {
1346        reth_tracing::init_test_tracing();
1347
1348        // RIG TEST
1349
1350        let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1351
1352        let eth68_hashes = [
1353            B256::from_slice(&[1; 32]),
1354            B256::from_slice(&[2; 32]),
1355            B256::from_slice(&[3; 32]),
1356            B256::from_slice(&[4; 32]),
1357            B256::from_slice(&[5; 32]),
1358        ];
1359        let eth68_sizes = [
1360            DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ - MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED - 1, // first will fit
1361            DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ, // second won't
1362            2, // free space > `MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED`, third will fit, no more after this
1363            9,
1364            0,
1365        ];
1366
1367        let expected_request_hashes =
1368            [eth68_hashes[0], eth68_hashes[2]].into_iter().collect::<B256Set>();
1369
1370        let expected_surplus_hashes =
1371            [eth68_hashes[1], eth68_hashes[3], eth68_hashes[4]].into_iter().collect::<B256Set>();
1372
1373        let mut eth68_hashes_to_request = RequestTxHashes::with_capacity(3);
1374
1375        let valid_announcement_data = TestValidAnnouncementData(
1376            eth68_hashes
1377                .into_iter()
1378                .zip(eth68_sizes)
1379                .map(|(hash, size)| (hash, Some((0u8, size))))
1380                .collect::<Vec<_>>(),
1381        );
1382
1383        // TEST
1384
1385        let surplus_eth68_hashes =
1386            tx_fetcher.pack_request_eth68(&mut eth68_hashes_to_request, valid_announcement_data);
1387
1388        let eth68_hashes_to_request = eth68_hashes_to_request.into_iter().collect::<B256Set>();
1389        let surplus_eth68_hashes = surplus_eth68_hashes.into_iter().collect::<B256Set>();
1390
1391        assert_eq!(expected_request_hashes, eth68_hashes_to_request);
1392        assert_eq!(expected_surplus_hashes, surplus_eth68_hashes);
1393    }
1394
1395    #[test]
1396    fn pack_eth68_request_does_not_overflow_announced_size() {
1397        reth_tracing::init_test_tracing();
1398
1399        let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1400
1401        let eth68_hashes =
1402            [B256::from_slice(&[1; 32]), B256::from_slice(&[2; 32]), B256::from_slice(&[3; 32])];
1403        let eth68_sizes = [
1404            DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ - MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED - 1,
1405            usize::MAX,
1406            2,
1407        ];
1408
1409        let expected_request_hashes =
1410            [eth68_hashes[0], eth68_hashes[2]].into_iter().collect::<B256Set>();
1411        let expected_surplus_hashes = std::iter::once(eth68_hashes[1]).collect::<B256Set>();
1412
1413        let mut eth68_hashes_to_request = RequestTxHashes::with_capacity(3);
1414        let valid_announcement_data = TestValidAnnouncementData(
1415            eth68_hashes
1416                .into_iter()
1417                .zip(eth68_sizes)
1418                .map(|(hash, size)| (hash, Some((0u8, size))))
1419                .collect::<Vec<_>>(),
1420        );
1421
1422        let surplus_eth68_hashes =
1423            tx_fetcher.pack_request_eth68(&mut eth68_hashes_to_request, valid_announcement_data);
1424
1425        let eth68_hashes_to_request = eth68_hashes_to_request.into_iter().collect::<B256Set>();
1426        let surplus_eth68_hashes = surplus_eth68_hashes.into_iter().collect::<B256Set>();
1427
1428        assert_eq!(expected_request_hashes, eth68_hashes_to_request);
1429        assert_eq!(expected_surplus_hashes, surplus_eth68_hashes);
1430    }
1431
1432    #[test]
1433    fn pack_eth72_request_uses_metadata_size_limit() {
1434        reth_tracing::init_test_tracing();
1435
1436        let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1437
1438        let hashes =
1439            [B256::from_slice(&[1; 32]), B256::from_slice(&[2; 32]), B256::from_slice(&[3; 32])];
1440        let announced_size =
1441            DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ;
1442        let announcement_data = hashes
1443            .into_iter()
1444            .map(|hash| (hash, Some((0u8, announced_size))))
1445            .collect::<B256Map<_>>();
1446        let valid_announcement_data = ValidAnnouncementData::from_partially_valid_data(
1447            PartiallyValidData::from_raw_data_eth72(announcement_data),
1448        );
1449
1450        let mut hashes_to_request = RequestTxHashes::with_capacity(3);
1451        let surplus_hashes =
1452            tx_fetcher.pack_request(&mut hashes_to_request, valid_announcement_data);
1453
1454        assert_eq!(1, hashes_to_request.len());
1455        assert_eq!(2, surplus_hashes.len());
1456    }
1457
1458    #[tokio::test]
1459    async fn test_on_fetch_pending_hashes() {
1460        reth_tracing::init_test_tracing();
1461
1462        let tx_fetcher = &mut TransactionFetcher::default();
1463
1464        // RIG TEST
1465
1466        // hashes that will be fetched because they are stored as pending fetch
1467        let seen_hashes = [
1468            B256::from_slice(&[1; 32]),
1469            B256::from_slice(&[2; 32]),
1470            B256::from_slice(&[3; 32]),
1471            B256::from_slice(&[4; 32]),
1472        ];
1473        //
1474        // txns 1-3 are small, all will fit in request. no metadata has been made available for
1475        // hash 4, it has only been seen over eth66 conn, so average tx size will be assumed in
1476        // filling request.
1477        let seen_eth68_hashes_sizes = [120, 158, 116];
1478
1479        // peer that will fetch seen hashes because they are pending fetch
1480        let peer_1 = PeerId::new([1; 64]);
1481        // second peer, won't do anything in this test
1482        let peer_2 = PeerId::new([2; 64]);
1483
1484        // add seen hashes to peers seen transactions
1485        //
1486        // get handle for peer_1's session to receive request for pending hashes
1487        let (mut peer_1_data, mut peer_1_mock_session_rx) =
1488            new_mock_session(peer_1, EthVersion::Eth66);
1489        for hash in &seen_hashes {
1490            peer_1_data.seen_transactions.insert(*hash);
1491        }
1492        let (mut peer_2_data, _) = new_mock_session(peer_2, EthVersion::Eth66);
1493        for hash in &seen_hashes {
1494            peer_2_data.seen_transactions.insert(*hash);
1495        }
1496        let mut peers: HashMap<PeerId, _, FbBuildHasher<64>> = HashMap::default();
1497        peers.insert(peer_1, peer_1_data);
1498        peers.insert(peer_2, peer_2_data);
1499
1500        // insert seen_hashes into tx fetcher
1501        for i in 0..3 {
1502            // insert peer_2 as fallback peer for seen_hashes
1503            buffer_hash_to_tx_fetcher(
1504                tx_fetcher,
1505                seen_hashes[i],
1506                peer_2,
1507                0,
1508                Some(seen_eth68_hashes_sizes[i]),
1509            );
1510        }
1511        buffer_hash_to_tx_fetcher(tx_fetcher, seen_hashes[3], peer_2, 0, None);
1512
1513        // insert pending hash without peer_1 as fallback peer, only with peer_2 as fallback peer
1514        let hash_other = B256::from_slice(&[5; 32]);
1515        buffer_hash_to_tx_fetcher(tx_fetcher, hash_other, peer_2, 0, None);
1516
1517        // add peer_1 as lru fallback peer for seen hashes
1518        for hash in &seen_hashes {
1519            buffer_hash_to_tx_fetcher(tx_fetcher, *hash, peer_1, 0, None);
1520        }
1521
1522        // seen hashes and the random hash from peer_2 are pending fetch
1523        assert_eq!(tx_fetcher.num_pending_hashes(), 5);
1524
1525        // TEST
1526
1527        tx_fetcher.on_fetch_pending_hashes(&peers, |_| true);
1528
1529        // mock session of peer_1 receives request
1530        let req = peer_1_mock_session_rx
1531            .recv()
1532            .await
1533            .expect("peer session should receive request with buffered hashes");
1534        let PeerRequest::GetPooledTransactions { request, .. } = req else { unreachable!() };
1535        let GetPooledTransactions(requested_hashes) = request;
1536
1537        assert_eq!(
1538            requested_hashes.into_iter().collect::<B256Set>(),
1539            seen_hashes.into_iter().collect::<B256Set>()
1540        )
1541    }
1542
1543    #[test]
1544    fn on_fetch_pending_hashes_rebuffers_on_disconnected_peer() {
1545        let tx_fetcher = &mut TransactionFetcher::default();
1546        let peer_1 = PeerId::new([1; 64]);
1547        let peer_2 = PeerId::new([2; 64]);
1548        let hash_1 = B256::from_slice(&[1; 32]);
1549
1550        buffer_hash_to_tx_fetcher(tx_fetcher, hash_1, peer_1, 0, Some(128));
1551        buffer_hash_to_tx_fetcher(tx_fetcher, hash_1, peer_2, 0, Some(128));
1552
1553        assert_eq!(tx_fetcher.num_pending_hashes(), 1);
1554
1555        // pass empty peers map — both peers are "disconnected"
1556        let peers: HashMap<PeerId, PeerMetadata, FbBuildHasher<64>> = HashMap::default();
1557        tx_fetcher.on_fetch_pending_hashes(&peers, |_| true);
1558
1559        // hash should be re-buffered, not lost
1560        assert_eq!(tx_fetcher.num_pending_hashes(), 1);
1561    }
1562
1563    #[test]
1564    fn verify_response_hashes() {
1565        let input = hex!(
1566            "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598daa"
1567        );
1568        let signed_tx_1: PooledTransaction =
1569            TransactionSigned::decode(&mut &input[..]).unwrap().try_into().unwrap();
1570        let input = hex!(
1571            "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76"
1572        );
1573        let signed_tx_2: PooledTransaction =
1574            TransactionSigned::decode(&mut &input[..]).unwrap().try_into().unwrap();
1575
1576        // only tx 1 is requested
1577        let request_hashes = [
1578            B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e67890")
1579                .unwrap(),
1580            *signed_tx_1.hash(),
1581            B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e12345")
1582                .unwrap(),
1583            B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4edabe3")
1584                .unwrap(),
1585        ];
1586
1587        for hash in &request_hashes {
1588            assert_ne!(hash, signed_tx_2.hash())
1589        }
1590
1591        let request_hashes = RequestTxHashes::new(request_hashes.into_iter().collect());
1592
1593        // but response contains tx 1 + another tx
1594        let response_txns = PooledTransactions(vec![signed_tx_1.clone(), signed_tx_2]);
1595        let payload = UnverifiedPooledTransactions::new(response_txns);
1596
1597        let (outcome, verified_payload) = payload.verify(&request_hashes, &PeerId::ZERO);
1598
1599        assert_eq!(VerificationOutcome::ReportPeer, outcome);
1600        assert_eq!(1, verified_payload.len());
1601        assert!(verified_payload.contains(&signed_tx_1));
1602    }
1603}