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