Skip to main content

reth_network/transactions/
mod.rs

1//! Transactions management for the p2p network.
2
3use alloy_consensus::{constants::EIP4844_TX_TYPE_ID, transaction::TxHashRef};
4use rayon::iter::{IntoParallelIterator, ParallelIterator};
5use smallvec::SmallVec;
6
7/// Aggregation on configurable parameters for [`TransactionsManager`].
8pub mod config;
9/// Default and spec'd bounds.
10pub mod constants;
11/// Component responsible for fetching transactions from [`NewPooledTransactionHashes`].
12pub mod fetcher;
13/// Defines the traits for transaction-related policies.
14pub mod policy;
15
16pub use self::constants::{
17    tx_fetcher::DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
18    SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
19};
20use config::AnnouncementAcceptance;
21pub use config::{
22    AnnouncementFilteringPolicy, TransactionFetcherConfig, TransactionIngressPolicy,
23    TransactionPropagationMode, TransactionPropagationPolicy, TransactionsManagerConfig,
24};
25use policy::NetworkPolicies;
26
27pub(crate) use fetcher::{FetchEvent, TransactionFetcher};
28
29use self::constants::{tx_manager::*, DEFAULT_SOFT_LIMIT_BYTE_SIZE_TRANSACTIONS_BROADCAST_MESSAGE};
30use crate::{
31    budget::{
32        DEFAULT_BUDGET_TRY_DRAIN_NETWORK_TRANSACTION_EVENTS,
33        DEFAULT_BUDGET_TRY_DRAIN_PENDING_POOL_IMPORTS, DEFAULT_BUDGET_TRY_DRAIN_STREAM,
34    },
35    cache::LruCache,
36    duration_metered_exec, metered_poll_nested_stream_with_budget,
37    metrics::{AnnouncedTxTypesMetrics, TransactionsManagerMetrics},
38    transactions::config::{StrictEthAnnouncementFilter, TransactionPropagationKind},
39    NetworkHandle, TxTypesCounter,
40};
41use alloy_eips::eip2718::Typed2718;
42use alloy_primitives::{
43    bytes::BufMut,
44    map::{hash_map::Entry, B256Map, B256Set, FbBuildHasher, HashMap, HashSet},
45    TxHash, B256,
46};
47use alloy_rlp::Encodable;
48use constants::SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE;
49use futures::{stream::FuturesUnordered, Future, StreamExt};
50use reth_eth_wire::{
51    BroadcastPoolTransactions, Cells, DedupPayload, EthNetworkPrimitives, EthVersion, GetCells,
52    GetPooledTransactions, HandleMempoolData, HandleVersionedMempoolData, LazyEncoded,
53    LazyEncodedTransaction, NetworkPrimitives, NewPooledTransactionHashes,
54    NewPooledTransactionHashes66, NewPooledTransactionHashes68, NewPooledTransactionHashes72,
55    PooledTransactions, RequestTxHashes, Transactions, ValidAnnouncementData,
56};
57use reth_ethereum_primitives::TxType;
58use reth_evm::SenderRecoveryCache;
59use reth_metrics::common::mpsc::MemoryBoundedReceiver;
60use reth_network_api::{
61    events::{PeerEvent, SessionInfo},
62    NetworkEvent, NetworkEventListenerProvider, PeerKind, PeerRequest, PeerRequestSender, Peers,
63};
64use reth_network_p2p::{
65    error::{RequestError, RequestResult},
66    sync::SyncStateProvider,
67};
68use reth_network_peers::PeerId;
69use reth_network_types::ReputationChangeKind;
70use reth_primitives_traits::{InMemorySize, SignedTransaction};
71use reth_tokio_util::EventStream;
72use reth_transaction_pool::{
73    error::{PoolError, PoolResult},
74    AddedTransactionOutcome, GetPooledTransactionLimit, PoolTransaction, PropagateKind,
75    PropagatedTransactions, TransactionPool, ValidPoolTransaction,
76};
77use std::{
78    pin::Pin,
79    sync::{
80        atomic::{AtomicUsize, Ordering},
81        Arc,
82    },
83    task::{Context, Poll},
84    time::{Duration, Instant},
85};
86use tokio::sync::{mpsc, oneshot, oneshot::error::RecvError};
87use tokio_stream::wrappers::UnboundedReceiverStream;
88use tracing::{debug, trace};
89
90/// The future for importing transactions into the pool.
91///
92/// Resolves with the result of each transaction import.
93pub type PoolImportFuture =
94    Pin<Box<dyn Future<Output = Vec<PoolResult<AddedTransactionOutcome>>> + Send + 'static>>;
95
96/// Api to interact with [`TransactionsManager`] task.
97///
98/// This can be obtained via [`TransactionsManager::handle`] and can be used to manually interact
99/// with the [`TransactionsManager`] task once it is spawned.
100///
101/// For example [`TransactionsHandle::get_peer_transaction_hashes`] returns the transaction hashes
102/// known by a specific peer.
103#[derive(Debug, Clone)]
104pub struct TransactionsHandle<N: NetworkPrimitives = EthNetworkPrimitives> {
105    /// Command channel to the [`TransactionsManager`]
106    manager_tx: mpsc::UnboundedSender<TransactionsCommand<N>>,
107}
108
109impl<N: NetworkPrimitives> TransactionsHandle<N> {
110    fn send(&self, cmd: TransactionsCommand<N>) {
111        let _ = self.manager_tx.send(cmd);
112    }
113
114    /// Fetch the [`PeerRequestSender`] for the given peer.
115    async fn peer_handle(
116        &self,
117        peer_id: PeerId,
118    ) -> Result<Option<PeerRequestSender<PeerRequest<N>>>, RecvError> {
119        let (tx, rx) = oneshot::channel();
120        self.send(TransactionsCommand::GetPeerSender { peer_id, peer_request_sender: tx });
121        rx.await
122    }
123
124    /// Manually propagate the transaction that belongs to the hash.
125    pub fn propagate(&self, hash: TxHash) {
126        self.send(TransactionsCommand::PropagateHash(hash))
127    }
128
129    /// Manually propagate the transaction hash to a specific peer.
130    ///
131    /// Note: this only propagates if the pool contains the transaction.
132    pub fn propagate_hash_to(&self, hash: TxHash, peer: PeerId) {
133        self.propagate_hashes_to(Some(hash), peer)
134    }
135
136    /// Manually propagate the transaction hashes to a specific peer.
137    ///
138    /// Note: this only propagates the transactions that are known to the pool.
139    pub fn propagate_hashes_to(&self, hash: impl IntoIterator<Item = TxHash>, peer: PeerId) {
140        let hashes = hash.into_iter().collect::<Vec<_>>();
141        if hashes.is_empty() {
142            return
143        }
144        self.send(TransactionsCommand::PropagateHashesTo(hashes, peer))
145    }
146
147    /// Request the active peer IDs from the [`TransactionsManager`].
148    pub async fn get_active_peers(&self) -> Result<HashSet<PeerId>, RecvError> {
149        let (tx, rx) = oneshot::channel();
150        self.send(TransactionsCommand::GetActivePeers(tx));
151        rx.await
152    }
153
154    /// Manually propagate full transaction hashes to a specific peer.
155    ///
156    /// Do nothing if transactions are empty.
157    pub fn propagate_transactions_to(&self, transactions: Vec<TxHash>, peer: PeerId) {
158        if transactions.is_empty() {
159            return
160        }
161        self.send(TransactionsCommand::PropagateTransactionsTo(transactions, peer))
162    }
163
164    /// Manually propagate the given transaction hashes to all peers.
165    ///
166    /// It's up to the [`TransactionsManager`] whether the transactions are sent as hashes or in
167    /// full.
168    pub fn propagate_transactions(&self, transactions: Vec<TxHash>) {
169        if transactions.is_empty() {
170            return
171        }
172        self.send(TransactionsCommand::PropagateTransactions(transactions))
173    }
174
175    /// Manually propagate the given transactions to all peers.
176    ///
177    /// It's up to the [`TransactionsManager`] whether the transactions are sent as hashes or in
178    /// full.
179    pub fn broadcast_transactions(
180        &self,
181        transactions: impl IntoIterator<Item = N::BroadcastedTransaction>,
182    ) {
183        let transactions =
184            transactions.into_iter().map(PropagateTransaction::new).collect::<Vec<_>>();
185        if transactions.is_empty() {
186            return
187        }
188        self.send(TransactionsCommand::BroadcastTransactions(transactions))
189    }
190
191    /// Request the transaction hashes known by specific peers.
192    pub async fn get_transaction_hashes(
193        &self,
194        peers: Vec<PeerId>,
195    ) -> Result<HashMap<PeerId, B256Set>, RecvError> {
196        if peers.is_empty() {
197            return Ok(Default::default())
198        }
199        let (tx, rx) = oneshot::channel();
200        self.send(TransactionsCommand::GetTransactionHashes { peers, tx });
201        rx.await
202    }
203
204    /// Request the transaction hashes known by a specific peer.
205    pub async fn get_peer_transaction_hashes(&self, peer: PeerId) -> Result<B256Set, RecvError> {
206        let res = self.get_transaction_hashes(vec![peer]).await?;
207        Ok(res.into_values().next().unwrap_or_default())
208    }
209
210    /// Requests the transactions directly from the given peer.
211    ///
212    /// Returns `None` if the peer is not connected.
213    ///
214    /// **Note**: this returns the response from the peer as received.
215    pub async fn get_pooled_transactions_from(
216        &self,
217        peer_id: PeerId,
218        hashes: Vec<B256>,
219    ) -> Result<Option<Vec<N::PooledTransaction>>, RequestError> {
220        let Some(peer) = self.peer_handle(peer_id).await? else { return Ok(None) };
221
222        let (tx, rx) = oneshot::channel();
223        let request = PeerRequest::GetPooledTransactions { request: hashes.into(), response: tx };
224        peer.try_send(request).ok();
225
226        rx.await?.map(|res| Some(res.0))
227    }
228
229    /// Requests cells from a specific eth/72 peer.
230    ///
231    /// This is the transport-level client primitive for EIP-8070. The caller supplies the
232    /// transaction hashes and the common cell mask for the request. Announcement-driven batching,
233    /// retries, cell verification, and blob reconstruction are layered above this method.
234    ///
235    /// Returns `None` if the peer is no longer connected.
236    pub async fn get_cells_from(
237        &self,
238        peer_id: PeerId,
239        hashes: Vec<B256>,
240        cell_mask: alloy_primitives::B128,
241    ) -> Result<Option<Cells>, RequestError> {
242        if hashes.is_empty() {
243            return Ok(Some(Cells { cell_mask, ..Default::default() }))
244        }
245
246        let Some(peer) = self.peer_handle(peer_id).await? else { return Ok(None) };
247
248        let (tx, rx) = oneshot::channel();
249        let request =
250            PeerRequest::GetCells { request: GetCells { hashes, cell_mask }, response: tx };
251        peer.try_send(request).map_err(|_| RequestError::ChannelClosed)?;
252
253        rx.await?.map(Some)
254    }
255}
256
257/// Manages transactions on top of the p2p network.
258///
259/// This can be spawned to another task and is supposed to be run as background service.
260/// [`TransactionsHandle`] can be used as frontend to programmatically send commands to it and
261/// interact with it.
262///
263/// The [`TransactionsManager`] is responsible for:
264///    - handling incoming eth messages for transactions.
265///    - serving transaction requests.
266///    - propagate transactions
267///
268/// This type communicates with the [`NetworkManager`](crate::NetworkManager) in both directions.
269///   - receives incoming network messages.
270///   - sends messages to dispatch (responses, propagate tx)
271///
272/// It is directly connected to the [`TransactionPool`] to retrieve requested transactions and
273/// propagate new transactions over the network.
274///
275/// It can be configured with different policies for transaction propagation and announcement
276/// filtering. See [`NetworkPolicies`] for more details.
277///
278/// ## Network Transaction Processing
279///
280/// ### Message Types
281///
282/// - **`Transactions`**: Full transaction broadcasts (rejects blob transactions)
283/// - **`NewPooledTransactionHashes`**: Hash announcements
284///
285/// ### Peer Tracking
286///
287/// - Maintains per-peer transaction cache (default: 10,240 entries)
288/// - Prevents duplicate imports and enables efficient propagation
289///
290/// ### Bad Transaction Handling
291///
292/// Caches and rejects transactions with consensus violations (gas, signature, chain ID).
293/// Penalizes peers sending invalid transactions.
294///
295/// ### Import Management
296///
297/// Limits concurrent pool imports and backs off when approaching capacity.
298///
299/// ### Transaction Fetching
300///
301/// For announced transactions: filters known → queues unknown → fetches → imports
302///
303/// ### Propagation Rules
304///
305/// Based on: origin (Local/External/Private), peer capabilities, and network state.
306/// Disabled during initial sync.
307///
308/// ### Security
309///
310/// Rate limiting via reputation, bad transaction isolation, peer scoring.
311#[derive(Debug)]
312#[must_use = "Manager does nothing unless polled."]
313pub struct TransactionsManager<Pool, N: NetworkPrimitives = EthNetworkPrimitives> {
314    /// Access to the transaction pool.
315    pool: Pool,
316    /// Cache of recovered transaction senders shared with payload execution, if enabled.
317    sender_recovery_cache: Option<SenderRecoveryCache>,
318    /// Network access.
319    network: NetworkHandle<N>,
320    /// Subscriptions to all network related events.
321    ///
322    /// From which we get all new incoming transaction related messages.
323    network_events: EventStream<NetworkEvent<PeerRequest<N>>>,
324    /// Transaction fetcher to handle inflight and missing transaction requests.
325    transaction_fetcher: TransactionFetcher<N>,
326    /// All currently pending transactions grouped by peers.
327    ///
328    /// This way we can track incoming transactions and prevent multiple pool imports for the same
329    /// transaction
330    transactions_by_peers: B256Map<SmallVec<[PeerId; 1]>>,
331    /// Transactions that are currently imported into the `Pool`.
332    ///
333    /// The import process includes:
334    ///  - validation of the transactions, e.g. transaction is well formed: valid tx type, fees are
335    ///    valid, or for 4844 transaction the blobs are valid. See also
336    ///    [`EthTransactionValidator`](reth_transaction_pool::validate::EthTransactionValidator)
337    /// - if the transaction is valid, it is added into the pool.
338    ///
339    /// Once the new transaction reaches the __pending__ state it will be emitted by the pool via
340    /// [`TransactionPool::pending_transactions_listener`] and arrive at the `pending_transactions`
341    /// receiver.
342    pool_imports: FuturesUnordered<PoolImportFuture>,
343    /// Stats on pending pool imports that help the node self-monitor.
344    pending_pool_imports_info: PendingPoolImportsInfo,
345    /// Bad imports.
346    bad_imports: LruCache<TxHash, FbBuildHasher<32>>,
347    /// All the connected peers.
348    peers: HashMap<PeerId, PeerMetadata<N>, FbBuildHasher<64>>,
349    /// Send half for the command channel.
350    ///
351    /// This is kept so that a new [`TransactionsHandle`] can be created at any time.
352    command_tx: mpsc::UnboundedSender<TransactionsCommand<N>>,
353    /// Incoming commands from [`TransactionsHandle`].
354    ///
355    /// This will only receive commands if a user manually sends a command to the manager through
356    /// the [`TransactionsHandle`] to interact with this type directly.
357    command_rx: UnboundedReceiverStream<TransactionsCommand<N>>,
358    /// A stream that yields new __pending__ transactions.
359    ///
360    /// A transaction is considered __pending__ if it is executable on the current state of the
361    /// chain. In other words, this only yields transactions that satisfy all consensus
362    /// requirements, these include:
363    ///   - no nonce gaps
364    ///   - all dynamic fee requirements are (currently) met
365    ///   - account has enough balance to cover the transaction's gas
366    pending_transactions: mpsc::Receiver<TxHash>,
367    /// Incoming events from the [`NetworkManager`](crate::NetworkManager).
368    transaction_events: MemoryBoundedReceiver<NetworkTransactionEvent<N>>,
369    /// How the `TransactionsManager` is configured.
370    config: TransactionsManagerConfig,
371    /// Network Policies
372    policies: NetworkPolicies<N>,
373    /// `TransactionsManager` metrics
374    metrics: TransactionsManagerMetrics,
375    /// `AnnouncedTxTypes` metrics
376    announced_tx_types_metrics: AnnouncedTxTypesMetrics,
377}
378
379impl<Pool: TransactionPool, N: NetworkPrimitives> TransactionsManager<Pool, N> {
380    /// Sets up a new instance.
381    ///
382    /// Note: This expects an existing [`NetworkManager`](crate::NetworkManager) instance.
383    pub fn new(
384        network: NetworkHandle<N>,
385        pool: Pool,
386        from_network: MemoryBoundedReceiver<NetworkTransactionEvent<N>>,
387        transactions_manager_config: TransactionsManagerConfig,
388    ) -> Self {
389        Self::with_policy(
390            network,
391            pool,
392            from_network,
393            transactions_manager_config,
394            NetworkPolicies::new(
395                TransactionPropagationKind::default(),
396                StrictEthAnnouncementFilter::default(),
397            ),
398        )
399    }
400}
401
402impl<Pool: TransactionPool, N: NetworkPrimitives> TransactionsManager<Pool, N> {
403    /// Sets up a new instance with given the settings.
404    ///
405    /// Note: This expects an existing [`NetworkManager`](crate::NetworkManager) instance.
406    pub fn with_policy(
407        network: NetworkHandle<N>,
408        pool: Pool,
409        from_network: MemoryBoundedReceiver<NetworkTransactionEvent<N>>,
410        transactions_manager_config: TransactionsManagerConfig,
411        policies: NetworkPolicies<N>,
412    ) -> Self {
413        let network_events = network.event_listener();
414
415        let (command_tx, command_rx) = mpsc::unbounded_channel();
416
417        let transaction_fetcher = TransactionFetcher::with_transaction_fetcher_config(
418            &transactions_manager_config.transaction_fetcher_config,
419        );
420
421        // install a listener for new __pending__ transactions that are allowed to be propagated
422        // over the network
423        let pending = pool.pending_transactions_listener();
424        let pending_pool_imports_info =
425            PendingPoolImportsInfo::new(transactions_manager_config.max_pending_pool_imports);
426        let metrics = TransactionsManagerMetrics::default();
427        metrics
428            .capacity_pending_pool_imports
429            .increment(pending_pool_imports_info.max_pending_pool_imports as u64);
430
431        Self {
432            pool,
433            sender_recovery_cache: None,
434            network,
435            network_events,
436            transaction_fetcher,
437            transactions_by_peers: Default::default(),
438            pool_imports: Default::default(),
439            pending_pool_imports_info,
440            bad_imports: LruCache::with_hasher(DEFAULT_MAX_COUNT_BAD_IMPORTS, Default::default()),
441            peers: Default::default(),
442            command_tx,
443            command_rx: UnboundedReceiverStream::new(command_rx),
444            pending_transactions: pending,
445            transaction_events: from_network,
446            config: transactions_manager_config,
447            policies,
448            metrics,
449            announced_tx_types_metrics: AnnouncedTxTypesMetrics::default(),
450        }
451    }
452
453    /// Returns a new handle that can send commands to this type.
454    pub fn handle(&self) -> TransactionsHandle<N> {
455        TransactionsHandle { manager_tx: self.command_tx.clone() }
456    }
457
458    /// Uses the provided sender recovery cache.
459    pub fn with_sender_recovery_cache(mut self, cache: SenderRecoveryCache) -> Self {
460        self.sender_recovery_cache = Some(cache);
461        self
462    }
463
464    /// Returns `true` if [`TransactionsManager`] has capacity to request pending hashes. Returns
465    /// `false` if [`TransactionsManager`] is operating close to full capacity.
466    fn has_capacity_for_fetching_pending_hashes(&self) -> bool {
467        self.has_capacity_for_pending_pool_imports() &&
468            self.transaction_fetcher.has_capacity_for_fetching_pending_hashes()
469    }
470
471    /// Returns `true` if [`TransactionsManager`] has capacity for more pending pool imports.
472    fn has_capacity_for_pending_pool_imports(&self) -> bool {
473        self.remaining_pool_import_capacity() > 0
474    }
475
476    /// Returns the remaining capacity for pending pool imports.
477    fn remaining_pool_import_capacity(&self) -> usize {
478        self.pending_pool_imports_info.max_pending_pool_imports.saturating_sub(
479            self.pending_pool_imports_info.pending_pool_imports.load(Ordering::Relaxed),
480        )
481    }
482
483    fn report_peer_bad_transactions(&self, peer_id: PeerId) {
484        self.report_peer(peer_id, ReputationChangeKind::BadTransactions);
485        self.metrics.reported_bad_transactions.increment(1);
486    }
487
488    fn report_peer(&self, peer_id: PeerId, kind: ReputationChangeKind) {
489        trace!(target: "net::tx", ?peer_id, ?kind, "reporting reputation change");
490        self.network.reputation_change(peer_id, kind);
491    }
492
493    fn report_already_seen(&self, peer_id: PeerId) {
494        trace!(target: "net::tx", ?peer_id, "Penalizing peer for already seen transaction");
495        self.network.reputation_change(peer_id, ReputationChangeKind::AlreadySeenTransaction);
496    }
497
498    /// Handles a closed peer session, removing the peer from transaction-local tracking state.
499    fn on_peer_session_closed(&mut self, peer_id: &PeerId) {
500        if let Some(mut peer) = self.peers.remove(peer_id) {
501            self.policies.propagation_policy_mut().on_session_closed(&mut peer);
502        }
503        self.transaction_fetcher.remove_peer(peer_id);
504    }
505
506    /// Clear the transaction
507    fn on_good_import(&mut self, hash: TxHash) {
508        self.transactions_by_peers.remove(&hash);
509    }
510
511    /// Handles a failed transaction import.
512    ///
513    /// Blob sidecar errors (e.g. invalid proof, missing sidecar) are penalized via
514    /// `report_peer_bad_transactions` but NOT cached in `bad_imports` — the transaction itself
515    /// may be valid when fetched from another peer with correct sidecar data.
516    ///
517    /// Other bad transactions are penalized and cached in `bad_imports` to avoid fetching or
518    /// importing them again.
519    ///
520    /// Errors that count as bad transactions are:
521    ///
522    /// - intrinsic gas too low
523    /// - exceeds gas limit
524    /// - gas uint overflow
525    /// - exceeds max init code size
526    /// - oversized data
527    /// - signer account has bytecode
528    /// - chain id mismatch
529    /// - old legacy chain id
530    /// - tx type not supported
531    ///
532    /// (and additionally for blobs txns...)
533    ///
534    /// - no blobs
535    /// - too many blobs
536    /// - invalid kzg proof
537    /// - kzg error
538    /// - not blob transaction (tx type mismatch)
539    /// - wrong versioned kzg commitment hash
540    fn on_bad_import(&mut self, err: PoolError) {
541        let peers = self.transactions_by_peers.remove(&err.hash);
542
543        if err.is_bad_blob_sidecar() {
544            // Blob sidecar errors: penalize but do NOT cache the hash as bad.
545            // The transaction may be valid — only the sidecar from this peer was wrong.
546            // Using regular penalties means repeated offenders still get disconnected.
547            //
548            // Eth/72 peers cannot reach this path with an elided sidecar: their blob
549            // transactions are dropped before import in `import_transactions`, so any sidecar
550            // failure here means actual blob data failed validation.
551            if let Some(peers) = peers {
552                for peer_id in peers {
553                    self.report_peer_bad_transactions(peer_id);
554                }
555            }
556            return
557        }
558
559        // if we're _currently_ syncing, we ignore a bad transaction
560        if !err.is_bad_transaction() || self.network.is_syncing() {
561            return
562        }
563        // otherwise we penalize the peer that sent the bad transaction, with the assumption that
564        // the peer should have known that this transaction is bad (e.g. violating consensus rules)
565        if let Some(peers) = peers {
566            for peer_id in peers {
567                self.report_peer_bad_transactions(peer_id);
568            }
569        }
570        self.metrics.bad_imports.increment(1);
571        self.bad_imports.insert(err.hash);
572    }
573
574    /// Runs an operation to fetch hashes that are cached in [`TransactionFetcher`].
575    ///
576    /// Returns `true` if a request was sent.
577    fn on_fetch_hashes_pending_fetch(&mut self) -> bool {
578        // try drain transaction hashes pending fetch
579        let info = &self.pending_pool_imports_info;
580        let max_pending_pool_imports = info.max_pending_pool_imports;
581        let has_capacity_wrt_pending_pool_imports =
582            |divisor| info.has_capacity(max_pending_pool_imports / divisor);
583
584        self.transaction_fetcher
585            .on_fetch_pending_hashes(&self.peers, has_capacity_wrt_pending_pool_imports)
586    }
587
588    fn on_request_error(&self, peer_id: PeerId, req_err: RequestError) {
589        let kind = match req_err {
590            RequestError::UnsupportedCapability => ReputationChangeKind::BadProtocol,
591            RequestError::Timeout => ReputationChangeKind::Timeout,
592            RequestError::ChannelClosed | RequestError::ConnectionDropped => {
593                // peer is already disconnected
594                return
595            }
596            RequestError::BadResponse => return self.report_peer_bad_transactions(peer_id),
597        };
598        self.report_peer(peer_id, kind);
599    }
600
601    #[inline]
602    fn update_poll_metrics(&self, start: Instant, poll_durations: TxManagerPollDurations) {
603        let metrics = &self.metrics;
604
605        let TxManagerPollDurations {
606            acc_network_events,
607            acc_pending_imports,
608            acc_tx_events,
609            acc_imported_txns,
610            acc_fetch_events,
611            acc_pending_fetch,
612            acc_cmds,
613        } = poll_durations;
614
615        // update metrics for whole poll function
616        metrics.duration_poll_tx_manager.set(start.elapsed().as_secs_f64());
617        // update metrics for nested expressions
618        metrics.acc_duration_poll_network_events.set(acc_network_events.as_secs_f64());
619        metrics.acc_duration_poll_pending_pool_imports.set(acc_pending_imports.as_secs_f64());
620        metrics.acc_duration_poll_transaction_events.set(acc_tx_events.as_secs_f64());
621        metrics.acc_duration_poll_imported_transactions.set(acc_imported_txns.as_secs_f64());
622        metrics.acc_duration_poll_fetch_events.set(acc_fetch_events.as_secs_f64());
623        metrics.acc_duration_fetch_pending_hashes.set(acc_pending_fetch.as_secs_f64());
624        metrics.acc_duration_poll_commands.set(acc_cmds.as_secs_f64());
625    }
626}
627
628impl<Pool: TransactionPool, N: NetworkPrimitives> TransactionsManager<Pool, N> {
629    /// Processes a batch import results.
630    fn on_batch_import_result(&mut self, batch_results: Vec<PoolResult<AddedTransactionOutcome>>) {
631        for res in batch_results {
632            match res {
633                Ok(AddedTransactionOutcome { hash, .. }) => {
634                    self.on_good_import(hash);
635                }
636                Err(err) => {
637                    self.on_bad_import(err);
638                }
639            }
640        }
641    }
642
643    /// Request handler for an incoming `NewPooledTransactionHashes`
644    fn on_new_pooled_transaction_hashes(
645        &mut self,
646        peer_id: PeerId,
647        msg: NewPooledTransactionHashes,
648    ) {
649        // If the node is initially syncing, ignore transactions
650        if self.network.is_initially_syncing() {
651            return
652        }
653        if self.network.tx_gossip_disabled() {
654            return
655        }
656
657        // get handle to peer's session, if the session is still active
658        let Some(peer) = self.peers.get_mut(&peer_id) else {
659            trace!(
660                peer_id = format!("{peer_id:#}"),
661                ?msg,
662                "discarding announcement from inactive peer"
663            );
664
665            return
666        };
667        let client = peer.client_version.clone();
668
669        // keep track of the transactions the peer knows
670        let mut count_txns_already_seen_by_peer = 0;
671        for tx in msg.iter_hashes().copied() {
672            if !peer.seen_transactions.insert(tx) {
673                count_txns_already_seen_by_peer += 1;
674            }
675        }
676        if count_txns_already_seen_by_peer > 0 {
677            // this may occur if transactions are sent or announced to a peer, at the same time as
678            // the peer sends/announces those hashes to us. this is because, marking
679            // txns as seen by a peer is done optimistically upon sending them to the
680            // peer.
681            self.metrics.messages_with_hashes_already_seen_by_peer.increment(1);
682            self.metrics
683                .occurrences_hash_already_seen_by_peer
684                .increment(count_txns_already_seen_by_peer);
685
686            trace!(target: "net::tx",
687                %count_txns_already_seen_by_peer,
688                peer_id=format!("{peer_id:#}"),
689                ?client,
690                "Peer sent hashes that have already been marked as seen by peer"
691            );
692
693            self.report_already_seen(peer_id);
694        }
695
696        // 1. filter out spam
697        if msg.is_empty() {
698            self.report_peer(peer_id, ReputationChangeKind::BadAnnouncement);
699            return;
700        }
701
702        let original_len = msg.len();
703        let mut partially_valid_msg = msg.dedup();
704
705        if partially_valid_msg.len() != original_len {
706            self.report_peer(peer_id, ReputationChangeKind::BadAnnouncement);
707        }
708
709        // 2. filter out transactions pending import to pool
710        partially_valid_msg.retain_by_hash(|hash| !self.transactions_by_peers.contains_key(hash));
711
712        // 3. filter out invalid entries (spam)
713        //
714        // validates messages with respect to the given network, e.g. allowed tx types.
715        // done before the pool lookup since these are cheap in-memory checks that shrink
716        // the set before acquiring the pool lock.
717        //
718        let mut should_report_peer = false;
719        let mut tx_types_counter = TxTypesCounter::default();
720
721        let has_eth68_metadata = partially_valid_msg
722            .msg_version()
723            .expect("partially valid announcement should have a version")
724            .has_eth68_metadata();
725
726        partially_valid_msg.retain(|tx_hash, metadata_ref_mut| {
727            let (ty_byte, size_val) = match *metadata_ref_mut {
728                Some((ty, size)) => {
729                    if !has_eth68_metadata {
730                        should_report_peer = true;
731                    }
732                    (ty, size)
733                }
734                None => {
735                    if has_eth68_metadata {
736                        should_report_peer = true;
737                        return false;
738                    }
739                    (0u8, 0)
740                }
741            };
742
743            if has_eth68_metadata && let Some((actual_ty_byte, _)) = *metadata_ref_mut {
744                match TxType::try_from(actual_ty_byte) {
745                    Ok(parsed_tx_type) => tx_types_counter.increase_by_tx_type(parsed_tx_type),
746                    Err(_) => tx_types_counter.increase_other(),
747                }
748            }
749
750            let decision = self
751                .policies
752                .announcement_filter()
753                .decide_on_announcement(ty_byte, tx_hash, size_val);
754
755            match decision {
756                AnnouncementAcceptance::Accept => true,
757                AnnouncementAcceptance::Ignore => false,
758                AnnouncementAcceptance::Reject { penalize_peer } => {
759                    if penalize_peer {
760                        should_report_peer = true;
761                    }
762                    false
763                }
764            }
765        });
766
767        if has_eth68_metadata {
768            self.announced_tx_types_metrics.update_eth68_announcement_metrics(tx_types_counter);
769        }
770
771        if should_report_peer {
772            self.report_peer(peer_id, ReputationChangeKind::BadAnnouncement);
773        }
774
775        // 4. filter out known hashes
776        //
777        // known txns have already been successfully fetched or received over gossip.
778        //
779        // most hashes will be filtered out here since the mempool protocol is a gossip
780        // protocol, healthy peers will send many of the same hashes.
781        //
782        let hashes_count_pre_pool_filter = partially_valid_msg.len();
783        self.pool.retain_unknown(&mut partially_valid_msg);
784        if hashes_count_pre_pool_filter > partially_valid_msg.len() {
785            let already_known_hashes_count =
786                hashes_count_pre_pool_filter - partially_valid_msg.len();
787            self.metrics
788                .occurrences_hashes_already_in_pool
789                .increment(already_known_hashes_count as u64);
790        }
791
792        if partially_valid_msg.is_empty() {
793            // nothing to request
794            return
795        }
796
797        let mut valid_announcement_data =
798            ValidAnnouncementData::from_partially_valid_data(partially_valid_msg);
799
800        if valid_announcement_data.is_empty() {
801            // no valid announcement data
802            return
803        }
804
805        // 5. filter out already seen unknown hashes
806        //
807        // seen hashes are already in the tx fetcher, pending fetch.
808        //
809        // for any seen hashes add the peer as fallback. unseen hashes are loaded into the tx
810        // fetcher, hence they should be valid at this point.
811        let bad_imports = &self.bad_imports;
812        self.transaction_fetcher.filter_unseen_and_pending_hashes(
813            &mut valid_announcement_data,
814            |hash| bad_imports.contains(hash),
815            &peer_id,
816            &client,
817        );
818
819        if valid_announcement_data.is_empty() {
820            // nothing to request
821            return
822        }
823
824        trace!(target: "net::tx::propagation",
825            peer_id=format!("{peer_id:#}"),
826            hashes_len=valid_announcement_data.len(),
827            hashes=?valid_announcement_data.keys(),
828            msg_version=%valid_announcement_data.msg_version(),
829            client_version=%client,
830            "received previously unseen and pending hashes in announcement from peer"
831        );
832
833        // only send request for hashes to idle peer, otherwise buffer hashes storing peer as
834        // fallback
835        if !self.transaction_fetcher.is_idle(&peer_id) {
836            // load message version before announcement data is destructed in packing
837            let msg_version = valid_announcement_data.msg_version();
838            let (hashes, _version) = valid_announcement_data.into_request_hashes();
839
840            trace!(target: "net::tx",
841                peer_id=format!("{peer_id:#}"),
842                hashes=?*hashes,
843                %msg_version,
844                %client,
845                "buffering hashes announced by busy peer"
846            );
847
848            self.transaction_fetcher.buffer_hashes(hashes, Some(peer_id));
849
850            return
851        }
852
853        let mut hashes_to_request =
854            RequestTxHashes::with_capacity(valid_announcement_data.len() / 4);
855        let surplus_hashes =
856            self.transaction_fetcher.pack_request(&mut hashes_to_request, valid_announcement_data);
857
858        if !surplus_hashes.is_empty() {
859            trace!(target: "net::tx",
860                peer_id=format!("{peer_id:#}"),
861                surplus_hashes=?*surplus_hashes,
862                %client,
863                "some hashes in announcement from peer didn't fit in `GetPooledTransactions` request, buffering surplus hashes"
864            );
865
866            self.transaction_fetcher.buffer_hashes(surplus_hashes, Some(peer_id));
867        }
868
869        trace!(target: "net::tx",
870            peer_id=format!("{peer_id:#}"),
871            hashes=?*hashes_to_request,
872            %client,
873            "sending hashes in `GetPooledTransactions` request to peer's session"
874        );
875
876        // request the missing transactions
877        //
878        // get handle to peer's session again, at this point we know it exists
879        let Some(peer) = self.peers.get_mut(&peer_id) else { return };
880        if let Some(failed_to_request_hashes) =
881            self.transaction_fetcher.request_transactions_from_peer(hashes_to_request, peer)
882        {
883            let conn_eth_version = peer.version;
884
885            trace!(target: "net::tx",
886                peer_id=format!("{peer_id:#}"),
887                failed_to_request_hashes=?*failed_to_request_hashes,
888                %conn_eth_version,
889                %client,
890                "sending `GetPooledTransactions` request to peer's session failed, buffering hashes"
891            );
892            self.transaction_fetcher.buffer_hashes(failed_to_request_hashes, Some(peer_id));
893        }
894    }
895}
896
897impl<Pool, N> TransactionsManager<Pool, N>
898where
899    Pool: TransactionPool + Unpin + 'static,
900    N: NetworkPrimitives<
901            BroadcastedTransaction: SignedTransaction,
902            PooledTransaction: SignedTransaction,
903        > + Unpin,
904    Pool::Transaction:
905        PoolTransaction<Consensus = N::BroadcastedTransaction, Pooled = N::PooledTransaction>,
906{
907    /// Invoked when transactions in the local mempool are considered __pending__.
908    ///
909    /// When a transaction in the local mempool is moved to the pending pool, we propagate them to
910    /// connected peers over network using the `Transactions` and `NewPooledTransactionHashes`
911    /// messages. The Transactions message relays complete transaction objects and is typically
912    /// sent to a small, random fraction of connected peers.
913    ///
914    /// All other peers receive a notification of the transaction hash and can request the
915    /// complete transaction object if it is unknown to them. The dissemination of complete
916    /// transactions to a fraction of peers usually ensures that all nodes receive the transaction
917    /// and won't need to request it.
918    fn on_new_pending_transactions(&mut self, hashes: Vec<TxHash>) {
919        // We intentionally do not gate this on initial sync.
920        // During initial sync we skip importing tx announcements from peers in
921        // `on_new_pooled_transaction_hashes`, so transactions reaching this path are local.
922        if self.network.tx_gossip_disabled() {
923            return
924        }
925
926        trace!(target: "net::tx", num_hashes=?hashes.len(), "Start propagating transactions");
927
928        self.propagate_all(hashes);
929    }
930
931    /// Propagate the full transactions to a specific peer.
932    ///
933    /// Returns the propagated transactions.
934    fn propagate_full_transactions_to_peer(
935        &mut self,
936        txs: Vec<TxHash>,
937        peer_id: PeerId,
938        propagation_mode: PropagationMode,
939    ) -> Option<PropagatedTransactions> {
940        let peer = self.peers.get_mut(&peer_id)?;
941        trace!(target: "net::tx", ?peer_id, "Propagating transactions to peer");
942        let mut propagated = PropagatedTransactions::default();
943
944        // filter all transactions unknown to the peer
945        let mut full_transactions = FullTransactionsBuilder::new(peer.version);
946
947        let to_propagate = self.pool.get_all(txs).into_iter().map(PropagateTransaction::pool_tx);
948
949        if propagation_mode.is_forced() {
950            // skip cache check if forced
951            full_transactions.extend(to_propagate);
952        } else {
953            // Iterate through the transactions to propagate and fill the hashes and full
954            // transaction
955            for tx in to_propagate {
956                if !peer.seen_transactions.contains(tx.tx_hash()) {
957                    // Only include if the peer hasn't seen the transaction
958                    full_transactions.push(&tx);
959                }
960            }
961        }
962
963        if full_transactions.is_empty() {
964            // nothing to propagate
965            return None
966        }
967
968        let PropagateTransactions { pooled, full } = full_transactions.build();
969
970        // send hashes if any
971        if let Some(new_pooled_hashes) = pooled {
972            for hash in new_pooled_hashes.iter_hashes().copied() {
973                propagated.record(hash, PropagateKind::Hash(peer_id));
974                // mark transaction as seen by peer
975                peer.seen_transactions.insert(hash);
976            }
977
978            // send hashes of transactions
979            self.network.send_transactions_hashes(peer_id, new_pooled_hashes);
980        }
981
982        // send full transactions, if any
983        if let Some(new_full_transactions) = full {
984            for hash in new_full_transactions.iter_hashes() {
985                propagated.record(*hash, PropagateKind::Full(peer_id));
986                // mark transaction as seen by peer
987                peer.seen_transactions.insert(*hash);
988            }
989
990            // send full transactions
991            self.network.send_broadcast_pool_transactions(peer_id, new_full_transactions);
992        }
993
994        // Update propagated transactions metrics
995        self.metrics.propagated_transactions.increment(propagated.len() as u64);
996
997        Some(propagated)
998    }
999
1000    /// Propagate the transaction hashes to the given peer
1001    ///
1002    /// Note: This will only send the hashes for transactions that exist in the pool.
1003    fn propagate_hashes_to(
1004        &mut self,
1005        hashes: Vec<TxHash>,
1006        peer_id: PeerId,
1007        propagation_mode: PropagationMode,
1008    ) {
1009        trace!(target: "net::tx", "Start propagating transactions as hashes");
1010
1011        // This fetches a transactions from the pool, including the blob transactions, which are
1012        // only ever sent as hashes.
1013        let propagated = {
1014            let Some(peer) = self.peers.get_mut(&peer_id) else {
1015                // no such peer
1016                return
1017            };
1018
1019            let to_propagate =
1020                self.pool.get_all(hashes).into_iter().map(PropagateTransaction::pool_tx);
1021
1022            let mut propagated = PropagatedTransactions::default();
1023
1024            // check if transaction is known to peer
1025            let mut hashes = PooledTransactionsHashesBuilder::new(peer.version);
1026
1027            if propagation_mode.is_forced() {
1028                hashes.extend(to_propagate)
1029            } else {
1030                for tx in to_propagate {
1031                    if !peer.seen_transactions.contains(tx.tx_hash()) {
1032                        // Include if the peer hasn't seen it
1033                        hashes.push(&tx);
1034                    }
1035                }
1036            }
1037
1038            let new_pooled_hashes = hashes.build();
1039
1040            if new_pooled_hashes.is_empty() {
1041                // nothing to propagate
1042                return
1043            }
1044
1045            for hash in new_pooled_hashes.iter_hashes().copied() {
1046                propagated.record(hash, PropagateKind::Hash(peer_id));
1047                peer.seen_transactions.insert(hash);
1048            }
1049
1050            trace!(target: "net::tx::propagation", ?peer_id, ?new_pooled_hashes, "Propagating transactions to peer");
1051
1052            // send hashes of transactions
1053            self.network.send_transactions_hashes(peer_id, new_pooled_hashes);
1054
1055            // Update propagated transactions metrics
1056            self.metrics.propagated_transactions.increment(propagated.len() as u64);
1057
1058            propagated
1059        };
1060
1061        // notify pool so events get fired
1062        self.pool.on_propagated(propagated);
1063    }
1064
1065    /// Propagate the transactions to all connected peers either as full objects or hashes.
1066    ///
1067    /// The message for new pooled hashes depends on the negotiated version of the stream.
1068    /// See [`NewPooledTransactionHashes`]
1069    ///
1070    /// Note: EIP-4844 are disallowed from being broadcast in full and are only ever sent as hashes, see also <https://eips.ethereum.org/EIPS/eip-4844#networking>.
1071    fn propagate_transactions(
1072        &mut self,
1073        to_propagate: Vec<PropagateTransaction>,
1074        propagation_mode: PropagationMode,
1075    ) -> PropagatedTransactions {
1076        let mut propagated = PropagatedTransactions::default();
1077        if self.network.tx_gossip_disabled() {
1078            return propagated
1079        }
1080
1081        // send full transactions to a set of the connected peers based on the configured mode
1082        let max_num_full = self.config.propagation_mode.full_peer_count(self.peers.len());
1083
1084        // Note: Assuming ~random~ order due to random state of the peers map hasher
1085        let mut num_full_peers = 0;
1086        for (peer_id, peer) in &mut self.peers {
1087            if !self.policies.propagation_policy().can_propagate(peer) {
1088                // skip peers we should not propagate to
1089                continue
1090            }
1091
1092            // determine whether to send full tx objects or hashes.
1093            let mut builder = if num_full_peers < max_num_full {
1094                num_full_peers += 1;
1095                PropagateTransactionsBuilder::full(peer.version, to_propagate.len())
1096            } else {
1097                PropagateTransactionsBuilder::pooled(peer.version, to_propagate.len())
1098            };
1099
1100            // Transactions are optimistically marked as seen by the peer when included in the
1101            // message, see `PeerMetadata::seen_transactions`.
1102            if propagation_mode.is_forced() {
1103                for tx in &to_propagate {
1104                    peer.seen_transactions.insert(*tx.tx_hash());
1105                    builder.push(tx);
1106                }
1107            } else {
1108                // Iterate through the transactions to propagate and fill the hashes and full
1109                // transaction lists, before deciding whether or not to send full transactions to
1110                // the peer.
1111                for tx in &to_propagate {
1112                    // Only include the transaction if the peer hasn't seen it yet
1113                    if peer.seen_transactions.insert(*tx.tx_hash()) {
1114                        builder.push(tx);
1115                    }
1116                }
1117            }
1118
1119            if builder.is_empty() {
1120                trace!(target: "net::tx", ?peer_id, "Nothing to propagate to peer; has seen all transactions");
1121                continue
1122            }
1123
1124            let PropagateTransactions { pooled, full } = builder.build();
1125
1126            // send hashes if any
1127            if let Some(mut new_pooled_hashes) = pooled {
1128                // Unhappy path: too many hashes for a single message. This should not happen
1129                // during regular propagation, which is capped at the soft limit per batch, and
1130                // is only reachable via manual propagation commands with oversized batches.
1131                if new_pooled_hashes.len() >
1132                    SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE
1133                {
1134                    // hashes that exceed the limit are not sent, so they must not be tracked as
1135                    // seen by the peer
1136                    for hash in new_pooled_hashes
1137                        .iter_hashes()
1138                        .skip(SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE)
1139                    {
1140                        peer.seen_transactions.remove(hash);
1141                    }
1142                    new_pooled_hashes.truncate(
1143                        SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE,
1144                    );
1145                }
1146
1147                for hash in new_pooled_hashes.iter_hashes().copied() {
1148                    propagated.record(hash, PropagateKind::Hash(*peer_id));
1149                }
1150
1151                trace!(target: "net::tx", ?peer_id, num_txs=?new_pooled_hashes.len(), "Propagating tx hashes to peer");
1152
1153                // send hashes of transactions
1154                self.network.send_transactions_hashes(*peer_id, new_pooled_hashes);
1155            }
1156
1157            // send full transactions, if any
1158            if let Some(new_full_transactions) = full {
1159                for hash in new_full_transactions.iter_hashes() {
1160                    propagated.record(*hash, PropagateKind::Full(*peer_id));
1161                }
1162
1163                trace!(target: "net::tx", ?peer_id, num_txs=?new_full_transactions.len(), "Propagating full transactions to peer");
1164
1165                // send full transactions
1166                self.network.send_broadcast_pool_transactions(*peer_id, new_full_transactions);
1167            }
1168        }
1169
1170        // Update propagated transactions metrics
1171        self.metrics.propagated_transactions.increment(propagated.len() as u64);
1172
1173        propagated
1174    }
1175
1176    /// Propagates the given transactions to the peers
1177    ///
1178    /// This fetches all transaction from the pool, including the 4844 blob transactions but
1179    /// __without__ their sidecar, because 4844 transactions are only ever announced as hashes.
1180    fn propagate_all(&mut self, hashes: Vec<TxHash>) {
1181        if self.peers.is_empty() {
1182            // nothing to propagate
1183            return
1184        }
1185        let propagated = self.propagate_transactions(
1186            self.pool.get_all(hashes).into_iter().map(PropagateTransaction::pool_tx).collect(),
1187            PropagationMode::Basic,
1188        );
1189
1190        // notify pool so events get fired
1191        self.pool.on_propagated(propagated);
1192    }
1193
1194    /// Request handler for an incoming request for transactions
1195    fn on_get_pooled_transactions(
1196        &mut self,
1197        peer_id: PeerId,
1198        request: GetPooledTransactions,
1199        response: oneshot::Sender<RequestResult<PooledTransactions<N::PooledTransaction>>>,
1200    ) {
1201        // fast exit if gossip is disabled
1202        if self.network.tx_gossip_disabled() {
1203            let _ = response.send(Ok(PooledTransactions::default()));
1204            return
1205        }
1206        if let Some(peer) = self.peers.get_mut(&peer_id) {
1207            let transactions = self.pool.get_pooled_transaction_elements(
1208                request.0,
1209                GetPooledTransactionLimit::ResponseSizeSoftLimit(
1210                    self.transaction_fetcher.info.soft_limit_byte_size_pooled_transactions_response,
1211                ),
1212            );
1213            trace!(target: "net::tx::propagation", sent_txs=?transactions.iter().map(|tx| tx.tx_hash()), "Sending requested transactions to peer");
1214
1215            // we sent a response at which point we assume that the peer is aware of the
1216            // transactions
1217            peer.seen_transactions.extend(transactions.iter().map(|tx| *tx.tx_hash()));
1218
1219            let resp = PooledTransactions(transactions);
1220            let _ = response.send(Ok(resp));
1221        }
1222    }
1223
1224    /// Handles a command received from a detached [`TransactionsHandle`]
1225    fn on_command(&mut self, cmd: TransactionsCommand<N>) {
1226        match cmd {
1227            TransactionsCommand::PropagateHash(hash) => {
1228                self.on_new_pending_transactions(vec![hash])
1229            }
1230            TransactionsCommand::PropagateHashesTo(hashes, peer) => {
1231                self.propagate_hashes_to(hashes, peer, PropagationMode::Forced)
1232            }
1233            TransactionsCommand::GetActivePeers(tx) => {
1234                let peers = self.peers.keys().copied().collect::<HashSet<_>>();
1235                tx.send(peers).ok();
1236            }
1237            TransactionsCommand::PropagateTransactionsTo(txs, peer) => {
1238                if let Some(propagated) =
1239                    self.propagate_full_transactions_to_peer(txs, peer, PropagationMode::Forced)
1240                {
1241                    self.pool.on_propagated(propagated);
1242                }
1243            }
1244            TransactionsCommand::PropagateTransactions(txs) => self.propagate_all(txs),
1245            TransactionsCommand::BroadcastTransactions(txs) => {
1246                let propagated = self.propagate_transactions(txs, PropagationMode::Forced);
1247                self.pool.on_propagated(propagated);
1248            }
1249            TransactionsCommand::GetTransactionHashes { peers, tx } => {
1250                let mut res = HashMap::with_capacity_and_hasher(peers.len(), Default::default());
1251                for peer_id in peers {
1252                    let hashes = self
1253                        .peers
1254                        .get(&peer_id)
1255                        .map(|peer| peer.seen_transactions.iter().copied().collect::<B256Set>())
1256                        .unwrap_or_default();
1257                    res.insert(peer_id, hashes);
1258                }
1259                tx.send(res).ok();
1260            }
1261            TransactionsCommand::GetPeerSender { peer_id, peer_request_sender } => {
1262                let sender = self.peers.get(&peer_id).map(|peer| peer.request_tx.clone());
1263                peer_request_sender.send(sender).ok();
1264            }
1265        }
1266    }
1267
1268    /// Handles session establishment and peer transactions initialization.
1269    ///
1270    /// This is invoked when a new session is established.
1271    fn handle_peer_session(
1272        &mut self,
1273        info: SessionInfo,
1274        messages: PeerRequestSender<PeerRequest<N>>,
1275    ) {
1276        let SessionInfo { peer_id, client_version, version, .. } = info;
1277
1278        // Insert a new peer into the peerset.
1279        let peer = PeerMetadata::<N>::new(
1280            messages,
1281            version,
1282            client_version,
1283            self.config.max_transactions_seen_by_peer_history,
1284            info.peer_kind,
1285        );
1286        let peer = match self.peers.entry(peer_id) {
1287            Entry::Occupied(mut entry) => {
1288                entry.insert(peer);
1289                entry.into_mut()
1290            }
1291            Entry::Vacant(entry) => entry.insert(peer),
1292        };
1293
1294        self.policies.propagation_policy_mut().on_session_established(peer);
1295
1296        // Send a `NewPooledTransactionHashes` to the peer with up to
1297        // `SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE`
1298        // transactions in the pool.
1299        if self.network.is_initially_syncing() || self.network.tx_gossip_disabled() {
1300            trace!(target: "net::tx", ?peer_id, "Skipping transaction broadcast: node syncing or gossip disabled");
1301            return
1302        }
1303
1304        // Get transactions to broadcast
1305        let pooled_txs = self.pool.pooled_transactions_max(
1306            SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE,
1307        );
1308        if pooled_txs.is_empty() {
1309            trace!(target: "net::tx", ?peer_id, "No transactions in the pool to broadcast");
1310            return;
1311        }
1312
1313        // Build and send transaction hashes message
1314        let mut msg_builder = PooledTransactionsHashesBuilder::new(version);
1315        for pooled_tx in pooled_txs {
1316            peer.seen_transactions.insert(*pooled_tx.hash());
1317            msg_builder.push_pooled(pooled_tx);
1318        }
1319
1320        debug!(target: "net::tx", ?peer_id, tx_count = msg_builder.len(), "Broadcasting transaction hashes");
1321        let msg = msg_builder.build();
1322        self.network.send_transactions_hashes(peer_id, msg);
1323    }
1324
1325    /// Handles a received event related to common network events.
1326    fn on_network_event(&mut self, event_result: NetworkEvent<PeerRequest<N>>) {
1327        match event_result {
1328            NetworkEvent::Peer(PeerEvent::SessionClosed { peer_id, .. }) => {
1329                self.on_peer_session_closed(&peer_id);
1330            }
1331            NetworkEvent::ActivePeerSession { info, messages } => {
1332                // process active peer session and broadcast available transaction from the pool
1333                self.handle_peer_session(info, messages);
1334            }
1335            NetworkEvent::Peer(PeerEvent::SessionEstablished(info)) => {
1336                let peer_id = info.peer_id;
1337                // get messages from existing peer
1338                let messages = match self.peers.get(&peer_id) {
1339                    Some(p) => p.request_tx.clone(),
1340                    None => {
1341                        debug!(target: "net::tx", ?peer_id, "No peer request sender found");
1342                        return;
1343                    }
1344                };
1345                self.handle_peer_session(info, messages);
1346            }
1347            _ => {}
1348        }
1349    }
1350
1351    /// Returns true if the ingress policy allows processing messages from the given peer.
1352    fn accepts_incoming_from(&self, peer_id: &PeerId) -> bool {
1353        if self.config.ingress_policy.allows_all() {
1354            return true;
1355        }
1356        let Some(peer) = self.peers.get(peer_id) else {
1357            return false;
1358        };
1359        self.config.ingress_policy.allows(peer.peer_kind())
1360    }
1361
1362    /// Handles dedicated transaction events related to the `eth` protocol.
1363    fn on_network_tx_event(&mut self, event: NetworkTransactionEvent<N>) {
1364        match event {
1365            NetworkTransactionEvent::IncomingTransactions { peer_id, msg } => {
1366                if !self.accepts_incoming_from(&peer_id) {
1367                    trace!(target: "net::tx", peer_id=format!("{peer_id:#}"), policy=?self.config.ingress_policy, "Ignoring full transactions from peer blocked by ingress policy");
1368                    return;
1369                }
1370
1371                // ensure we didn't receive any blob transactions as these are disallowed to be
1372                // broadcasted in full
1373
1374                let has_blob_txs = msg.has_eip4844();
1375
1376                let non_blob_txs = msg
1377                    .into_iter()
1378                    .map(N::PooledTransaction::try_from)
1379                    .filter_map(Result::ok)
1380                    .collect();
1381
1382                self.import_transactions(peer_id, non_blob_txs, TransactionSource::Broadcast);
1383
1384                if has_blob_txs {
1385                    debug!(target: "net::tx", ?peer_id, "received bad full blob transaction broadcast");
1386                    self.report_peer_bad_transactions(peer_id);
1387                }
1388            }
1389            NetworkTransactionEvent::IncomingPooledTransactionHashes { peer_id, msg } => {
1390                if !self.accepts_incoming_from(&peer_id) {
1391                    trace!(target: "net::tx", peer_id=format!("{peer_id:#}"), policy=?self.config.ingress_policy, "Ignoring transaction hashes from peer blocked by ingress policy");
1392                    return;
1393                }
1394                self.on_new_pooled_transaction_hashes(peer_id, msg)
1395            }
1396            NetworkTransactionEvent::GetPooledTransactions { peer_id, request, response } => {
1397                self.on_get_pooled_transactions(peer_id, request, response)
1398            }
1399            NetworkTransactionEvent::GetTransactionsHandle(response) => {
1400                let _ = response.send(Some(self.handle()));
1401            }
1402        }
1403    }
1404
1405    /// Starts the import process for the given transactions.
1406    fn import_transactions(
1407        &mut self,
1408        peer_id: PeerId,
1409        transactions: PooledTransactions<N::PooledTransaction>,
1410        source: TransactionSource,
1411    ) {
1412        // If the node is pipeline syncing, ignore transactions
1413        if self.network.is_initially_syncing() {
1414            return
1415        }
1416        if self.network.tx_gossip_disabled() {
1417            return
1418        }
1419
1420        // Early return if we don't have capacity for any imports
1421        if !self.has_capacity_for_pending_pool_imports() {
1422            return
1423        }
1424
1425        let mut transactions = transactions.0;
1426
1427        // Truncate to remaining capacity early to bound work on all subsequent processing.
1428        // Well-behaved peers follow the 4096 soft limit, so oversized payloads are likely
1429        // malicious and we avoid wasting CPU on them.
1430        let capacity = self.remaining_pool_import_capacity();
1431        if transactions.len() > capacity {
1432            let skipped = transactions.len() - capacity;
1433            transactions.truncate(capacity);
1434            self.metrics
1435                .skipped_transactions_pending_pool_imports_at_capacity
1436                .increment(skipped as u64);
1437            trace!(target: "net::tx", skipped, capacity, "Truncated transactions batch to capacity");
1438        }
1439
1440        let Some(peer) = self.peers.get_mut(&peer_id) else { return };
1441        let client_version = peer.client_version.clone();
1442
1443        let start = Instant::now();
1444
1445        // mark the transactions as received
1446        self.transaction_fetcher
1447            .remove_hashes_from_transaction_fetcher(transactions.iter().map(|tx| tx.tx_hash()));
1448
1449        // track that the peer knows these transaction, but only if this is a new broadcast.
1450        // If we received the transactions as the response to our `GetPooledTransactions``
1451        // requests (based on received `NewPooledTransactionHashes`) then we already
1452        // recorded the hashes as seen by this peer in `Self::on_new_pooled_transaction_hashes`.
1453        let mut num_already_seen_by_peer = 0;
1454        for tx in &transactions {
1455            if source.is_broadcast() && !peer.seen_transactions.insert(*tx.tx_hash()) {
1456                num_already_seen_by_peer += 1;
1457            }
1458        }
1459
1460        // Eth/72 `PooledTransactions` responses elide blob payloads from type 3 transactions per
1461        // EIP-8070, so their sidecars can never validate. Drop them before touching the pool;
1462        // geth equivalently diverts these bodies into a buffer that is completed with cells
1463        // fetched via `GetCells`, which is not implemented yet.
1464        if peer.version() == EthVersion::Eth72 {
1465            let len_before = transactions.len();
1466            transactions.retain(|tx| !tx.is_eip4844());
1467            let dropped = len_before - transactions.len();
1468            if dropped > 0 {
1469                trace!(target: "net::tx",
1470                    peer_id=format!("{peer_id:#}"),
1471                    dropped,
1472                    "dropped blob transactions from eth72 response, cell fetching not implemented"
1473                );
1474            }
1475        }
1476
1477        // tracks the quality of the given transactions
1478        let mut has_bad_transactions = false;
1479
1480        // 1. Remove known, already-tracked, and invalid transactions first since these are
1481        // cheap in-memory checks against local maps
1482        transactions.retain(|tx| {
1483            if let Entry::Occupied(mut entry) = self.transactions_by_peers.entry(*tx.tx_hash()) {
1484                let peers = entry.get_mut();
1485                if !peers.contains(&peer_id) {
1486                    peers.push(peer_id);
1487                }
1488                return false
1489            }
1490            if self.bad_imports.contains(tx.tx_hash()) {
1491                trace!(target: "net::tx",
1492                    peer_id=format!("{peer_id:#}"),
1493                    hash=%tx.tx_hash(),
1494                    %client_version,
1495                    "received a known bad transaction from peer"
1496                );
1497                has_bad_transactions = true;
1498                return false;
1499            }
1500            true
1501        });
1502
1503        // 2. filter out txns already inserted into pool
1504        let txns_count_pre_pool_filter = transactions.len();
1505        self.pool.retain_unknown(&mut transactions);
1506        if txns_count_pre_pool_filter > transactions.len() {
1507            let already_known_txns_count = txns_count_pre_pool_filter - transactions.len();
1508            self.metrics
1509                .occurrences_transactions_already_in_pool
1510                .increment(already_known_txns_count as u64);
1511        }
1512
1513        let txs_len = transactions.len();
1514
1515        let recover = |tx| {
1516            let recovered = if let Some(cache) = &self.sender_recovery_cache {
1517                Pool::Transaction::try_recover_with_cache(tx, cache)
1518            } else {
1519                Pool::Transaction::try_recover(tx)
1520            };
1521            match recovered {
1522                Ok(tx) => Some(tx),
1523                Err(badtx) => {
1524                    trace!(target: "net::tx",
1525                        peer_id=format!("{peer_id:#}"),
1526                        hash=%badtx.tx_hash(),
1527                        client_version=%client_version,
1528                        "failed ecrecovery for transaction"
1529                    );
1530                    None
1531                }
1532            }
1533        };
1534
1535        let new_txs = transactions.into_par_iter().filter_map(recover).collect::<Vec<_>>();
1536
1537        has_bad_transactions |= new_txs.len() != txs_len;
1538
1539        // Record the transactions as seen by the peer
1540        for tx in &new_txs {
1541            self.transactions_by_peers.insert(*tx.hash(), smallvec::smallvec![peer_id]);
1542        }
1543
1544        // 3. import new transactions as a batch to minimize lock contention on the underlying
1545        // pool
1546        if !new_txs.is_empty() {
1547            let pool = self.pool.clone();
1548            // update metrics
1549            let metric_pending_pool_imports = self.metrics.pending_pool_imports.clone();
1550            metric_pending_pool_imports.increment(new_txs.len() as f64);
1551
1552            // update self-monitoring info
1553            self.pending_pool_imports_info
1554                .pending_pool_imports
1555                .fetch_add(new_txs.len(), Ordering::Relaxed);
1556            let tx_manager_info_pending_pool_imports =
1557                self.pending_pool_imports_info.pending_pool_imports.clone();
1558
1559            trace!(target: "net::tx::propagation", new_txs_len=?new_txs.len(), "Importing new transactions");
1560            let import = Box::pin(async move {
1561                let added = new_txs.len();
1562                let res = pool.add_external_transactions(new_txs).await;
1563
1564                // update metrics
1565                metric_pending_pool_imports.decrement(added as f64);
1566                // update self-monitoring info
1567                tx_manager_info_pending_pool_imports.fetch_sub(added, Ordering::Relaxed);
1568
1569                res
1570            });
1571
1572            self.pool_imports.push(import);
1573        }
1574
1575        if num_already_seen_by_peer > 0 {
1576            self.metrics.messages_with_transactions_already_seen_by_peer.increment(1);
1577            self.metrics
1578                .occurrences_of_transaction_already_seen_by_peer
1579                .increment(num_already_seen_by_peer);
1580            trace!(target: "net::tx", num_txs=%num_already_seen_by_peer, ?peer_id, client=%client_version, "Peer sent already seen transactions");
1581        }
1582
1583        if has_bad_transactions {
1584            // peer sent us invalid transactions
1585            self.report_peer_bad_transactions(peer_id)
1586        }
1587
1588        if num_already_seen_by_peer > 0 {
1589            self.report_already_seen(peer_id);
1590        }
1591
1592        self.metrics.pool_import_prepare_duration.record(start.elapsed());
1593    }
1594
1595    /// Processes a [`FetchEvent`].
1596    fn on_fetch_event(&mut self, fetch_event: FetchEvent<N::PooledTransaction>) {
1597        match fetch_event {
1598            FetchEvent::TransactionsFetched { peer_id, transactions, report_peer } => {
1599                self.import_transactions(peer_id, transactions, TransactionSource::Response);
1600                if report_peer {
1601                    self.report_peer(peer_id, ReputationChangeKind::BadTransactions);
1602                }
1603            }
1604            FetchEvent::FetchError { peer_id, error } => {
1605                trace!(target: "net::tx", ?peer_id, %error, "requesting transactions from peer failed");
1606                self.on_request_error(peer_id, error);
1607            }
1608            FetchEvent::EmptyResponse { peer_id } => {
1609                trace!(target: "net::tx", ?peer_id, "peer returned empty response");
1610            }
1611        }
1612    }
1613}
1614
1615/// An endless future. Preemption ensure that future is non-blocking, nonetheless. See
1616/// [`crate::NetworkManager`] for more context on the design pattern.
1617///
1618/// This should be spawned or used as part of `tokio::select!`.
1619//
1620// spawned in `NodeConfig::start_network`(reth_node_core::NodeConfig) and
1621// `NetworkConfig::start_network`(reth_network::NetworkConfig)
1622impl<
1623        Pool: TransactionPool + Unpin + 'static,
1624        N: NetworkPrimitives<
1625                BroadcastedTransaction: SignedTransaction,
1626                PooledTransaction: SignedTransaction,
1627            > + Unpin,
1628    > Future for TransactionsManager<Pool, N>
1629where
1630    Pool::Transaction:
1631        PoolTransaction<Consensus = N::BroadcastedTransaction, Pooled = N::PooledTransaction>,
1632{
1633    type Output = ();
1634
1635    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1636        let start = Instant::now();
1637        let mut poll_durations = TxManagerPollDurations::default();
1638
1639        let this = self.get_mut();
1640
1641        // All streams are polled until their corresponding budget is exhausted, then we manually
1642        // yield back control to tokio. See `NetworkManager` for more context on the design
1643        // pattern.
1644
1645        // Advance network/peer related events (update peers map).
1646        let maybe_more_network_events = metered_poll_nested_stream_with_budget!(
1647            poll_durations.acc_network_events,
1648            "net::tx",
1649            "Network events stream",
1650            DEFAULT_BUDGET_TRY_DRAIN_STREAM,
1651            this.network_events.poll_next_unpin(cx),
1652            |event| this.on_network_event(event)
1653        );
1654
1655        // Advance incoming transaction events (stream new txns/announcements from
1656        // network manager and queue for import to pool/fetch txns).
1657        //
1658        // This will potentially remove hashes from hashes pending fetch, it the event
1659        // is an announcement (if same hashes are announced that didn't fit into a
1660        // previous request).
1661        //
1662        // The smallest decodable transaction is an empty legacy transaction, 10 bytes
1663        // (128 KiB / 10 bytes > 13k transactions).
1664        //
1665        // If this is an event with `Transactions` message, since transactions aren't
1666        // validated until they are inserted into the pool, this can potentially queue
1667        // >13k transactions for insertion to pool. More if the message size is bigger
1668        // than the soft limit on a `Transactions` broadcast message, which is 128 KiB.
1669        let maybe_more_tx_events = metered_poll_nested_stream_with_budget!(
1670            poll_durations.acc_tx_events,
1671            "net::tx",
1672            "Network transaction events stream",
1673            DEFAULT_BUDGET_TRY_DRAIN_NETWORK_TRANSACTION_EVENTS,
1674            this.transaction_events.poll_next_unpin(cx),
1675            |event: NetworkTransactionEvent<N>| this.on_network_tx_event(event),
1676        );
1677
1678        // Advance inflight fetch requests (flush transaction fetcher and queue for
1679        // import to pool).
1680        //
1681        // The smallest decodable transaction is an empty legacy transaction, 10 bytes
1682        // (2 MiB / 10 bytes > 200k transactions).
1683        //
1684        // Since transactions aren't validated until they are inserted into the pool,
1685        // this can potentially queue >200k transactions for insertion to pool. More
1686        // if the message size is bigger than the soft limit on a `PooledTransactions`
1687        // response which is 2 MiB.
1688        let mut maybe_more_tx_fetch_events = metered_poll_nested_stream_with_budget!(
1689            poll_durations.acc_fetch_events,
1690            "net::tx",
1691            "Transaction fetch events stream",
1692            DEFAULT_BUDGET_TRY_DRAIN_STREAM,
1693            this.transaction_fetcher.poll_next_unpin(cx),
1694            |event| this.on_fetch_event(event),
1695        );
1696
1697        // Advance pool imports (flush txns to pool).
1698        //
1699        // Note, this is done in batches. A batch is filled from one `Transactions`
1700        // broadcast messages or one `PooledTransactions` response at a time. The
1701        // minimum batch size is 1 transaction (and might often be the case with blob
1702        // transactions).
1703        //
1704        // The smallest decodable transaction is an empty legacy transaction, 10 bytes
1705        // (2 MiB / 10 bytes > 200k transactions).
1706        //
1707        // Since transactions aren't validated until they are inserted into the pool,
1708        // this can potentially validate >200k transactions. More if the message size
1709        // is bigger than the soft limit on a `PooledTransactions` response which is
1710        // 2 MiB (`Transactions` broadcast messages is smaller, 128 KiB).
1711        let maybe_more_pool_imports = metered_poll_nested_stream_with_budget!(
1712            poll_durations.acc_pending_imports,
1713            "net::tx",
1714            "Batched pool imports stream",
1715            DEFAULT_BUDGET_TRY_DRAIN_PENDING_POOL_IMPORTS,
1716            this.pool_imports.poll_next_unpin(cx),
1717            |batch_results| this.on_batch_import_result(batch_results)
1718        );
1719
1720        // Advances new __pending__ transactions, transactions that were successfully inserted into
1721        // pending set in pool (are valid), and propagates them (inform peers which
1722        // transactions we have seen).
1723        //
1724        // This is polled after pool imports so transactions that became pending in this poll
1725        // iteration are propagated immediately, instead of waiting for the task to be woken
1726        // again.
1727        //
1728        // We try to drain this to batch the transactions in a single message.
1729        //
1730        // We don't expect this buffer to be large, since only pending transactions are
1731        // emitted here.
1732        let mut new_txs = Vec::new();
1733        let maybe_more_pending_txns = match this.pending_transactions.poll_recv_many(
1734            cx,
1735            &mut new_txs,
1736            SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE,
1737        ) {
1738            Poll::Ready(count) => {
1739                if count == SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE {
1740                    // we filled the entire buffer capacity and need to try again on the next poll
1741                    // immediately
1742                    true
1743                } else {
1744                    // try once more, because mostlikely the channel is now empty and the waker is
1745                    // registered if this is pending, if we filled additional hashes, we poll again
1746                    // on the next iteration
1747                    let limit =
1748                        SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE -
1749                            new_txs.len();
1750                    this.pending_transactions.poll_recv_many(cx, &mut new_txs, limit).is_ready()
1751                }
1752            }
1753            Poll::Pending => false,
1754        };
1755        if !new_txs.is_empty() {
1756            this.on_new_pending_transactions(new_txs);
1757        }
1758
1759        // Tries to drain hashes pending fetch cache if the tx manager currently has
1760        // capacity for this (fetch txns).
1761        //
1762        // Sends at most one request.
1763        duration_metered_exec!(
1764            {
1765                if this.has_capacity_for_fetching_pending_hashes() &&
1766                    this.on_fetch_hashes_pending_fetch()
1767                {
1768                    maybe_more_tx_fetch_events = true;
1769                }
1770            },
1771            poll_durations.acc_pending_fetch
1772        );
1773
1774        // Advance commands (propagate/fetch/serve txns).
1775        let maybe_more_commands = metered_poll_nested_stream_with_budget!(
1776            poll_durations.acc_cmds,
1777            "net::tx",
1778            "Commands channel",
1779            DEFAULT_BUDGET_TRY_DRAIN_STREAM,
1780            this.command_rx.poll_next_unpin(cx),
1781            |cmd| this.on_command(cmd)
1782        );
1783
1784        this.transaction_fetcher.update_metrics();
1785
1786        // all channels are fully drained and import futures pending
1787        if maybe_more_network_events ||
1788            maybe_more_commands ||
1789            maybe_more_tx_events ||
1790            maybe_more_tx_fetch_events ||
1791            maybe_more_pool_imports ||
1792            maybe_more_pending_txns
1793        {
1794            // make sure we're woken up again
1795            cx.waker().wake_by_ref();
1796            return Poll::Pending
1797        }
1798
1799        this.update_poll_metrics(start, poll_durations);
1800
1801        Poll::Pending
1802    }
1803}
1804
1805/// Represents the different modes of transaction propagation.
1806///
1807/// This enum is used to determine how transactions are propagated to peers in the network.
1808#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1809enum PropagationMode {
1810    /// Default propagation mode.
1811    ///
1812    /// Transactions are only sent to peers that haven't seen them yet.
1813    Basic,
1814    /// Forced propagation mode.
1815    ///
1816    /// Transactions are sent to all peers regardless of whether they have been sent or received
1817    /// before.
1818    Forced,
1819}
1820
1821impl PropagationMode {
1822    /// Returns `true` if the propagation kind is `Forced`.
1823    const fn is_forced(self) -> bool {
1824        matches!(self, Self::Forced)
1825    }
1826}
1827
1828/// A transaction that's about to be propagated to multiple peers.
1829#[derive(Debug, Clone)]
1830struct PropagateTransaction {
1831    is_broadcastable_in_full: bool,
1832    /// Size advertised in `NewPooledTransactionHashes` metadata and used for full broadcast
1833    /// soft-limit accounting.
1834    ///
1835    /// This is the network encoded transaction size. For pool-backed blob transactions, this is
1836    /// the pool's cached encoded length, which includes the sidecar returned by
1837    /// `PooledTransactions`.
1838    propagation_size: usize,
1839    transaction: LazyEncodedTransaction,
1840}
1841
1842impl PropagateTransaction {
1843    /// Create a new instance from a transaction supplied directly for propagation.
1844    ///
1845    /// Direct transactions use their EIP-2718 encoded length so eth/68+ hash announcements carry
1846    /// the same size metadata as [`NewPooledTransactionHashes68::push`] and
1847    /// [`NewPooledTransactionHashes72::push`].
1848    fn new<T: SignedTransaction>(transaction: T) -> Self {
1849        let is_broadcastable_in_full = transaction.is_broadcastable_in_full();
1850        let propagation_size = transaction.encode_2718_len();
1851
1852        Self {
1853            is_broadcastable_in_full,
1854            propagation_size,
1855            transaction: LazyEncoded::new(transaction),
1856        }
1857    }
1858
1859    /// Create a new instance from a pooled transaction.
1860    ///
1861    /// Pool transactions already cache the network encoded size used by txpool admission and
1862    /// pooled hash announcements. For blob transactions, this includes the sidecar size expected in
1863    /// a `PooledTransactions` response.
1864    fn pool_tx<P: PoolTransaction>(tx: Arc<ValidPoolTransaction<P>>) -> Self {
1865        let is_broadcastable_in_full = tx.transaction.consensus_ref().is_broadcastable_in_full();
1866        let propagation_size = tx.encoded_length();
1867        Self {
1868            is_broadcastable_in_full,
1869            propagation_size,
1870            transaction: LazyEncoded::new(PropagatePooledTransactionEncoder::new(tx)),
1871        }
1872    }
1873
1874    fn tx_hash(&self) -> &TxHash {
1875        self.transaction.tx_hash()
1876    }
1877
1878    /// Returns the network encoded size used for propagation limits and hash metadata.
1879    const fn propagation_size(&self) -> usize {
1880        self.propagation_size
1881    }
1882
1883    fn tx_type(&self) -> u8 {
1884        self.transaction.ty()
1885    }
1886
1887    const fn is_broadcastable_in_full(&self) -> bool {
1888        self.is_broadcastable_in_full
1889    }
1890
1891    fn shared(&self) -> LazyEncodedTransaction {
1892        self.transaction.clone()
1893    }
1894}
1895
1896/// A pooled transaction encoder that avoids cloning into the consensus transaction for propagation.
1897#[derive(Debug)]
1898struct PropagatePooledTransactionEncoder<P: PoolTransaction> {
1899    transaction: Arc<ValidPoolTransaction<P>>,
1900}
1901
1902impl<P: PoolTransaction> PropagatePooledTransactionEncoder<P> {
1903    const fn new(transaction: Arc<ValidPoolTransaction<P>>) -> Self {
1904        Self { transaction }
1905    }
1906
1907    fn encode_uncached(&self, out: &mut dyn BufMut) {
1908        (*self.transaction.transaction.consensus_ref().inner()).encode(out);
1909    }
1910}
1911
1912impl<P: PoolTransaction> Encodable for PropagatePooledTransactionEncoder<P> {
1913    fn encode(&self, out: &mut dyn BufMut) {
1914        self.encode_uncached(out);
1915    }
1916
1917    fn length(&self) -> usize {
1918        (*self.transaction.transaction.consensus_ref().inner()).length()
1919    }
1920}
1921
1922impl<P: PoolTransaction> TxHashRef for PropagatePooledTransactionEncoder<P> {
1923    fn tx_hash(&self) -> &TxHash {
1924        self.transaction.hash()
1925    }
1926}
1927
1928impl<P: PoolTransaction> Typed2718 for PropagatePooledTransactionEncoder<P> {
1929    fn ty(&self) -> u8 {
1930        self.transaction.transaction.ty()
1931    }
1932}
1933
1934/// Helper type to construct the appropriate message to send to the peer based on whether the peer
1935/// should receive them in full or as pooled
1936#[derive(Debug, Clone)]
1937enum PropagateTransactionsBuilder {
1938    Pooled(PooledTransactionsHashesBuilder),
1939    Full(FullTransactionsBuilder),
1940}
1941
1942impl PropagateTransactionsBuilder {
1943    /// Create a builder for pooled transactions with capacity for the expected number of
1944    /// transactions.
1945    fn pooled(version: EthVersion, capacity: usize) -> Self {
1946        Self::Pooled(PooledTransactionsHashesBuilder::with_capacity(version, capacity))
1947    }
1948
1949    /// Create a builder that sends transactions in full and records transactions that don't fit,
1950    /// with capacity for the expected number of transactions.
1951    fn full(version: EthVersion, capacity: usize) -> Self {
1952        Self::Full(FullTransactionsBuilder::with_capacity(version, capacity))
1953    }
1954
1955    /// Returns true if no transactions are recorded.
1956    fn is_empty(&self) -> bool {
1957        match self {
1958            Self::Pooled(builder) => builder.is_empty(),
1959            Self::Full(builder) => builder.is_empty(),
1960        }
1961    }
1962
1963    /// Consumes the type and returns the built messages that should be sent to the peer.
1964    fn build(self) -> PropagateTransactions {
1965        match self {
1966            Self::Pooled(pooled) => {
1967                PropagateTransactions { pooled: Some(pooled.build()), full: None }
1968            }
1969            Self::Full(full) => full.build(),
1970        }
1971    }
1972}
1973
1974impl PropagateTransactionsBuilder {
1975    /// Appends a transaction to the list.
1976    fn push(&mut self, transaction: &PropagateTransaction) {
1977        match self {
1978            Self::Pooled(builder) => builder.push(transaction),
1979            Self::Full(builder) => builder.push(transaction),
1980        }
1981    }
1982}
1983
1984/// Represents how the transactions should be sent to a peer if any.
1985struct PropagateTransactions {
1986    /// The pooled transaction hashes to send.
1987    pooled: Option<NewPooledTransactionHashes>,
1988    /// The transactions to send in full.
1989    full: Option<BroadcastPoolTransactions>,
1990}
1991
1992/// Helper type for constructing the full transaction message that enforces the
1993/// [`DEFAULT_SOFT_LIMIT_BYTE_SIZE_TRANSACTIONS_BROADCAST_MESSAGE`] for full transaction broadcast
1994/// and enforces other propagation rules for EIP-4844 and tracks those transactions that can't be
1995/// broadcasted in full.
1996#[derive(Debug, Clone)]
1997struct FullTransactionsBuilder {
1998    /// The soft limit to enforce for a single broadcast message of full transactions.
1999    total_size: usize,
2000    /// All transactions to be broadcasted.
2001    transactions: Vec<LazyEncodedTransaction>,
2002    /// Transactions that didn't fit into the broadcast message
2003    pooled: PooledTransactionsHashesBuilder,
2004}
2005
2006impl FullTransactionsBuilder {
2007    /// Create a builder for the negotiated version of the peer's session
2008    fn new(version: EthVersion) -> Self {
2009        Self {
2010            total_size: 0,
2011            pooled: PooledTransactionsHashesBuilder::new(version),
2012            transactions: vec![],
2013        }
2014    }
2015
2016    /// Create a builder with capacity for the expected number of full transactions.
2017    ///
2018    /// The overflow hashes builder remains lazily allocated since most transactions are expected
2019    /// to be broadcast in full.
2020    fn with_capacity(version: EthVersion, capacity: usize) -> Self {
2021        Self {
2022            total_size: 0,
2023            pooled: PooledTransactionsHashesBuilder::new(version),
2024            transactions: Vec::with_capacity(capacity),
2025        }
2026    }
2027
2028    /// Returns whether or not any transactions are in the [`FullTransactionsBuilder`].
2029    fn is_empty(&self) -> bool {
2030        self.transactions.is_empty() && self.pooled.is_empty()
2031    }
2032
2033    /// Returns the messages that should be propagated to the peer.
2034    fn build(self) -> PropagateTransactions {
2035        let pooled = Some(self.pooled.build()).filter(|pooled| !pooled.is_empty());
2036        let full =
2037            (!self.transactions.is_empty()).then_some(BroadcastPoolTransactions(self.transactions));
2038        PropagateTransactions { pooled, full }
2039    }
2040
2041    /// Appends all transactions.
2042    fn extend(&mut self, txs: impl IntoIterator<Item = PropagateTransaction>) {
2043        for tx in txs {
2044            self.push(&tx)
2045        }
2046    }
2047
2048    /// Append a transaction to the list of full transaction if the total message bytes size doesn't
2049    /// exceed the soft maximum target byte size. The limit is soft, meaning if one single
2050    /// transaction goes over the limit, it will be broadcasted in its own [`Transactions`]
2051    /// message. The same pattern is followed in filling a [`GetPooledTransactions`] request in
2052    /// [`TransactionFetcher::fill_request_from_hashes_pending_fetch`].
2053    ///
2054    /// If the transaction is unsuitable for broadcast or would exceed the softlimit, it is appended
2055    /// to list of pooled transactions, (e.g. 4844 transactions).
2056    /// See also [`SignedTransaction::is_broadcastable_in_full`].
2057    fn push(&mut self, transaction: &PropagateTransaction) {
2058        // Do not send full 4844 transaction hashes to peers.
2059        //
2060        //  Nodes MUST NOT automatically broadcast blob transactions to their peers.
2061        //  Instead, those transactions are only announced using
2062        //  `NewPooledTransactionHashes` messages, and can then be manually requested
2063        //  via `GetPooledTransactions`.
2064        //
2065        // From: <https://eips.ethereum.org/EIPS/eip-4844#networking>
2066        if !transaction.is_broadcastable_in_full() {
2067            self.pooled.push(transaction);
2068            return
2069        }
2070
2071        let new_size = self.total_size + transaction.propagation_size();
2072        if new_size > DEFAULT_SOFT_LIMIT_BYTE_SIZE_TRANSACTIONS_BROADCAST_MESSAGE &&
2073            self.total_size > 0
2074        {
2075            // transaction does not fit into the message
2076            self.pooled.push(transaction);
2077            return
2078        }
2079
2080        self.total_size = new_size;
2081        self.transactions.push(transaction.shared());
2082    }
2083}
2084
2085/// A helper type to create the pooled transactions message based on the negotiated version of the
2086/// session with the peer
2087#[derive(Debug, Clone)]
2088enum PooledTransactionsHashesBuilder {
2089    Eth66(NewPooledTransactionHashes66),
2090    Eth68(NewPooledTransactionHashes68),
2091    Eth72(NewPooledTransactionHashes72),
2092}
2093
2094// === impl PooledTransactionsHashesBuilder ===
2095
2096impl PooledTransactionsHashesBuilder {
2097    /// Push a transaction from the pool to the list.
2098    fn push_pooled<T: PoolTransaction>(&mut self, pooled_tx: Arc<ValidPoolTransaction<T>>) {
2099        match self {
2100            Self::Eth66(msg) => msg.push(*pooled_tx.hash()),
2101            Self::Eth68(msg) => {
2102                msg.hashes.push(*pooled_tx.hash());
2103                msg.sizes.push(pooled_tx.encoded_length());
2104                msg.types.push(pooled_tx.transaction.ty());
2105            }
2106            Self::Eth72(msg) => {
2107                msg.hashes.push(*pooled_tx.hash());
2108                msg.sizes.push(pooled_tx.encoded_length());
2109                let ty = pooled_tx.transaction.ty();
2110                msg.types.push(ty);
2111                if ty == EIP4844_TX_TYPE_ID {
2112                    // The pool holds the full sidecar, so every cell can be served.
2113                    msg.cell_mask = Some(NewPooledTransactionHashes72::ALL_CELLS_MASK);
2114                }
2115            }
2116        }
2117    }
2118
2119    /// Returns whether or not any transactions are in the [`PooledTransactionsHashesBuilder`].
2120    fn is_empty(&self) -> bool {
2121        match self {
2122            Self::Eth66(hashes) => hashes.is_empty(),
2123            Self::Eth68(hashes) => hashes.is_empty(),
2124            Self::Eth72(hashes) => hashes.is_empty(),
2125        }
2126    }
2127
2128    /// Returns the number of transactions in the builder.
2129    fn len(&self) -> usize {
2130        match self {
2131            Self::Eth66(hashes) => hashes.len(),
2132            Self::Eth68(hashes) => hashes.len(),
2133            Self::Eth72(hashes) => hashes.len(),
2134        }
2135    }
2136
2137    /// Appends all hashes
2138    fn extend(&mut self, txs: impl IntoIterator<Item = PropagateTransaction>) {
2139        for tx in txs {
2140            self.push(&tx);
2141        }
2142    }
2143
2144    fn push(&mut self, tx: &PropagateTransaction) {
2145        match self {
2146            Self::Eth66(msg) => msg.push(*tx.tx_hash()),
2147            Self::Eth68(msg) => {
2148                msg.hashes.push(*tx.tx_hash());
2149                msg.sizes.push(tx.propagation_size());
2150                msg.types.push(tx.tx_type());
2151            }
2152            Self::Eth72(msg) => {
2153                msg.hashes.push(*tx.tx_hash());
2154                msg.sizes.push(tx.propagation_size());
2155                let ty = tx.tx_type();
2156                msg.types.push(ty);
2157                if ty == EIP4844_TX_TYPE_ID {
2158                    // The pool holds the full sidecar, so every cell can be served.
2159                    msg.cell_mask = Some(NewPooledTransactionHashes72::ALL_CELLS_MASK);
2160                }
2161            }
2162        }
2163    }
2164
2165    /// Create a builder for the negotiated version of the peer's session
2166    fn new(version: EthVersion) -> Self {
2167        match version {
2168            EthVersion::Eth66 | EthVersion::Eth67 => Self::Eth66(Default::default()),
2169            EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 | EthVersion::Eth71 => {
2170                Self::Eth68(Default::default())
2171            }
2172            EthVersion::Eth72 => Self::Eth72(Default::default()),
2173        }
2174    }
2175
2176    /// Create a builder for the negotiated version of the peer's session with capacity for the
2177    /// expected number of hashes.
2178    fn with_capacity(version: EthVersion, capacity: usize) -> Self {
2179        match version {
2180            EthVersion::Eth66 | EthVersion::Eth67 => {
2181                Self::Eth66(NewPooledTransactionHashes66::with_capacity(capacity))
2182            }
2183            EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 | EthVersion::Eth71 => {
2184                Self::Eth68(NewPooledTransactionHashes68::with_capacity(capacity))
2185            }
2186            EthVersion::Eth72 => Self::Eth72(NewPooledTransactionHashes72::with_capacity(capacity)),
2187        }
2188    }
2189
2190    fn build(self) -> NewPooledTransactionHashes {
2191        match self {
2192            Self::Eth66(mut msg) => {
2193                msg.shrink_to_fit();
2194                msg.into()
2195            }
2196            Self::Eth68(mut msg) => {
2197                msg.shrink_to_fit();
2198                msg.into()
2199            }
2200            Self::Eth72(mut msg) => {
2201                msg.shrink_to_fit();
2202                msg.into()
2203            }
2204        }
2205    }
2206}
2207
2208/// How we received the transactions.
2209enum TransactionSource {
2210    /// Transactions were broadcast to us via [`Transactions`] message.
2211    Broadcast,
2212    /// Transactions were sent as the response of [`fetcher::GetPooledTxRequest`] issued by us.
2213    Response,
2214}
2215
2216// === impl TransactionSource ===
2217
2218impl TransactionSource {
2219    /// Whether the transaction were sent as broadcast.
2220    const fn is_broadcast(&self) -> bool {
2221        matches!(self, Self::Broadcast)
2222    }
2223}
2224
2225/// Tracks a single peer in the context of [`TransactionsManager`].
2226#[derive(Debug)]
2227pub struct PeerMetadata<N: NetworkPrimitives = EthNetworkPrimitives> {
2228    /// Optimistically keeps track of transactions that we know the peer has seen. Optimistic, in
2229    /// the sense that transactions are preemptively marked as seen by peer when they are sent to
2230    /// the peer.
2231    seen_transactions: LruCache<TxHash, FbBuildHasher<32>>,
2232    /// A communication channel directly to the peer's session task.
2233    request_tx: PeerRequestSender<PeerRequest<N>>,
2234    /// negotiated version of the session.
2235    version: EthVersion,
2236    /// The peer's client version.
2237    client_version: Arc<str>,
2238    /// The kind of peer.
2239    peer_kind: PeerKind,
2240}
2241
2242impl<N: NetworkPrimitives> PeerMetadata<N> {
2243    /// Returns a new instance of [`PeerMetadata`].
2244    pub fn new(
2245        request_tx: PeerRequestSender<PeerRequest<N>>,
2246        version: EthVersion,
2247        client_version: Arc<str>,
2248        max_transactions_seen_by_peer: u32,
2249        peer_kind: PeerKind,
2250    ) -> Self {
2251        Self {
2252            seen_transactions: LruCache::with_hasher(
2253                max_transactions_seen_by_peer,
2254                Default::default(),
2255            ),
2256            request_tx,
2257            version,
2258            client_version,
2259            peer_kind,
2260        }
2261    }
2262
2263    /// Returns a reference to the peer's request sender channel.
2264    pub const fn request_tx(&self) -> &PeerRequestSender<PeerRequest<N>> {
2265        &self.request_tx
2266    }
2267
2268    /// Returns a mutable reference to the seen transactions LRU cache.
2269    pub const fn seen_transactions_mut(&mut self) -> &mut LruCache<TxHash, FbBuildHasher<32>> {
2270        &mut self.seen_transactions
2271    }
2272
2273    /// Returns the negotiated `EthVersion` of the session.
2274    pub const fn version(&self) -> EthVersion {
2275        self.version
2276    }
2277
2278    /// Returns a reference to the peer's client version string.
2279    pub fn client_version(&self) -> &str {
2280        &self.client_version
2281    }
2282
2283    /// Returns the peer's kind.
2284    pub const fn peer_kind(&self) -> PeerKind {
2285        self.peer_kind
2286    }
2287}
2288
2289/// Commands to send to the [`TransactionsManager`]
2290#[derive(Debug)]
2291enum TransactionsCommand<N: NetworkPrimitives = EthNetworkPrimitives> {
2292    /// Propagate a transaction hash to the network.
2293    PropagateHash(B256),
2294    /// Propagate transaction hashes to a specific peer.
2295    PropagateHashesTo(Vec<B256>, PeerId),
2296    /// Request the list of active peer IDs from the [`TransactionsManager`].
2297    GetActivePeers(oneshot::Sender<HashSet<PeerId>>),
2298    /// Propagate a collection of full transactions to a specific peer.
2299    PropagateTransactionsTo(Vec<TxHash>, PeerId),
2300    /// Propagate a collection of hashes to all peers.
2301    PropagateTransactions(Vec<TxHash>),
2302    /// Propagate a collection of broadcastable transactions in full to all peers.
2303    BroadcastTransactions(Vec<PropagateTransaction>),
2304    /// Request transaction hashes known by specific peers from the [`TransactionsManager`].
2305    GetTransactionHashes { peers: Vec<PeerId>, tx: oneshot::Sender<HashMap<PeerId, B256Set>> },
2306    /// Requests a clone of the sender channel to the peer.
2307    GetPeerSender {
2308        peer_id: PeerId,
2309        peer_request_sender: oneshot::Sender<Option<PeerRequestSender<PeerRequest<N>>>>,
2310    },
2311}
2312
2313/// All events related to transactions emitted by the network.
2314#[derive(Debug)]
2315pub enum NetworkTransactionEvent<N: NetworkPrimitives = EthNetworkPrimitives> {
2316    /// Represents the event of receiving a list of transactions from a peer.
2317    ///
2318    /// This indicates transactions that were broadcasted to us from the peer.
2319    IncomingTransactions {
2320        /// The ID of the peer from which the transactions were received.
2321        peer_id: PeerId,
2322        /// The received transactions.
2323        msg: Transactions<N::BroadcastedTransaction>,
2324    },
2325    /// Represents the event of receiving a list of transaction hashes from a peer.
2326    IncomingPooledTransactionHashes {
2327        /// The ID of the peer from which the transaction hashes were received.
2328        peer_id: PeerId,
2329        /// The received new pooled transaction hashes.
2330        msg: NewPooledTransactionHashes,
2331    },
2332    /// Represents the event of receiving a `GetPooledTransactions` request from a peer.
2333    GetPooledTransactions {
2334        /// The ID of the peer from which the request was received.
2335        peer_id: PeerId,
2336        /// The received `GetPooledTransactions` request.
2337        request: GetPooledTransactions,
2338        /// The sender for responding to the request with a result of `PooledTransactions`.
2339        response: oneshot::Sender<RequestResult<PooledTransactions<N::PooledTransaction>>>,
2340    },
2341    /// Represents the event of receiving a `GetTransactionsHandle` request.
2342    GetTransactionsHandle(oneshot::Sender<Option<TransactionsHandle<N>>>),
2343}
2344
2345/// Tracks stats about the [`TransactionsManager`].
2346#[derive(Debug)]
2347pub struct PendingPoolImportsInfo {
2348    /// Number of transactions about to be inserted into the pool.
2349    pending_pool_imports: Arc<AtomicUsize>,
2350    /// Max number of transactions allowed to be imported concurrently.
2351    max_pending_pool_imports: usize,
2352}
2353
2354impl PendingPoolImportsInfo {
2355    /// Returns a new [`PendingPoolImportsInfo`].
2356    pub fn new(max_pending_pool_imports: usize) -> Self {
2357        Self { pending_pool_imports: Arc::new(AtomicUsize::default()), max_pending_pool_imports }
2358    }
2359
2360    /// Returns `true` if the number of pool imports is under a given tolerated max.
2361    pub fn has_capacity(&self, max_pending_pool_imports: usize) -> bool {
2362        self.pending_pool_imports.load(Ordering::Relaxed) < max_pending_pool_imports
2363    }
2364}
2365
2366impl Default for PendingPoolImportsInfo {
2367    fn default() -> Self {
2368        Self::new(DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS)
2369    }
2370}
2371
2372#[derive(Debug, Default)]
2373struct TxManagerPollDurations {
2374    acc_network_events: Duration,
2375    acc_pending_imports: Duration,
2376    acc_tx_events: Duration,
2377    acc_imported_txns: Duration,
2378    acc_fetch_events: Duration,
2379    acc_pending_fetch: Duration,
2380    acc_cmds: Duration,
2381}
2382
2383impl<N: NetworkPrimitives> InMemorySize for NetworkTransactionEvent<N> {
2384    // `N::BroadcastedTransaction` and `N::PooledTransaction` already implement
2385    // `InMemorySize` via `SignedTransaction: InMemorySize`, so no extra bound is needed.
2386    fn size(&self) -> usize {
2387        match self {
2388            Self::IncomingTransactions { peer_id, msg } => {
2389                core::mem::size_of_val(peer_id) +
2390                    msg.0.iter().map(InMemorySize::size).sum::<usize>()
2391            }
2392            Self::IncomingPooledTransactionHashes { peer_id, msg } => {
2393                core::mem::size_of_val(peer_id) + msg.size()
2394            }
2395            Self::GetPooledTransactions { peer_id, request, response } => {
2396                core::mem::size_of_val(peer_id) +
2397                    request.0.len() * core::mem::size_of::<TxHash>() +
2398                    core::mem::size_of_val(response)
2399            }
2400            Self::GetTransactionsHandle(_) => 0,
2401        }
2402    }
2403}
2404
2405#[cfg(test)]
2406mod tests {
2407    use super::*;
2408    use crate::{
2409        test_utils::{
2410            transactions::{buffer_hash_to_tx_fetcher, new_mock_session, new_tx_manager},
2411            Testnet,
2412        },
2413        transactions::config::RelaxedEthAnnouncementFilter,
2414        NetworkConfigBuilder, NetworkManager,
2415    };
2416    use alloy_consensus::{Transaction as _, TxEip1559, TxLegacy};
2417    use alloy_eips::{eip2718::Encodable2718, eip4844::BlobTransactionValidationError};
2418    use alloy_primitives::{hex, Signature, TxKind, B256, U256};
2419    use alloy_rlp::Decodable;
2420    use futures::FutureExt;
2421    use reth_chainspec::MIN_TRANSACTION_GAS;
2422    use reth_ethereum_primitives::{PooledTransactionVariant, Transaction, TransactionSigned};
2423    use reth_network_api::{NetworkInfo, PeerKind};
2424    use reth_network_p2p::{
2425        error::{RequestError, RequestResult},
2426        sync::{NetworkSyncUpdater, SyncState},
2427    };
2428    use reth_storage_api::noop::NoopProvider;
2429    use reth_tasks::Runtime;
2430    use reth_transaction_pool::{
2431        blobstore::InMemoryBlobStore,
2432        error::{Eip4844PoolTransactionError, InvalidPoolTransactionError, PoolError},
2433        identifier::SenderIdentifiers,
2434        test_utils::{
2435            testing_pool, MockTransaction, MockTransactionFactory, OkValidator, TestPool,
2436            TransactionGenerator,
2437        },
2438        CoinbaseTipOrdering, EthPooledTransaction, Pool, TransactionOrigin, ValidPoolTransaction,
2439    };
2440    use secp256k1::SecretKey;
2441    use std::{
2442        future::poll_fn,
2443        net::{IpAddr, Ipv4Addr, SocketAddr},
2444        str::FromStr,
2445        time::Instant,
2446    };
2447    use tracing::error;
2448
2449    type EthTestPool = Pool<
2450        OkValidator<EthPooledTransaction>,
2451        CoinbaseTipOrdering<EthPooledTransaction>,
2452        InMemoryBlobStore,
2453    >;
2454
2455    async fn new_eth_tx_manager() -> (
2456        TransactionsManager<EthTestPool, EthNetworkPrimitives>,
2457        NetworkManager<EthNetworkPrimitives>,
2458    ) {
2459        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
2460        let client = NoopProvider::default();
2461
2462        let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
2463            .listener_port(0)
2464            .disable_discovery()
2465            .build(client);
2466
2467        let pool = Pool::new(
2468            OkValidator::default(),
2469            CoinbaseTipOrdering::default(),
2470            InMemoryBlobStore::default(),
2471            Default::default(),
2472        );
2473
2474        let transactions_manager_config = config.transactions_manager_config.clone();
2475        let (_network_handle, network, transactions, _) = NetworkManager::new(config)
2476            .await
2477            .unwrap()
2478            .into_builder()
2479            .transactions(pool.clone(), transactions_manager_config)
2480            .split_with_handle();
2481
2482        (transactions, network)
2483    }
2484
2485    fn valid_eth_pool_transaction(
2486        transaction: EthPooledTransaction,
2487    ) -> Arc<ValidPoolTransaction<EthPooledTransaction>> {
2488        let mut ids = SenderIdentifiers::default();
2489        let transaction_id =
2490            ids.sender_id_or_create(transaction.sender()).into_transaction_id(transaction.nonce());
2491
2492        Arc::new(ValidPoolTransaction {
2493            propagate: false,
2494            transaction_id,
2495            transaction,
2496            timestamp: Instant::now(),
2497            origin: TransactionOrigin::External,
2498            authority_ids: None,
2499        })
2500    }
2501
2502    fn gen_eip1559_pooled_with_nonce<R: rand::RngCore>(
2503        tx_gen: &mut TransactionGenerator<R>,
2504        nonce: u64,
2505    ) -> EthPooledTransaction {
2506        EthPooledTransaction::try_from_consensus(
2507            tx_gen.transaction().nonce(nonce).into_eip1559().try_into_recovered().unwrap(),
2508        )
2509        .unwrap()
2510    }
2511
2512    #[tokio::test(flavor = "multi_thread")]
2513    async fn test_ignored_tx_broadcasts_while_initially_syncing() {
2514        reth_tracing::init_test_tracing();
2515        let net = Testnet::create(3).await;
2516
2517        let mut handles = net.handles();
2518        let handle0 = handles.next().unwrap();
2519        let handle1 = handles.next().unwrap();
2520
2521        drop(handles);
2522        let handle = net.spawn();
2523
2524        let listener0 = handle0.event_listener();
2525        handle0.add_peer(*handle1.peer_id(), handle1.local_addr());
2526        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
2527
2528        let client = NoopProvider::default();
2529        let pool = testing_pool();
2530        let config = NetworkConfigBuilder::eth(secret_key, Runtime::test())
2531            .disable_discovery()
2532            .listener_port(0)
2533            .build(client);
2534        let transactions_manager_config = config.transactions_manager_config.clone();
2535        let (network_handle, network, mut transactions, _) = NetworkManager::new(config)
2536            .await
2537            .unwrap()
2538            .into_builder()
2539            .transactions(pool.clone(), transactions_manager_config)
2540            .split_with_handle();
2541
2542        tokio::task::spawn(network);
2543
2544        // go to syncing (pipeline sync)
2545        network_handle.update_sync_state(SyncState::Syncing);
2546        assert!(NetworkInfo::is_syncing(&network_handle));
2547        assert!(NetworkInfo::is_initially_syncing(&network_handle));
2548
2549        // wait for all initiator connections
2550        let mut established = listener0.take(2);
2551        while let Some(ev) = established.next().await {
2552            match ev {
2553                NetworkEvent::Peer(PeerEvent::SessionEstablished(info)) => {
2554                    // to insert a new peer in transactions peerset
2555                    transactions
2556                        .on_network_event(NetworkEvent::Peer(PeerEvent::SessionEstablished(info)))
2557                }
2558                NetworkEvent::Peer(PeerEvent::PeerAdded(_peer_id)) => {}
2559                ev => {
2560                    error!("unexpected event {ev:?}")
2561                }
2562            }
2563        }
2564        // random tx: <https://etherscan.io/getRawTx?tx=0x9448608d36e721ef403c53b00546068a6474d6cbab6816c3926de449898e7bce>
2565        let input = hex!(
2566            "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76"
2567        );
2568        let signed_tx = TransactionSigned::decode(&mut &input[..]).unwrap();
2569        transactions.on_network_tx_event(NetworkTransactionEvent::IncomingTransactions {
2570            peer_id: *handle1.peer_id(),
2571            msg: Transactions(vec![signed_tx.clone()]),
2572        });
2573        poll_fn(|cx| {
2574            let _ = transactions.poll_unpin(cx);
2575            Poll::Ready(())
2576        })
2577        .await;
2578        assert!(pool.is_empty());
2579        handle.terminate().await;
2580    }
2581
2582    #[tokio::test(flavor = "multi_thread")]
2583    async fn test_tx_broadcasts_through_two_syncs() {
2584        reth_tracing::init_test_tracing();
2585        let net = Testnet::create(3).await;
2586
2587        let mut handles = net.handles();
2588        let handle0 = handles.next().unwrap();
2589        let handle1 = handles.next().unwrap();
2590
2591        drop(handles);
2592        let handle = net.spawn();
2593
2594        let listener0 = handle0.event_listener();
2595        handle0.add_peer(*handle1.peer_id(), handle1.local_addr());
2596        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
2597
2598        let client = NoopProvider::default();
2599        let pool = testing_pool();
2600        let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
2601            .disable_discovery()
2602            .listener_port(0)
2603            .build(client);
2604        let transactions_manager_config = config.transactions_manager_config.clone();
2605        let (network_handle, network, mut transactions, _) = NetworkManager::new(config)
2606            .await
2607            .unwrap()
2608            .into_builder()
2609            .transactions(pool.clone(), transactions_manager_config)
2610            .split_with_handle();
2611
2612        tokio::task::spawn(network);
2613
2614        // go to syncing (pipeline sync) to idle and then to syncing (live)
2615        network_handle.update_sync_state(SyncState::Syncing);
2616        assert!(NetworkInfo::is_syncing(&network_handle));
2617        network_handle.update_sync_state(SyncState::Idle);
2618        assert!(!NetworkInfo::is_syncing(&network_handle));
2619        network_handle.update_sync_state(SyncState::Syncing);
2620        assert!(NetworkInfo::is_syncing(&network_handle));
2621
2622        // wait for all initiator connections
2623        let mut established = listener0.take(2);
2624        while let Some(ev) = established.next().await {
2625            match ev {
2626                NetworkEvent::ActivePeerSession { .. } |
2627                NetworkEvent::Peer(PeerEvent::SessionEstablished(_)) => {
2628                    // to insert a new peer in transactions peerset
2629                    transactions.on_network_event(ev);
2630                }
2631                NetworkEvent::Peer(PeerEvent::PeerAdded(_peer_id)) => {}
2632                _ => {
2633                    error!("unexpected event {ev:?}")
2634                }
2635            }
2636        }
2637        // random tx: <https://etherscan.io/getRawTx?tx=0x9448608d36e721ef403c53b00546068a6474d6cbab6816c3926de449898e7bce>
2638        let input = hex!(
2639            "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76"
2640        );
2641        let signed_tx = TransactionSigned::decode(&mut &input[..]).unwrap();
2642        transactions.on_network_tx_event(NetworkTransactionEvent::IncomingTransactions {
2643            peer_id: *handle1.peer_id(),
2644            msg: Transactions(vec![signed_tx.clone()]),
2645        });
2646        poll_fn(|cx| {
2647            let _ = transactions.poll_unpin(cx);
2648            Poll::Ready(())
2649        })
2650        .await;
2651        assert!(!NetworkInfo::is_initially_syncing(&network_handle));
2652        assert!(NetworkInfo::is_syncing(&network_handle));
2653        assert!(!pool.is_empty());
2654        handle.terminate().await;
2655    }
2656
2657    // Ensure that the transaction manager correctly handles the `IncomingPooledTransactionHashes`
2658    // event and is able to retrieve the corresponding transactions.
2659    #[tokio::test(flavor = "multi_thread")]
2660    async fn test_handle_incoming_transactions_hashes() {
2661        reth_tracing::init_test_tracing();
2662
2663        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
2664        let client = NoopProvider::default();
2665
2666        let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
2667            // let OS choose port
2668            .listener_port(0)
2669            .disable_discovery()
2670            .build(client);
2671
2672        let pool = testing_pool();
2673
2674        let transactions_manager_config = config.transactions_manager_config.clone();
2675        let (_network_handle, _network, mut tx_manager, _) = NetworkManager::new(config)
2676            .await
2677            .unwrap()
2678            .into_builder()
2679            .transactions(pool.clone(), transactions_manager_config)
2680            .split_with_handle();
2681
2682        let peer_id_1 = PeerId::new([1; 64]);
2683        let eth_version = EthVersion::Eth66;
2684
2685        let txs = vec![TransactionSigned::new_unhashed(
2686            Transaction::Legacy(TxLegacy {
2687                chain_id: Some(4),
2688                nonce: 15u64,
2689                gas_price: 2200000000,
2690                gas_limit: 34811,
2691                to: TxKind::Call(hex!("cf7f9e66af820a19257a2108375b180b0ec49167").into()),
2692                value: U256::from(1234u64),
2693                input: Default::default(),
2694            }),
2695            Signature::new(
2696                U256::from_str(
2697                    "0x35b7bfeb9ad9ece2cbafaaf8e202e706b4cfaeb233f46198f00b44d4a566a981",
2698                )
2699                .unwrap(),
2700                U256::from_str(
2701                    "0x612638fb29427ca33b9a3be2a0a561beecfe0269655be160d35e72d366a6a860",
2702                )
2703                .unwrap(),
2704                true,
2705            ),
2706        )];
2707
2708        let txs_hashes: Vec<B256> = txs.iter().map(|tx| *tx.hash()).collect();
2709
2710        let (peer_1, mut to_mock_session_rx) = new_mock_session(peer_id_1, eth_version);
2711        tx_manager.peers.insert(peer_id_1, peer_1);
2712
2713        assert!(pool.is_empty());
2714
2715        tx_manager.on_network_tx_event(NetworkTransactionEvent::IncomingPooledTransactionHashes {
2716            peer_id: peer_id_1,
2717            msg: NewPooledTransactionHashes::from(NewPooledTransactionHashes66::from(
2718                txs_hashes.clone(),
2719            )),
2720        });
2721
2722        // mock session of peer_1 receives request
2723        let req = to_mock_session_rx
2724            .recv()
2725            .await
2726            .expect("peer_1 session should receive request with buffered hashes");
2727        let PeerRequest::GetPooledTransactions { request, response } = req else { unreachable!() };
2728        assert_eq!(request, GetPooledTransactions::from(txs_hashes.clone()));
2729
2730        let message: Vec<PooledTransactionVariant> = txs
2731            .into_iter()
2732            .map(|tx| {
2733                PooledTransactionVariant::try_from(tx)
2734                    .expect("Failed to convert MockTransaction to PooledTransaction")
2735            })
2736            .collect();
2737
2738        // return the transactions corresponding to the transaction hashes.
2739        response
2740            .send(Ok(PooledTransactions(message)))
2741            .expect("should send peer_1 response to tx manager");
2742
2743        // adance the transaction manager future
2744        poll_fn(|cx| {
2745            let _ = tx_manager.poll_unpin(cx);
2746            Poll::Ready(())
2747        })
2748        .await;
2749
2750        // ensure that the transactions corresponding to the transaction hashes have been
2751        // successfully retrieved and stored in the Pool.
2752        assert_eq!(pool.get_all(txs_hashes.clone()).len(), txs_hashes.len());
2753    }
2754
2755    #[tokio::test(flavor = "multi_thread")]
2756    async fn test_handle_incoming_transactions() {
2757        reth_tracing::init_test_tracing();
2758        let net = Testnet::create(3).await;
2759
2760        let mut handles = net.handles();
2761        let handle0 = handles.next().unwrap();
2762        let handle1 = handles.next().unwrap();
2763
2764        drop(handles);
2765        let handle = net.spawn();
2766
2767        let listener0 = handle0.event_listener();
2768
2769        handle0.add_peer(*handle1.peer_id(), handle1.local_addr());
2770        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
2771
2772        let client = NoopProvider::default();
2773        let pool = testing_pool();
2774        let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
2775            .disable_discovery()
2776            .listener_port(0)
2777            .build(client);
2778        let transactions_manager_config = config.transactions_manager_config.clone();
2779        let (network_handle, network, mut transactions, _) = NetworkManager::new(config)
2780            .await
2781            .unwrap()
2782            .into_builder()
2783            .transactions(pool.clone(), transactions_manager_config)
2784            .split_with_handle();
2785        tokio::task::spawn(network);
2786
2787        network_handle.update_sync_state(SyncState::Idle);
2788
2789        assert!(!NetworkInfo::is_syncing(&network_handle));
2790
2791        // wait for all initiator connections
2792        let mut established = listener0.take(2);
2793        while let Some(ev) = established.next().await {
2794            match ev {
2795                NetworkEvent::ActivePeerSession { .. } |
2796                NetworkEvent::Peer(PeerEvent::SessionEstablished(_)) => {
2797                    // to insert a new peer in transactions peerset
2798                    transactions.on_network_event(ev);
2799                }
2800                NetworkEvent::Peer(PeerEvent::PeerAdded(_peer_id)) => {}
2801                ev => {
2802                    error!("unexpected event {ev:?}")
2803                }
2804            }
2805        }
2806        // random tx: <https://etherscan.io/getRawTx?tx=0x9448608d36e721ef403c53b00546068a6474d6cbab6816c3926de449898e7bce>
2807        let input = hex!(
2808            "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76"
2809        );
2810        let signed_tx = TransactionSigned::decode(&mut &input[..]).unwrap();
2811        transactions.on_network_tx_event(NetworkTransactionEvent::IncomingTransactions {
2812            peer_id: *handle1.peer_id(),
2813            msg: Transactions(vec![signed_tx.clone()]),
2814        });
2815        assert!(transactions
2816            .transactions_by_peers
2817            .get(signed_tx.tx_hash())
2818            .unwrap()
2819            .contains(handle1.peer_id()));
2820
2821        // advance the transaction manager future
2822        poll_fn(|cx| {
2823            let _ = transactions.poll_unpin(cx);
2824            Poll::Ready(())
2825        })
2826        .await;
2827
2828        assert!(!pool.is_empty());
2829        assert!(pool.get(signed_tx.tx_hash()).is_some());
2830        handle.terminate().await;
2831    }
2832
2833    #[tokio::test(flavor = "multi_thread")]
2834    async fn test_session_closed_cleans_transaction_peer_state() {
2835        let (mut tx_manager, _network) = new_tx_manager().await;
2836        let peer_id = PeerId::new([1; 64]);
2837        let fallback_peer = PeerId::new([2; 64]);
2838        let (peer, _) = new_mock_session(peer_id, EthVersion::Eth66);
2839        let hash_shared = B256::from_slice(&[1; 32]);
2840
2841        tx_manager.peers.insert(peer_id, peer);
2842        buffer_hash_to_tx_fetcher(
2843            &mut tx_manager.transaction_fetcher,
2844            hash_shared,
2845            peer_id,
2846            0,
2847            None,
2848        );
2849        buffer_hash_to_tx_fetcher(
2850            &mut tx_manager.transaction_fetcher,
2851            hash_shared,
2852            fallback_peer,
2853            0,
2854            None,
2855        );
2856        tx_manager.transaction_fetcher.active_peers.insert(peer_id, 1);
2857
2858        tx_manager.on_network_event(NetworkEvent::Peer(PeerEvent::SessionClosed {
2859            peer_id,
2860            reason: None,
2861        }));
2862
2863        // peer removed from peers map and active_peers
2864        assert!(!tx_manager.peers.contains_key(&peer_id));
2865        assert!(tx_manager.transaction_fetcher.active_peers.peek(&peer_id).is_none());
2866        // fallback peer is still available for the hash
2867        assert_eq!(
2868            tx_manager.transaction_fetcher.get_idle_peer_for(hash_shared),
2869            Some(&fallback_peer)
2870        );
2871    }
2872
2873    #[tokio::test(flavor = "multi_thread")]
2874    async fn test_bad_blob_sidecar_not_cached_as_bad_import() {
2875        let (mut tx_manager, _network) = new_tx_manager().await;
2876        let peer_id = PeerId::new([1; 64]);
2877        let hash = B256::from_slice(&[1; 32]);
2878
2879        tx_manager.network.update_sync_state(SyncState::Idle);
2880        tx_manager.transactions_by_peers.insert(hash, smallvec::smallvec![peer_id]);
2881
2882        let err = PoolError::new(
2883            hash,
2884            InvalidPoolTransactionError::Eip4844(Eip4844PoolTransactionError::InvalidEip4844Blob(
2885                BlobTransactionValidationError::InvalidProof,
2886            )),
2887        );
2888
2889        tx_manager.on_bad_import(err);
2890
2891        assert!(!tx_manager.bad_imports.contains(&hash));
2892    }
2893
2894    #[tokio::test(flavor = "multi_thread")]
2895    async fn test_missing_blob_sidecar_not_cached_as_bad_import() {
2896        let (mut tx_manager, _network) = new_tx_manager().await;
2897        let peer_id = PeerId::new([1; 64]);
2898        let hash = B256::from_slice(&[3; 32]);
2899
2900        tx_manager.network.update_sync_state(SyncState::Idle);
2901        tx_manager.transactions_by_peers.insert(hash, smallvec::smallvec![peer_id]);
2902
2903        let err = PoolError::new(
2904            hash,
2905            InvalidPoolTransactionError::Eip4844(
2906                Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
2907            ),
2908        );
2909
2910        tx_manager.on_bad_import(err);
2911
2912        assert!(!tx_manager.bad_imports.contains(&hash));
2913    }
2914
2915    #[tokio::test(flavor = "multi_thread")]
2916    async fn test_non_blob_sidecar_error_still_cached_as_bad_import() {
2917        let (mut tx_manager, _network) = new_tx_manager().await;
2918        let peer_id = PeerId::new([1; 64]);
2919        let hash = B256::from_slice(&[2; 32]);
2920
2921        tx_manager.network.update_sync_state(SyncState::Idle);
2922        tx_manager.transactions_by_peers.insert(hash, smallvec::smallvec![peer_id]);
2923
2924        let err = PoolError::new(
2925            hash,
2926            InvalidPoolTransactionError::Eip4844(Eip4844PoolTransactionError::NoEip4844Blobs),
2927        );
2928
2929        tx_manager.on_bad_import(err);
2930
2931        assert!(tx_manager.bad_imports.contains(&hash));
2932    }
2933
2934    #[tokio::test(flavor = "multi_thread")]
2935    async fn test_on_get_pooled_transactions_network() {
2936        reth_tracing::init_test_tracing();
2937        let net = Testnet::create(2).await;
2938
2939        let mut handles = net.handles();
2940        let handle0 = handles.next().unwrap();
2941        let handle1 = handles.next().unwrap();
2942
2943        drop(handles);
2944        let handle = net.spawn();
2945
2946        let listener0 = handle0.event_listener();
2947
2948        handle0.add_peer(*handle1.peer_id(), handle1.local_addr());
2949        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
2950
2951        let client = NoopProvider::default();
2952        let pool = testing_pool();
2953        let config = NetworkConfigBuilder::new(secret_key, Runtime::test())
2954            .disable_discovery()
2955            .listener_port(0)
2956            .build(client);
2957        let transactions_manager_config = config.transactions_manager_config.clone();
2958        let (network_handle, network, mut transactions, _) = NetworkManager::new(config)
2959            .await
2960            .unwrap()
2961            .into_builder()
2962            .transactions(pool.clone(), transactions_manager_config)
2963            .split_with_handle();
2964        tokio::task::spawn(network);
2965
2966        network_handle.update_sync_state(SyncState::Idle);
2967
2968        assert!(!NetworkInfo::is_syncing(&network_handle));
2969
2970        // wait for all initiator connections
2971        let mut established = listener0.take(2);
2972        while let Some(ev) = established.next().await {
2973            match ev {
2974                NetworkEvent::ActivePeerSession { .. } |
2975                NetworkEvent::Peer(PeerEvent::SessionEstablished(_)) => {
2976                    transactions.on_network_event(ev);
2977                }
2978                NetworkEvent::Peer(PeerEvent::PeerAdded(_peer_id)) => {}
2979                ev => {
2980                    error!("unexpected event {ev:?}")
2981                }
2982            }
2983        }
2984        handle.terminate().await;
2985
2986        let tx = MockTransaction::eip1559();
2987        let _ = transactions
2988            .pool
2989            .add_transaction(reth_transaction_pool::TransactionOrigin::External, tx.clone())
2990            .await;
2991
2992        let request = GetPooledTransactions(vec![*tx.get_hash()]);
2993
2994        let (send, receive) =
2995            oneshot::channel::<RequestResult<PooledTransactions<PooledTransactionVariant>>>();
2996
2997        transactions.on_network_tx_event(NetworkTransactionEvent::GetPooledTransactions {
2998            peer_id: *handle1.peer_id(),
2999            request,
3000            response: send,
3001        });
3002
3003        match receive.await.unwrap() {
3004            Ok(PooledTransactions(transactions)) => {
3005                assert_eq!(transactions.len(), 1);
3006            }
3007            Err(e) => {
3008                panic!("error: {e:?}");
3009            }
3010        }
3011    }
3012
3013    // Ensure that when the remote peer only returns part of the requested transactions, the
3014    // replied transactions are removed from the `tx_fetcher`, while the unresponsive ones are
3015    // re-buffered.
3016    #[tokio::test]
3017    async fn test_partially_tx_response() {
3018        reth_tracing::init_test_tracing();
3019
3020        let mut tx_manager = new_tx_manager().await.0;
3021        let tx_fetcher = &mut tx_manager.transaction_fetcher;
3022
3023        let peer_id_1 = PeerId::new([1; 64]);
3024        let eth_version = EthVersion::Eth66;
3025
3026        let txs = vec![
3027            TransactionSigned::new_unhashed(
3028                Transaction::Legacy(TxLegacy {
3029                    chain_id: Some(4),
3030                    nonce: 15u64,
3031                    gas_price: 2200000000,
3032                    gas_limit: 34811,
3033                    to: TxKind::Call(hex!("cf7f9e66af820a19257a2108375b180b0ec49167").into()),
3034                    value: U256::from(1234u64),
3035                    input: Default::default(),
3036                }),
3037                Signature::new(
3038                    U256::from_str(
3039                        "0x35b7bfeb9ad9ece2cbafaaf8e202e706b4cfaeb233f46198f00b44d4a566a981",
3040                    )
3041                    .unwrap(),
3042                    U256::from_str(
3043                        "0x612638fb29427ca33b9a3be2a0a561beecfe0269655be160d35e72d366a6a860",
3044                    )
3045                    .unwrap(),
3046                    true,
3047                ),
3048            ),
3049            TransactionSigned::new_unhashed(
3050                Transaction::Eip1559(TxEip1559 {
3051                    chain_id: 4,
3052                    nonce: 26u64,
3053                    max_priority_fee_per_gas: 1500000000,
3054                    max_fee_per_gas: 1500000013,
3055                    gas_limit: MIN_TRANSACTION_GAS,
3056                    to: TxKind::Call(hex!("61815774383099e24810ab832a5b2a5425c154d5").into()),
3057                    value: U256::from(3000000000000000000u64),
3058                    input: Default::default(),
3059                    access_list: Default::default(),
3060                }),
3061                Signature::new(
3062                    U256::from_str(
3063                        "0x59e6b67f48fb32e7e570dfb11e042b5ad2e55e3ce3ce9cd989c7e06e07feeafd",
3064                    )
3065                    .unwrap(),
3066                    U256::from_str(
3067                        "0x016b83f4f980694ed2eee4d10667242b1f40dc406901b34125b008d334d47469",
3068                    )
3069                    .unwrap(),
3070                    true,
3071                ),
3072            ),
3073        ];
3074
3075        let txs_hashes: Vec<B256> = txs.iter().map(|tx| *tx.hash()).collect();
3076
3077        let (mut peer_1, mut to_mock_session_rx) = new_mock_session(peer_id_1, eth_version);
3078        // mark hashes as seen by peer so it can fish them out from the cache for hashes pending
3079        // fetch
3080        peer_1.seen_transactions.insert(txs_hashes[0]);
3081        peer_1.seen_transactions.insert(txs_hashes[1]);
3082        tx_manager.peers.insert(peer_id_1, peer_1);
3083
3084        buffer_hash_to_tx_fetcher(tx_fetcher, txs_hashes[0], peer_id_1, 0, None);
3085        buffer_hash_to_tx_fetcher(tx_fetcher, txs_hashes[1], peer_id_1, 0, None);
3086
3087        // peer_1 is idle
3088        assert!(tx_fetcher.is_idle(&peer_id_1));
3089        assert_eq!(tx_fetcher.active_peers.len(), 0);
3090
3091        // sends requests for buffered hashes to peer_1
3092        tx_fetcher.on_fetch_pending_hashes(&tx_manager.peers, |_| true);
3093
3094        assert_eq!(tx_fetcher.num_pending_hashes(), 0);
3095        // as long as request is in flight peer_1 is not idle
3096        assert!(!tx_fetcher.is_idle(&peer_id_1));
3097        assert_eq!(tx_fetcher.active_peers.len(), 1);
3098
3099        // mock session of peer_1 receives request
3100        let req = to_mock_session_rx
3101            .recv()
3102            .await
3103            .expect("peer_1 session should receive request with buffered hashes");
3104        let PeerRequest::GetPooledTransactions { response, .. } = req else { unreachable!() };
3105
3106        let message: Vec<PooledTransactionVariant> = txs
3107            .into_iter()
3108            .take(1)
3109            .map(|tx| {
3110                PooledTransactionVariant::try_from(tx)
3111                    .expect("Failed to convert MockTransaction to PooledTransaction")
3112            })
3113            .collect();
3114        // response partial request
3115        response
3116            .send(Ok(PooledTransactions(message)))
3117            .expect("should send peer_1 response to tx manager");
3118        let Some(FetchEvent::TransactionsFetched { peer_id, .. }) = tx_fetcher.next().await else {
3119            unreachable!()
3120        };
3121
3122        // request has resolved, peer_1 is idle again
3123        assert!(tx_fetcher.is_idle(&peer_id));
3124        assert_eq!(tx_fetcher.active_peers.len(), 0);
3125        // failing peer_1's request buffers requested hashes for retry.
3126        assert_eq!(tx_fetcher.num_pending_hashes(), 1);
3127    }
3128
3129    #[tokio::test]
3130    async fn test_max_retries_tx_request() {
3131        reth_tracing::init_test_tracing();
3132
3133        let mut tx_manager = new_tx_manager().await.0;
3134        let tx_fetcher = &mut tx_manager.transaction_fetcher;
3135
3136        let peer_id_1 = PeerId::new([1; 64]);
3137        let peer_id_2 = PeerId::new([2; 64]);
3138        let eth_version = EthVersion::Eth66;
3139        let seen_hashes = [B256::from_slice(&[1; 32]), B256::from_slice(&[2; 32])];
3140
3141        let (mut peer_1, mut to_mock_session_rx) = new_mock_session(peer_id_1, eth_version);
3142        // mark hashes as seen by peer so it can fish them out from the cache for hashes pending
3143        // fetch
3144        peer_1.seen_transactions.insert(seen_hashes[0]);
3145        peer_1.seen_transactions.insert(seen_hashes[1]);
3146        tx_manager.peers.insert(peer_id_1, peer_1);
3147
3148        // hashes are seen and currently not inflight, with one fallback peer, and are buffered
3149        // for first retry in reverse order to make index 0 lru
3150        let retries = 1;
3151        buffer_hash_to_tx_fetcher(tx_fetcher, seen_hashes[1], peer_id_1, retries, None);
3152        buffer_hash_to_tx_fetcher(tx_fetcher, seen_hashes[0], peer_id_1, retries, None);
3153
3154        // peer_1 is idle
3155        assert!(tx_fetcher.is_idle(&peer_id_1));
3156        assert_eq!(tx_fetcher.active_peers.len(), 0);
3157
3158        // sends request for buffered hashes to peer_1
3159        tx_fetcher.on_fetch_pending_hashes(&tx_manager.peers, |_| true);
3160
3161        let tx_fetcher = &mut tx_manager.transaction_fetcher;
3162
3163        assert_eq!(tx_fetcher.num_pending_hashes(), 0);
3164        // as long as request is in inflight peer_1 is not idle
3165        assert!(!tx_fetcher.is_idle(&peer_id_1));
3166        assert_eq!(tx_fetcher.active_peers.len(), 1);
3167
3168        // mock session of peer_1 receives request
3169        let req = to_mock_session_rx
3170            .recv()
3171            .await
3172            .expect("peer_1 session should receive request with buffered hashes");
3173        let PeerRequest::GetPooledTransactions { request, response } = req else { unreachable!() };
3174        let GetPooledTransactions(hashes) = request;
3175
3176        let hashes = hashes.into_iter().collect::<B256Set>();
3177
3178        assert_eq!(hashes, seen_hashes.into_iter().collect::<B256Set>());
3179
3180        // fail request to peer_1
3181        response
3182            .send(Err(RequestError::BadResponse))
3183            .expect("should send peer_1 response to tx manager");
3184        let Some(FetchEvent::FetchError { peer_id, .. }) = tx_fetcher.next().await else {
3185            unreachable!()
3186        };
3187
3188        // request has resolved, peer_1 is idle again
3189        assert!(tx_fetcher.is_idle(&peer_id));
3190        assert_eq!(tx_fetcher.active_peers.len(), 0);
3191        // failing peer_1's request buffers requested hashes for retry
3192        assert_eq!(tx_fetcher.num_pending_hashes(), 2);
3193
3194        let (peer_2, mut to_mock_session_rx) = new_mock_session(peer_id_2, eth_version);
3195        tx_manager.peers.insert(peer_id_2, peer_2);
3196
3197        // peer_2 announces same hashes as peer_1
3198        let msg =
3199            NewPooledTransactionHashes::Eth66(NewPooledTransactionHashes66(seen_hashes.to_vec()));
3200        tx_manager.on_new_pooled_transaction_hashes(peer_id_2, msg);
3201
3202        let tx_fetcher = &mut tx_manager.transaction_fetcher;
3203
3204        // peer_2 should be in active_peers.
3205        assert_eq!(tx_fetcher.active_peers.len(), 1);
3206
3207        // since hashes are already seen, no changes to length of unknown hashes
3208        assert_eq!(tx_fetcher.num_all_hashes(), 2);
3209        // but hashes are taken out of buffer and packed into request to peer_2
3210        assert_eq!(tx_fetcher.num_pending_hashes(), 0);
3211
3212        // mock session of peer_2 receives request
3213        let req = to_mock_session_rx
3214            .recv()
3215            .await
3216            .expect("peer_2 session should receive request with buffered hashes");
3217        let PeerRequest::GetPooledTransactions { response, .. } = req else { unreachable!() };
3218
3219        // report failed request to tx manager
3220        response
3221            .send(Err(RequestError::BadResponse))
3222            .expect("should send peer_2 response to tx manager");
3223        let Some(FetchEvent::FetchError { .. }) = tx_fetcher.next().await else { unreachable!() };
3224
3225        // `MAX_REQUEST_RETRIES_PER_TX_HASH`, 2, for hashes reached so this time won't be buffered
3226        // for retry
3227        assert_eq!(tx_fetcher.num_pending_hashes(), 0);
3228        assert_eq!(tx_fetcher.active_peers.len(), 0);
3229    }
3230
3231    #[test]
3232    fn test_direct_propagation_transaction_uses_2718_size() {
3233        let mut tx_gen = TransactionGenerator::new(rand::rng());
3234        let tx = tx_gen.gen_eip1559();
3235        let expected_size = tx.encode_2718_len();
3236
3237        let tx = PropagateTransaction::new(tx);
3238
3239        assert_eq!(tx.propagation_size(), expected_size);
3240    }
3241
3242    #[test]
3243    fn test_transaction_builder_empty() {
3244        let mut builder = PropagateTransactionsBuilder::pooled(EthVersion::Eth68, 0);
3245        assert!(builder.is_empty());
3246
3247        let mut tx_gen = TransactionGenerator::new(rand::rng());
3248        let tx =
3249            PropagateTransaction::pool_tx(valid_eth_pool_transaction(tx_gen.gen_eip1559_pooled()));
3250        builder.push(&tx);
3251        assert!(!builder.is_empty());
3252
3253        let txs = builder.build();
3254        assert!(txs.full.is_none());
3255        let txs = txs.pooled.unwrap();
3256        assert_eq!(txs.len(), 1);
3257    }
3258
3259    #[test]
3260    fn test_pooled_propagation_transaction_encoder_length_matches_network_encoding() {
3261        let mut tx_gen = TransactionGenerator::new(rand::rng());
3262        let tx = valid_eth_pool_transaction(tx_gen.gen_eip1559_pooled());
3263        let pooled = PropagatePooledTransactionEncoder::new(tx);
3264
3265        let mut pooled_encoded = Vec::new();
3266        pooled.encode(&mut pooled_encoded);
3267        assert_eq!(pooled.length(), pooled_encoded.len());
3268
3269        let broadcast = BroadcastPoolTransactions(vec![LazyEncoded::new(pooled)]);
3270        let mut first_encoded = Vec::new();
3271        broadcast.encode(&mut first_encoded);
3272        let mut second_encoded = Vec::new();
3273        broadcast.encode(&mut second_encoded);
3274        assert_eq!(first_encoded, second_encoded);
3275
3276        let mut encoded = first_encoded.as_slice();
3277        let decoded = Transactions::<TransactionSigned>::decode(&mut encoded).unwrap();
3278        assert_eq!(decoded.len(), 1);
3279        assert!(encoded.is_empty());
3280    }
3281
3282    #[test]
3283    fn test_transaction_builder_large() {
3284        let mut builder = PropagateTransactionsBuilder::full(EthVersion::Eth68, 0);
3285        assert!(builder.is_empty());
3286
3287        let mut tx_gen = TransactionGenerator::new(rand::rng());
3288        let mut tx = tx_gen.gen_eip1559_pooled();
3289        // create a transaction that still fits
3290        tx.encoded_length = DEFAULT_SOFT_LIMIT_BYTE_SIZE_TRANSACTIONS_BROADCAST_MESSAGE + 1;
3291        let tx = PropagateTransaction::pool_tx(valid_eth_pool_transaction(tx));
3292        builder.push(&tx);
3293        assert!(!builder.is_empty());
3294
3295        let txs = builder.clone().build();
3296        assert!(txs.pooled.is_none());
3297        let txs = txs.full.unwrap();
3298        assert_eq!(txs.len(), 1);
3299
3300        builder.push(&tx);
3301
3302        let txs = builder.clone().build();
3303        let pooled = txs.pooled.unwrap();
3304        assert_eq!(pooled.len(), 1);
3305        let txs = txs.full.unwrap();
3306        assert_eq!(txs.len(), 1);
3307    }
3308
3309    #[test]
3310    fn test_transaction_builder_eip4844() {
3311        let mut builder = PropagateTransactionsBuilder::full(EthVersion::Eth68, 0);
3312        assert!(builder.is_empty());
3313
3314        let mut tx_gen = TransactionGenerator::new(rand::rng());
3315        let tx =
3316            PropagateTransaction::pool_tx(valid_eth_pool_transaction(tx_gen.gen_eip4844_pooled()));
3317        builder.push(&tx);
3318        assert!(!builder.is_empty());
3319
3320        let txs = builder.clone().build();
3321        assert!(txs.full.is_none());
3322        let txs = txs.pooled.unwrap();
3323        assert_eq!(txs.len(), 1);
3324
3325        let tx =
3326            PropagateTransaction::pool_tx(valid_eth_pool_transaction(tx_gen.gen_eip1559_pooled()));
3327        builder.push(&tx);
3328
3329        let txs = builder.clone().build();
3330        let pooled = txs.pooled.unwrap();
3331        assert_eq!(pooled.len(), 1);
3332        let txs = txs.full.unwrap();
3333        assert_eq!(txs.len(), 1);
3334    }
3335
3336    #[tokio::test]
3337    async fn test_propagate_full() {
3338        reth_tracing::init_test_tracing();
3339
3340        let (mut tx_manager, network) = new_eth_tx_manager().await;
3341        let peer_id = PeerId::random();
3342
3343        // ensure not syncing
3344        network.handle().update_sync_state(SyncState::Idle);
3345
3346        // mock a peer
3347        let (tx, _rx) = mpsc::channel::<PeerRequest>(1);
3348
3349        let session_info = SessionInfo {
3350            peer_id,
3351            remote_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
3352            client_version: Arc::from(""),
3353            capabilities: Arc::new(vec![].into()),
3354            status: Arc::new(Default::default()),
3355            version: EthVersion::Eth68,
3356            peer_kind: PeerKind::Basic,
3357        };
3358        let messages: PeerRequestSender<PeerRequest> = PeerRequestSender::new(peer_id, tx);
3359        tx_manager
3360            .on_network_event(NetworkEvent::ActivePeerSession { info: session_info, messages });
3361        let mut propagate = vec![];
3362        let mut tx_gen = TransactionGenerator::new(rand::rng());
3363        let eip1559_tx = valid_eth_pool_transaction(tx_gen.gen_eip1559_pooled());
3364        propagate.push(eip1559_tx.clone());
3365        let eip4844_tx = valid_eth_pool_transaction(tx_gen.gen_eip4844_pooled());
3366        propagate.push(eip4844_tx.clone());
3367
3368        let propagated = tx_manager.propagate_transactions(
3369            propagate.clone().into_iter().map(PropagateTransaction::pool_tx).collect(),
3370            PropagationMode::Basic,
3371        );
3372        assert_eq!(propagated.len(), 2);
3373        let prop_txs = propagated.get(eip1559_tx.transaction.hash()).unwrap();
3374        assert_eq!(prop_txs.len(), 1);
3375        assert!(prop_txs[0].is_full());
3376
3377        let prop_txs = propagated.get(eip4844_tx.transaction.hash()).unwrap();
3378        assert_eq!(prop_txs.len(), 1);
3379        assert!(prop_txs[0].is_hash());
3380
3381        let peer = tx_manager.peers.get(&peer_id).unwrap();
3382        assert!(peer.seen_transactions.contains(eip1559_tx.transaction.hash()));
3383        assert!(peer.seen_transactions.contains(eip1559_tx.transaction.hash()));
3384        peer.seen_transactions.contains(eip4844_tx.transaction.hash());
3385
3386        // propagate again
3387        let propagated = tx_manager.propagate_transactions(
3388            propagate.into_iter().map(PropagateTransaction::pool_tx).collect(),
3389            PropagationMode::Basic,
3390        );
3391        assert!(propagated.is_empty());
3392    }
3393
3394    #[test]
3395    fn test_eth72_hashes_builder_sets_cell_mask_for_blob_txs() {
3396        let mut tx_gen = TransactionGenerator::new(rand::rng());
3397
3398        // no blob transactions: the mask stays unset and encodes as a zero mask
3399        let mut builder = PooledTransactionsHashesBuilder::new(EthVersion::Eth72);
3400        builder.push(&PropagateTransaction::pool_tx(valid_eth_pool_transaction(
3401            tx_gen.gen_eip1559_pooled(),
3402        )));
3403        let msg = builder.build();
3404        assert_eq!(msg.as_eth72().unwrap().cell_mask, None);
3405
3406        // announcing a blob transaction advertises every cell as available
3407        let mut builder = PooledTransactionsHashesBuilder::new(EthVersion::Eth72);
3408        builder.push(&PropagateTransaction::pool_tx(valid_eth_pool_transaction(
3409            tx_gen.gen_eip1559_pooled(),
3410        )));
3411        builder.push_pooled(valid_eth_pool_transaction(tx_gen.gen_eip4844_pooled()));
3412        let msg = builder.build();
3413        assert_eq!(
3414            msg.as_eth72().unwrap().cell_mask,
3415            Some(NewPooledTransactionHashes72::ALL_CELLS_MASK)
3416        );
3417    }
3418
3419    #[tokio::test]
3420    async fn test_truncated_hash_announcement_not_marked_seen() {
3421        reth_tracing::init_test_tracing();
3422
3423        let (mut tx_manager, network) = new_eth_tx_manager().await;
3424        // all peers receive hash announcements only
3425        tx_manager.config.propagation_mode = TransactionPropagationMode::Max(0);
3426
3427        // ensure not syncing
3428        network.handle().update_sync_state(SyncState::Idle);
3429
3430        let peer_id = PeerId::random();
3431        let (peer, _rx) = new_mock_session(peer_id, EthVersion::Eth68);
3432        tx_manager.peers.insert(peer_id, peer);
3433
3434        // one more transaction than fits into a single hashes broadcast message
3435        let mut tx_gen = TransactionGenerator::new(rand::rng());
3436        let txs = (0..=SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE)
3437            .map(|nonce| {
3438                valid_eth_pool_transaction(gen_eip1559_pooled_with_nonce(&mut tx_gen, nonce as u64))
3439            })
3440            .collect::<Vec<_>>();
3441        let last_sent = *txs[txs.len() - 2].hash();
3442        let truncated = *txs[txs.len() - 1].hash();
3443
3444        let propagated = tx_manager.propagate_transactions(
3445            txs.into_iter().map(PropagateTransaction::pool_tx).collect(),
3446            PropagationMode::Basic,
3447        );
3448
3449        // the truncated hash was not sent, so it must not be tracked as seen by the peer
3450        assert!(propagated.get(&truncated).is_none());
3451        let peer = tx_manager.peers.get(&peer_id).unwrap();
3452        assert!(!peer.seen_transactions.contains(&truncated));
3453        assert!(peer.seen_transactions.contains(&last_sent));
3454    }
3455
3456    #[tokio::test]
3457    async fn test_propagate_pending_txs_while_initially_syncing() {
3458        reth_tracing::init_test_tracing();
3459
3460        let (mut tx_manager, network) = new_eth_tx_manager().await;
3461        let peer_id = PeerId::random();
3462
3463        // Keep the node in initial sync mode.
3464        network.handle().update_sync_state(SyncState::Syncing);
3465        assert!(NetworkInfo::is_initially_syncing(&network.handle()));
3466
3467        // Add a peer so propagation has a destination.
3468        let (peer, _rx) = new_mock_session(peer_id, EthVersion::Eth68);
3469        tx_manager.peers.insert(peer_id, peer);
3470
3471        let mut tx_gen = TransactionGenerator::new(rand::rng());
3472        let tx = gen_eip1559_pooled_with_nonce(&mut tx_gen, 0);
3473        let tx_hash = *tx.hash();
3474        tx_manager
3475            .pool
3476            .add_transaction(reth_transaction_pool::TransactionOrigin::External, tx.clone())
3477            .await
3478            .expect("transaction should be accepted into the pool");
3479
3480        tx_manager.on_new_pending_transactions(vec![tx_hash]);
3481
3482        let peer = tx_manager.peers.get(&peer_id).expect("peer should exist");
3483        assert!(peer.seen_transactions.contains(&tx_hash));
3484    }
3485
3486    #[tokio::test]
3487    async fn test_relaxed_filter_ignores_unknown_tx_types() {
3488        reth_tracing::init_test_tracing();
3489
3490        let transactions_manager_config = TransactionsManagerConfig::default();
3491
3492        let propagation_policy = TransactionPropagationKind::default();
3493        let announcement_policy = RelaxedEthAnnouncementFilter::default();
3494
3495        let policy_bundle = NetworkPolicies::new(propagation_policy, announcement_policy);
3496
3497        let pool = testing_pool();
3498        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
3499        let client = NoopProvider::default();
3500
3501        let network_config = NetworkConfigBuilder::new(secret_key, Runtime::test())
3502            .listener_port(0)
3503            .disable_discovery()
3504            .build(client.clone());
3505
3506        let mut network_manager = NetworkManager::new(network_config).await.unwrap();
3507        let (to_tx_manager_tx, from_network_rx) =
3508            reth_metrics::common::mpsc::memory_bounded_channel::<
3509                NetworkTransactionEvent<EthNetworkPrimitives>,
3510            >(
3511                crate::transactions::constants::tx_manager::DEFAULT_TX_MANAGER_CHANNEL_MEMORY_LIMIT_BYTES,
3512                "test_tx_channel",
3513            );
3514        network_manager.set_transactions(to_tx_manager_tx);
3515        let network_handle = network_manager.handle().clone();
3516        let network_service_handle = tokio::spawn(network_manager);
3517
3518        let mut tx_manager = TransactionsManager::<TestPool, EthNetworkPrimitives>::with_policy(
3519            network_handle.clone(),
3520            pool.clone(),
3521            from_network_rx,
3522            transactions_manager_config,
3523            policy_bundle,
3524        );
3525
3526        let peer_id = PeerId::random();
3527        let eth_version = EthVersion::Eth68;
3528        let (mock_peer_metadata, mut mock_session_rx) = new_mock_session(peer_id, eth_version);
3529        tx_manager.peers.insert(peer_id, mock_peer_metadata);
3530
3531        let mut tx_factory = MockTransactionFactory::default();
3532
3533        let valid_known_tx = tx_factory.create_eip1559();
3534        let known_tx_signed: Arc<ValidPoolTransaction<MockTransaction>> = Arc::new(valid_known_tx);
3535
3536        let known_tx_hash = *known_tx_signed.hash();
3537        let known_tx_type_byte = known_tx_signed.transaction.tx_type();
3538        let known_tx_size = known_tx_signed.encoded_length();
3539
3540        let unknown_tx_hash = B256::random();
3541        let unknown_tx_type_byte = 0xff_u8;
3542        let unknown_tx_size = 150;
3543
3544        let announcement_msg = NewPooledTransactionHashes::Eth68(NewPooledTransactionHashes68 {
3545            types: vec![known_tx_type_byte, unknown_tx_type_byte],
3546            sizes: vec![known_tx_size, unknown_tx_size],
3547            hashes: vec![known_tx_hash, unknown_tx_hash],
3548        });
3549
3550        tx_manager.on_new_pooled_transaction_hashes(peer_id, announcement_msg);
3551
3552        poll_fn(|cx| {
3553            let _ = tx_manager.poll_unpin(cx);
3554            Poll::Ready(())
3555        })
3556        .await;
3557
3558        let mut requested_hashes_in_getpooled = B256Set::default();
3559        let mut unexpected_request_received = false;
3560
3561        match tokio::time::timeout(std::time::Duration::from_millis(200), mock_session_rx.recv())
3562            .await
3563        {
3564            Ok(Some(PeerRequest::GetPooledTransactions { request, response: tx_response_ch })) => {
3565                let GetPooledTransactions(hashes) = request;
3566                for hash in hashes {
3567                    requested_hashes_in_getpooled.insert(hash);
3568                }
3569                let _ = tx_response_ch.send(Ok(PooledTransactions(vec![])));
3570            }
3571            Ok(Some(other_request)) => {
3572                tracing::error!(?other_request, "Received unexpected PeerRequest type");
3573                unexpected_request_received = true;
3574            }
3575            Ok(None) => tracing::info!("Mock session channel closed or no request received."),
3576            Err(_timeout_err) => {
3577                tracing::info!("Timeout: No GetPooledTransactions request received.")
3578            }
3579        }
3580
3581        assert!(
3582            requested_hashes_in_getpooled.contains(&known_tx_hash),
3583            "Should have requested the known EIP-1559 transaction. Requested: {requested_hashes_in_getpooled:?}"
3584        );
3585        assert!(
3586            !requested_hashes_in_getpooled.contains(&unknown_tx_hash),
3587            "Should NOT have requested the unknown transaction type. Requested: {requested_hashes_in_getpooled:?}"
3588        );
3589        assert!(
3590            !unexpected_request_received,
3591            "An unexpected P2P request was received by the mock peer."
3592        );
3593
3594        network_service_handle.abort();
3595    }
3596}