Skip to main content

reth_network/session/
active.rs

1//! Represents an established session.
2
3use core::sync::atomic::Ordering;
4use std::{
5    collections::VecDeque,
6    future::Future,
7    net::SocketAddr,
8    pin::Pin,
9    sync::{
10        atomic::{AtomicU64, AtomicUsize},
11        Arc,
12    },
13    task::{ready, Context, Poll},
14    time::{Duration, Instant},
15};
16
17use crate::{
18    message::{NewBlockMessage, PeerMessage, PeerResponse, PeerResponseResult},
19    session::{
20        conn::EthRlpxConnection,
21        handle::{ActiveSessionMessage, SessionCommand},
22        BlockRangeInfo, EthVersion, SessionId,
23    },
24};
25use alloy_eips::merge::EPOCH_SLOTS;
26use alloy_primitives::Sealable;
27use futures::{stream::Fuse, SinkExt, StreamExt};
28use metrics::{Counter, Gauge};
29use reth_eth_wire::{
30    errors::{EthHandshakeError, EthStreamError},
31    message::{EthBroadcastMessage, MessageError},
32    Capabilities, DisconnectP2P, DisconnectReason, EthMessage, EthSnapMessage, NetworkPrimitives,
33    NewBlockPayload,
34};
35use reth_eth_wire_types::{
36    message::RequestPair, snap::SnapProtocolMessage, NewPooledTransactionHashes,
37    RawCapabilityMessage,
38};
39use reth_metrics::common::mpsc::MeteredPollSender;
40use reth_network_api::{PeerRequest, RequestMessage};
41use reth_network_p2p::{error::RequestError, snap::client::SnapResponse};
42use reth_network_peers::PeerId;
43use reth_network_types::session::config::INITIAL_REQUEST_TIMEOUT;
44use reth_primitives_traits::Block;
45use rustc_hash::FxHashMap;
46use tokio::{
47    sync::{mpsc, mpsc::error::TrySendError, oneshot},
48    time::Interval,
49};
50use tokio_stream::wrappers::ReceiverStream;
51use tokio_util::sync::PollSender;
52use tracing::{debug, trace};
53
54/// The recommended interval at which to check if a new range update should be sent to the remote
55/// peer.
56///
57/// Updates are only sent when the block height has advanced by at least one epoch (32 blocks)
58/// since the last update. The interval is set to one epoch duration in seconds.
59pub(super) const RANGE_UPDATE_INTERVAL: Duration = Duration::from_secs(EPOCH_SLOTS * 12);
60
61// Constants for timeout updating.
62
63/// Minimum timeout value
64const MINIMUM_TIMEOUT: Duration = Duration::from_secs(2);
65
66/// Maximum timeout value
67const MAXIMUM_TIMEOUT: Duration = INITIAL_REQUEST_TIMEOUT;
68/// How much the new measurements affect the current timeout (X percent)
69const SAMPLE_IMPACT: f64 = 0.1;
70/// Amount of RTTs before timeout
71const TIMEOUT_SCALING: u32 = 3;
72
73/// Restricts the number of queued outgoing messages for larger responses:
74///  - Block Bodies
75///  - Receipts
76///  - Headers
77///  - `PooledTransactions`
78///
79/// With proper softlimits in place (2MB) this targets 10MB (4+1 * 2MB) of outgoing response data.
80///
81/// This parameter serves as backpressure for reading additional requests from the remote.
82/// Once we've queued up more responses than this, the session should prioritize message flushing
83/// before reading any more messages from the remote peer, throttling the peer.
84const MAX_QUEUED_OUTGOING_RESPONSES: usize = 4;
85
86/// Capacity above which the drained outgoing message queue is shrunk back to its steady-state
87/// size, see [`QueuedOutgoingMessages::shrink_to_fit`].
88const SHRINK_CAPACITY_THRESHOLD: usize = 64;
89
90/// Maximum number of messages read from the connection per session poll before the task yields
91/// back to the scheduler, see the receive loop in the session's `Future` impl.
92///
93/// Message decoding is CPU intensive, so the budget bounds how long a single busy session can
94/// occupy the executor thread. Small tx gossip messages dominate under load and are cheap to
95/// decode individually, so the budget is sized such that their per-poll fixed costs (draining
96/// command channels, advancing the sink, flushing the transport) amortize over a larger batch.
97const RECEIVE_MESSAGE_BUDGET: usize = 16;
98
99/// Soft limit for the total number of buffered outgoing broadcast items (e.g. transaction hashes).
100///
101/// Many small broadcast messages carrying a single tx hash each are equivalent in cost to one
102/// message carrying many hashes. This limit counts individual items (hashes, transactions, blocks)
103/// rather than messages, so that many small messages don't trigger aggressive drops unnecessarily.
104const MAX_QUEUED_BROADCAST_ITEMS: usize = 4096;
105
106/// Shared counter for in-flight broadcast items (tx hashes, transactions, blocks) across the
107/// bounded command channel, unbounded overflow channel, and session outgoing queue.
108///
109/// Wrapped in a newtype so the backing storage can be changed later (e.g. to track memory) without
110/// touching every call-site.
111#[derive(Debug, Clone)]
112pub(crate) struct BroadcastItemCounter(Arc<AtomicUsize>);
113
114impl BroadcastItemCounter {
115    /// Creates a new counter starting at zero.
116    pub(crate) fn new() -> Self {
117        Self(Arc::new(AtomicUsize::new(0)))
118    }
119
120    /// Returns the current count.
121    pub(crate) fn get(&self) -> usize {
122        self.0.load(Ordering::Relaxed)
123    }
124
125    /// Attempts to add `n` items. Returns `true` if under the limit, `false` if over (no change).
126    pub(crate) fn try_add(&self, n: usize) -> bool {
127        let prev = self.0.fetch_add(n, Ordering::Relaxed);
128        if prev >= MAX_QUEUED_BROADCAST_ITEMS {
129            self.0.fetch_sub(n, Ordering::Relaxed);
130            false
131        } else {
132            true
133        }
134    }
135
136    /// Subtracts `n` items from the counter.
137    pub(crate) fn sub(&self, n: usize) {
138        self.0.fetch_sub(n, Ordering::Relaxed);
139    }
140}
141
142/// The type that advances an established session by listening for incoming messages (from local
143/// node or read from connection) and emitting events back to the
144/// [`SessionManager`](super::SessionManager).
145///
146/// It listens for
147///    - incoming commands from the [`SessionManager`](super::SessionManager)
148///    - incoming _internal_ requests/broadcasts via the request/command channel
149///    - incoming requests/broadcasts _from remote_ via the connection
150///    - responses for handled ETH requests received from the remote peer.
151#[expect(dead_code)]
152pub(crate) struct ActiveSession<N: NetworkPrimitives> {
153    /// Keeps track of request ids.
154    pub(crate) next_id: u64,
155    /// The underlying connection.
156    pub(crate) conn: EthRlpxConnection<N>,
157    /// Identifier of the node we're connected to.
158    pub(crate) remote_peer_id: PeerId,
159    /// The address we're connected to.
160    pub(crate) remote_addr: SocketAddr,
161    /// All capabilities the peer announced
162    pub(crate) remote_capabilities: Arc<Capabilities>,
163    /// Internal identifier of this session
164    pub(crate) session_id: SessionId,
165    /// Incoming commands from the manager
166    pub(crate) commands_rx: ReceiverStream<SessionCommand<N>>,
167    /// Unbounded channel for commands that couldn't fit in the bounded channel (broadcast
168    /// overflow) and for disconnect commands that must never be dropped.
169    pub(crate) unbounded_rx: mpsc::UnboundedReceiver<SessionCommand<N>>,
170    /// Counter for broadcast messages received via the unbounded overflow channel.
171    pub(crate) unbounded_broadcast_msgs: Counter,
172    /// Sink to send messages to the [`SessionManager`](super::SessionManager).
173    pub(crate) to_session_manager: MeteredPollSender<ActiveSessionMessage<N>>,
174    /// A message that needs to be delivered to the session manager
175    pub(crate) pending_message_to_session: Option<ActiveSessionMessage<N>>,
176    /// Incoming internal requests which are delegated to the remote peer.
177    pub(crate) internal_request_rx: Fuse<ReceiverStream<PeerRequest<N>>>,
178    /// All requests sent to the remote peer we're waiting on a response for, including `snap/2`
179    /// requests ([`PeerRequest::GetSnap`]).
180    pub(crate) inflight_requests: FxHashMap<u64, InflightRequest<PeerRequest<N>>>,
181    /// All requests that were sent by the remote peer and we're waiting on an internal response
182    pub(crate) received_requests_from_remote: Vec<ReceivedRequest<N>>,
183    /// Buffered messages that should be handled and sent to the peer.
184    pub(crate) queued_outgoing: QueuedOutgoingMessages<N>,
185    /// The maximum time we wait for a response from a peer.
186    pub(crate) internal_request_timeout: Arc<AtomicU64>,
187    /// Interval when to check for timed out requests.
188    pub(crate) internal_request_timeout_interval: Interval,
189    /// If an [`ActiveSession`] does not receive a response at all within this duration then it is
190    /// considered a protocol violation and the session will initiate a drop.
191    pub(crate) protocol_breach_request_timeout: Duration,
192    /// Used to reserve a slot to guarantee that the termination message is delivered
193    pub(crate) terminate_message:
194        Option<(PollSender<ActiveSessionMessage<N>>, ActiveSessionMessage<N>)>,
195    /// The eth69 range info for the remote peer.
196    /// This is `None` for peers negotiating versions below `eth/69`.
197    pub(crate) range_info: Option<BlockRangeInfo>,
198    /// The eth69 range info for the local node (this node).
199    /// This represents the range of blocks that this node can serve to other peers.
200    pub(crate) local_range_info: BlockRangeInfo,
201    /// Optional interval for sending periodic range updates to the remote peer (eth69+)
202    /// The interval is set to one epoch duration (~6.4 minutes), but updates are only sent when
203    /// the block height has advanced by at least one epoch (32 blocks) since the last update
204    pub(crate) range_update_interval: Option<Interval>,
205    /// The last latest block number we sent in a range update
206    /// Used to avoid sending unnecessary updates when block height hasn't changed significantly
207    pub(crate) last_sent_latest_block: Option<u64>,
208}
209
210impl<N: NetworkPrimitives> ActiveSession<N> {
211    /// Returns `true` if the session is currently in the process of disconnecting
212    fn is_disconnecting(&self) -> bool {
213        self.conn.inner().is_disconnecting()
214    }
215
216    /// Returns the next request id
217    const fn next_id(&mut self) -> u64 {
218        let id = self.next_id;
219        self.next_id += 1;
220        id
221    }
222
223    /// Shrinks the capacity of the outgoing message queue once it is drained.
224    ///
225    /// The buffered incoming requests need no shrinking: the receive loop stops reading from the
226    /// wire while more than [`MAX_QUEUED_OUTGOING_RESPONSES`] of them are pending, which keeps
227    /// that buffer's capacity small.
228    pub fn shrink_to_fit(&mut self) {
229        self.queued_outgoing.shrink_to_fit();
230    }
231
232    /// Drains messages queued for sending into the connection's sink as long as the connection
233    /// can accept more, without flushing the underlying transport.
234    ///
235    /// This always advances the sink at least once, even with nothing queued, so connection
236    /// keepalive (ping) timers embedded in the sink's readiness logic are polled every session
237    /// poll.
238    ///
239    /// Returns `true` if at least one message was handed to the connection.
240    fn poll_send_queued(&mut self, cx: &mut Context<'_>) -> Result<bool, EthStreamError> {
241        let mut progress = false;
242        while self.conn.poll_ready_unpin(cx).is_ready() {
243            let Some(msg) = self.queued_outgoing.pop_front() else { break };
244            progress = true;
245            let res = match msg {
246                OutgoingMessage::Snap(msg) => self.conn.start_send_snap(msg),
247                OutgoingMessage::Eth(msg) => self.conn.start_send_unpin(msg),
248                OutgoingMessage::Broadcast(msg) => self.conn.start_send_broadcast(msg),
249                OutgoingMessage::Raw(msg) => self.conn.start_send_raw(msg),
250            };
251            res?;
252        }
253        Ok(progress)
254    }
255
256    /// Handle a message read from the connection.
257    ///
258    /// Returns an error if the message is considered to be in violation of the protocol.
259    fn on_incoming_message(&mut self, msg: EthMessage<N>) -> OnIncomingMessageOutcome<N> {
260        /// A macro that handles an incoming request
261        /// This creates a new channel and tries to send the sender half to the session while
262        /// storing the receiver half internally so the pending response can be polled.
263        macro_rules! on_request {
264            ($req:ident, $resp_item:ident, $req_item:ident) => {{
265                let RequestPair { request_id, message: request } = $req;
266                let (tx, response) = oneshot::channel();
267                let received = ReceivedRequest {
268                    request_id,
269                    rx: PeerResponse::$resp_item { response },
270                    received: Instant::now(),
271                };
272                self.received_requests_from_remote.push(received);
273                self.try_emit_request(PeerMessage::EthRequest(PeerRequest::$req_item {
274                    request,
275                    response: tx,
276                }))
277                .into()
278            }};
279        }
280
281        /// Processes a response received from the peer
282        macro_rules! on_response {
283            ($resp:ident, $item:ident) => {{
284                let RequestPair { request_id, message } = $resp;
285                if let Some(req) = self.inflight_requests.remove(&request_id) {
286                    match req.request {
287                        RequestState::Waiting(PeerRequest::$item { response, .. }) => {
288                            trace!(peer_id=?self.remote_peer_id, ?request_id, "received response from peer");
289                            let _ = response.send(Ok(message));
290                            self.update_request_timeout(req.timestamp, Instant::now());
291                        }
292                        RequestState::Waiting(request) => {
293                            request.send_bad_response();
294                        }
295                        RequestState::TimedOut => {
296                            // request was already timed out internally
297                            self.update_request_timeout(req.timestamp, Instant::now());
298                        }
299                    }
300                } else {
301                    trace!(peer_id=?self.remote_peer_id, ?request_id, "received response to unknown request");
302                    // we received a response to a request we never sent
303                    self.on_bad_message();
304                }
305
306                OnIncomingMessageOutcome::Ok
307            }};
308        }
309
310        match msg {
311            message @ EthMessage::Status(_) => OnIncomingMessageOutcome::BadMessage {
312                error: EthStreamError::EthHandshakeError(EthHandshakeError::StatusNotInHandshake),
313                message,
314            },
315            EthMessage::NewBlockHashes(msg) => {
316                self.try_emit_broadcast(PeerMessage::NewBlockHashes(msg)).into()
317            }
318            EthMessage::NewBlock(msg) => {
319                let block = NewBlockMessage {
320                    hash: msg.block().header().hash_slow(),
321                    block: Arc::new(*msg),
322                };
323                self.try_emit_broadcast(PeerMessage::NewBlock(block)).into()
324            }
325            EthMessage::Transactions(msg) => {
326                self.try_emit_broadcast(PeerMessage::ReceivedTransaction(msg)).into()
327            }
328            EthMessage::NewPooledTransactionHashes66(msg) => {
329                self.try_emit_broadcast(PeerMessage::PooledTransactions(msg.into())).into()
330            }
331            EthMessage::NewPooledTransactionHashes68(msg) => {
332                self.try_emit_broadcast(PeerMessage::PooledTransactions(msg.into())).into()
333            }
334            EthMessage::NewPooledTransactionHashes72(msg) => {
335                self.try_emit_broadcast(PeerMessage::PooledTransactions(msg.into())).into()
336            }
337            EthMessage::GetBlockHeaders(req) => {
338                on_request!(req, BlockHeaders, GetBlockHeaders)
339            }
340            EthMessage::BlockHeaders(resp) => {
341                on_response!(resp, GetBlockHeaders)
342            }
343            EthMessage::GetBlockBodies(req) => {
344                on_request!(req, BlockBodies, GetBlockBodies)
345            }
346            EthMessage::BlockBodies(resp) => {
347                on_response!(resp, GetBlockBodies)
348            }
349            EthMessage::GetPooledTransactions(req) => {
350                on_request!(req, PooledTransactions, GetPooledTransactions)
351            }
352            EthMessage::PooledTransactions(resp) => {
353                on_response!(resp, GetPooledTransactions)
354            }
355            EthMessage::GetNodeData(req) => {
356                on_request!(req, NodeData, GetNodeData)
357            }
358            EthMessage::NodeData(resp) => {
359                on_response!(resp, GetNodeData)
360            }
361            EthMessage::GetReceipts(req) => {
362                if self.conn.version() >= EthVersion::Eth69 {
363                    on_request!(req, Receipts69, GetReceipts69)
364                } else {
365                    on_request!(req, Receipts, GetReceipts)
366                }
367            }
368            EthMessage::GetReceipts70(req) => {
369                on_request!(req, Receipts70, GetReceipts70)
370            }
371            EthMessage::Receipts(resp) => {
372                on_response!(resp, GetReceipts)
373            }
374            EthMessage::Receipts69(resp) => {
375                on_response!(resp, GetReceipts69)
376            }
377            EthMessage::Receipts70(resp) => {
378                on_response!(resp, GetReceipts70)
379            }
380            EthMessage::GetBlockAccessLists(req) => {
381                on_request!(req, BlockAccessLists, GetBlockAccessLists)
382            }
383            EthMessage::BlockAccessLists(resp) => {
384                on_response!(resp, GetBlockAccessLists)
385            }
386            EthMessage::Cells(resp) => {
387                on_response!(resp, GetCells)
388            }
389            EthMessage::BlockRangeUpdate(msg) => {
390                // Validate that earliest <= latest according to the spec
391                if msg.earliest > msg.latest {
392                    return OnIncomingMessageOutcome::BadMessage {
393                        error: EthStreamError::InvalidMessage(MessageError::Other(format!(
394                            "invalid block range: earliest ({}) > latest ({})",
395                            msg.earliest, msg.latest
396                        ))),
397                        message: EthMessage::BlockRangeUpdate(msg),
398                    };
399                }
400
401                // Validate that the latest hash is not zero
402                if msg.latest_hash.is_zero() {
403                    return OnIncomingMessageOutcome::BadMessage {
404                        error: EthStreamError::InvalidMessage(MessageError::Other(
405                            "invalid block range: latest_hash cannot be zero".to_string(),
406                        )),
407                        message: EthMessage::BlockRangeUpdate(msg),
408                    };
409                }
410
411                if let Some(range_info) = self.range_info.as_ref() {
412                    range_info.update(msg.earliest, msg.latest, msg.latest_hash);
413                }
414
415                OnIncomingMessageOutcome::Ok
416            }
417            EthMessage::GetCells(resp) => {
418                on_request!(resp, Cells, GetCells)
419            }
420            EthMessage::Other(bytes) => self.try_emit_broadcast(PeerMessage::Other(bytes)).into(),
421        }
422    }
423
424    /// Handles an inbound `snap/2` message.
425    ///
426    /// Responses are correlated to the in-flight [`PeerRequest::GetSnap`] by `request_id` (shared
427    /// with eth requests in [`Self::inflight_requests`]) and type-checked against the originally
428    /// sent request kind; unsolicited or mismatched ones count as bad messages. Inbound requests
429    /// are routed upward as [`PeerRequest::GetSnap`], same as any other eth request.
430    fn on_incoming_snap_message(
431        &mut self,
432        mut msg: SnapProtocolMessage,
433    ) -> OnIncomingMessageOutcome<N> {
434        let request_id = msg.request_id();
435        if !msg.is_response() {
436            let (tx, response) = oneshot::channel();
437            self.received_requests_from_remote.push(ReceivedRequest {
438                request_id,
439                rx: PeerResponse::Snap { response },
440                received: Instant::now(),
441            });
442            return self
443                .try_emit_request(PeerMessage::EthRequest(PeerRequest::GetSnap {
444                    request: msg,
445                    response: tx,
446                }))
447                .into()
448        }
449
450        let Some(req) = self.inflight_requests.remove(&request_id) else {
451            trace!(target: "net::session", ?request_id, remote_peer_id=?self.remote_peer_id, "received snap response to unknown request");
452            self.on_bad_message();
453            return OnIncomingMessageOutcome::Ok
454        };
455
456        match req.request {
457            RequestState::Waiting(PeerRequest::GetSnap { request, response }) => {
458                if Some(msg.message_id()) != request.message_id().response() {
459                    debug!(target: "net::session", ?request_id, msg_id=?msg.message_id(), remote_peer_id=?self.remote_peer_id, "received snap response of wrong type");
460                    self.on_bad_message();
461                    let _ = response.send(Err(RequestError::BadResponse));
462                    return OnIncomingMessageOutcome::Ok
463                }
464                // Restore the caller's original request id, not the wire-assigned one.
465                msg.set_request_id(request.request_id());
466                match SnapResponse::try_from(msg) {
467                    Ok(snap_response) => {
468                        trace!(target: "net::session", ?request_id, remote_peer_id=?self.remote_peer_id, "received snap response from peer");
469                        let _ = response.send(Ok(snap_response));
470                        self.update_request_timeout(req.timestamp, Instant::now());
471                    }
472                    Err(_) => {
473                        let _ = response.send(Err(RequestError::BadResponse));
474                    }
475                }
476            }
477            RequestState::Waiting(request) => {
478                // A different PeerRequest kind was pending for this id.
479                request.send_bad_response();
480            }
481            RequestState::TimedOut => {
482                self.update_request_timeout(req.timestamp, Instant::now());
483            }
484        }
485        OnIncomingMessageOutcome::Ok
486    }
487
488    /// Handle an internal peer request that will be sent to the remote.
489    fn on_internal_peer_request(&mut self, request: PeerRequest<N>, deadline: Instant) {
490        let version = self.conn.version();
491        if !Self::is_request_supported_for_version(&request, version) {
492            debug!(
493                target: "net",
494                ?request,
495                peer_id=?self.remote_peer_id,
496                ?version,
497                "Request not supported for negotiated eth version",
498            );
499            request.send_err_response(RequestError::UnsupportedCapability);
500            return;
501        }
502
503        // `GetSnap` isn't covered by the eth-version check above, and a connection that never
504        // negotiated `snap/2` can't send one without erroring the whole session.
505        if matches!(request, PeerRequest::GetSnap { .. }) && !self.conn.supports_snap() {
506            request.send_err_response(RequestError::UnsupportedCapability);
507            return;
508        }
509
510        let request_id = self.next_id();
511        trace!(?request, peer_id=?self.remote_peer_id, ?request_id, "sending request to peer");
512        let msg = match request.create_request_message(request_id) {
513            RequestMessage::Eth(msg) => msg.map_versioned(version).into(),
514            RequestMessage::Snap(msg) => OutgoingMessage::Snap(msg),
515        };
516
517        self.queued_outgoing.push_back(msg);
518        let req = InflightRequest {
519            request: RequestState::Waiting(request),
520            timestamp: Instant::now(),
521            deadline,
522        };
523        self.inflight_requests.insert(request_id, req);
524    }
525
526    #[inline]
527    fn is_request_supported_for_version(request: &PeerRequest<N>, version: EthVersion) -> bool {
528        request.is_supported_by_eth_version(version)
529    }
530
531    /// Handle a message received from the internal network
532    fn on_internal_peer_message(&mut self, msg: PeerMessage<N>) {
533        match msg {
534            PeerMessage::NewBlockHashes(msg) => {
535                self.queued_outgoing.push_back(EthMessage::NewBlockHashes(msg).into());
536            }
537            PeerMessage::NewBlock(msg) => {
538                self.queued_outgoing.push_back(EthBroadcastMessage::NewBlock(msg.block).into());
539            }
540            PeerMessage::PooledTransactions(msg) => {
541                if msg.is_valid_for_version(self.conn.version()) {
542                    self.queued_outgoing.push_pooled_hashes(msg);
543                } else {
544                    self.queued_outgoing.broadcast_items.sub(msg.len());
545                    debug!(target: "net", ?msg,  version=?self.conn.version(), "Message is invalid for connection version, skipping");
546                }
547            }
548            PeerMessage::EthRequest(req) => {
549                let deadline = self.request_deadline();
550                self.on_internal_peer_request(req, deadline);
551            }
552            PeerMessage::SendTransactions(msg) => {
553                self.queued_outgoing.push_back(EthBroadcastMessage::Transactions(msg).into());
554            }
555            PeerMessage::SendBroadcastPoolTransactions(msg) => {
556                self.queued_outgoing
557                    .push_back(EthBroadcastMessage::BroadcastPoolTransactions(msg).into());
558            }
559            PeerMessage::BlockRangeUpdated(_) => {}
560            PeerMessage::ReceivedTransaction(_) => {
561                unreachable!("Not emitted by network")
562            }
563            PeerMessage::Other(other) => {
564                self.queued_outgoing.push_back(OutgoingMessage::Raw(other));
565            }
566        }
567    }
568
569    /// Returns the deadline timestamp at which the request times out
570    fn request_deadline(&self) -> Instant {
571        Instant::now() +
572            Duration::from_millis(self.internal_request_timeout.load(Ordering::Relaxed))
573    }
574
575    /// Handle a Response to the peer
576    ///
577    /// This will queue the response to be sent to the peer
578    fn handle_outgoing_response(&mut self, id: u64, resp: PeerResponseResult<N>) {
579        match resp.try_into_message(id) {
580            Ok(RequestMessage::Eth(msg)) => {
581                self.queued_outgoing.push_back(msg.into());
582            }
583            Ok(RequestMessage::Snap(msg)) => {
584                self.queued_outgoing.push_back(OutgoingMessage::Snap(msg));
585            }
586            Err(err) => {
587                debug!(target: "net", %err, "Failed to respond to received request");
588            }
589        }
590    }
591
592    /// Send a message back to the [`SessionManager`](super::SessionManager).
593    ///
594    /// Returns the message if the bounded channel is currently unable to handle this message.
595    #[expect(clippy::result_large_err)]
596    fn try_emit_broadcast(&self, message: PeerMessage<N>) -> Result<(), ActiveSessionMessage<N>> {
597        let Some(sender) = self.to_session_manager.inner().get_ref() else { return Ok(()) };
598
599        match sender
600            .try_send(ActiveSessionMessage::ValidMessage { peer_id: self.remote_peer_id, message })
601        {
602            Ok(_) => Ok(()),
603            Err(err) => {
604                trace!(
605                    target: "net",
606                    %err,
607                    "no capacity for incoming broadcast",
608                );
609                match err {
610                    TrySendError::Full(msg) => Err(msg),
611                    TrySendError::Closed(_) => Ok(()),
612                }
613            }
614        }
615    }
616
617    /// Send a message back to the [`SessionManager`](super::SessionManager)
618    /// covering both broadcasts and incoming requests.
619    ///
620    /// Returns the message if the bounded channel is currently unable to handle this message.
621    #[expect(clippy::result_large_err)]
622    fn try_emit_request(&self, message: PeerMessage<N>) -> Result<(), ActiveSessionMessage<N>> {
623        let Some(sender) = self.to_session_manager.inner().get_ref() else { return Ok(()) };
624
625        match sender
626            .try_send(ActiveSessionMessage::ValidMessage { peer_id: self.remote_peer_id, message })
627        {
628            Ok(_) => Ok(()),
629            Err(err) => {
630                trace!(
631                    target: "net",
632                    %err,
633                    "no capacity for incoming request",
634                );
635                match err {
636                    TrySendError::Full(msg) => Err(msg),
637                    TrySendError::Closed(_) => {
638                        // Note: this would mean the `SessionManager` was dropped, which is already
639                        // handled by checking if the command receiver channel has been closed.
640                        Ok(())
641                    }
642                }
643            }
644        }
645    }
646
647    /// Notify the manager that the peer sent a bad message
648    fn on_bad_message(&self) {
649        let Some(sender) = self.to_session_manager.inner().get_ref() else { return };
650        let _ = sender.try_send(ActiveSessionMessage::BadMessage { peer_id: self.remote_peer_id });
651    }
652
653    /// Report back that this session has been closed.
654    fn emit_disconnect(&mut self, cx: &mut Context<'_>) -> Poll<()> {
655        trace!(target: "net::session", remote_peer_id=?self.remote_peer_id, "emitting disconnect");
656        let msg = ActiveSessionMessage::Disconnected {
657            peer_id: self.remote_peer_id,
658            remote_addr: self.remote_addr,
659        };
660
661        self.terminate_message = Some((self.to_session_manager.inner().clone(), msg));
662        self.poll_terminate_message(cx).expect("message is set")
663    }
664
665    /// Report back that this session has been closed due to an error
666    fn close_on_error(&mut self, error: EthStreamError, cx: &mut Context<'_>) -> Poll<()> {
667        let msg = ActiveSessionMessage::ClosedOnConnectionError {
668            peer_id: self.remote_peer_id,
669            remote_addr: self.remote_addr,
670            error,
671        };
672        self.terminate_message = Some((self.to_session_manager.inner().clone(), msg));
673        self.poll_terminate_message(cx).expect("message is set")
674    }
675
676    /// Starts the disconnect process
677    fn start_disconnect(&mut self, reason: DisconnectReason) -> Result<(), EthStreamError> {
678        Ok(self.conn.inner_mut().start_disconnect(reason)?)
679    }
680
681    /// Flushes the disconnect message and emits the corresponding message
682    fn poll_disconnect(&mut self, cx: &mut Context<'_>) -> Poll<()> {
683        debug_assert!(self.is_disconnecting(), "not disconnecting");
684
685        // try to close the flush out the remaining Disconnect message
686        let _ = ready!(self.conn.poll_close_unpin(cx));
687        self.emit_disconnect(cx)
688    }
689
690    /// Attempts to disconnect by sending the given disconnect reason
691    fn try_disconnect(&mut self, reason: DisconnectReason, cx: &mut Context<'_>) -> Poll<()> {
692        match self.start_disconnect(reason) {
693            Ok(()) => {
694                // we're done
695                self.poll_disconnect(cx)
696            }
697            Err(err) => {
698                debug!(target: "net::session", %err, remote_peer_id=?self.remote_peer_id, "could not send disconnect");
699                self.close_on_error(err, cx)
700            }
701        }
702    }
703
704    /// Checks for _internally_ timed out requests.
705    ///
706    /// If a requests misses its deadline, then it is timed out internally.
707    /// If a request misses the `protocol_breach_request_timeout` then this session is considered in
708    /// protocol violation and will close.
709    ///
710    /// Returns `true` if a peer missed the `protocol_breach_request_timeout`, in which case the
711    /// session should be terminated.
712    #[must_use]
713    fn check_timed_out_requests(&mut self, now: Instant) -> bool {
714        for (id, req) in &mut self.inflight_requests {
715            if req.is_timed_out(now) {
716                if req.is_waiting() {
717                    debug!(target: "net::session", ?id, remote_peer_id=?self.remote_peer_id, "timed out outgoing request");
718                    req.timeout();
719                } else if now - req.timestamp > self.protocol_breach_request_timeout {
720                    return true
721                }
722            }
723        }
724
725        false
726    }
727
728    /// Updates the request timeout with a request's timestamps
729    fn update_request_timeout(&mut self, sent: Instant, received: Instant) {
730        let elapsed = received.saturating_duration_since(sent);
731
732        let current = Duration::from_millis(self.internal_request_timeout.load(Ordering::Relaxed));
733        let request_timeout = calculate_new_timeout(current, elapsed);
734        self.internal_request_timeout.store(request_timeout.as_millis() as u64, Ordering::Relaxed);
735        self.internal_request_timeout_interval = request_timeout_interval(request_timeout);
736    }
737
738    /// If a termination message is queued this will try to send it
739    fn poll_terminate_message(&mut self, cx: &mut Context<'_>) -> Option<Poll<()>> {
740        let (mut tx, msg) = self.terminate_message.take()?;
741        match tx.poll_reserve(cx) {
742            Poll::Pending => {
743                self.terminate_message = Some((tx, msg));
744                return Some(Poll::Pending)
745            }
746            Poll::Ready(Ok(())) => {
747                let _ = tx.send_item(msg);
748            }
749            Poll::Ready(Err(_)) => {
750                // channel closed
751            }
752        }
753        // terminate the task
754        Some(Poll::Ready(()))
755    }
756}
757
758impl<N: NetworkPrimitives> Future for ActiveSession<N> {
759    type Output = ();
760
761    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
762        let this = self.get_mut();
763
764        // if the session is terminate we have to send the termination message before we can close
765        if let Some(terminate) = this.poll_terminate_message(cx) {
766            return terminate
767        }
768
769        if this.is_disconnecting() {
770            return this.poll_disconnect(cx)
771        }
772
773        // The receive loop can be CPU intensive since it involves message decoding which could take
774        // up a lot of resources and increase latencies for other sessions if not yielded manually.
775        // If the budget is exhausted we manually yield back control to the (coop) scheduler. This
776        // manual yield point should prevent situations where polling appears to be frozen. See also <https://tokio.rs/blog/2020-04-preemption>
777        // And tokio's docs on cooperative scheduling <https://docs.rs/tokio/latest/tokio/task/#cooperative-scheduling>
778        let mut budget = RECEIVE_MESSAGE_BUDGET;
779
780        // The main poll loop that drives the session
781        'main: loop {
782            let mut progress = false;
783            let mut receive_pending = false;
784
785            // we prioritize incoming commands sent from the session manager
786            loop {
787                match this.commands_rx.poll_next_unpin(cx) {
788                    Poll::Pending => break,
789                    Poll::Ready(None) => {
790                        // this is only possible when the manager was dropped, in which case we also
791                        // terminate this session
792                        return Poll::Ready(())
793                    }
794                    Poll::Ready(Some(cmd)) => {
795                        progress = true;
796                        match cmd {
797                            SessionCommand::Disconnect { reason } => {
798                                debug!(
799                                    target: "net::session",
800                                    ?reason,
801                                    remote_peer_id=?this.remote_peer_id,
802                                    "Received disconnect command for session"
803                                );
804                                let reason =
805                                    reason.unwrap_or(DisconnectReason::DisconnectRequested);
806
807                                return this.try_disconnect(reason, cx)
808                            }
809                            SessionCommand::Message(msg) => {
810                                this.on_internal_peer_message(msg);
811                            }
812                        }
813                    }
814                }
815            }
816
817            // Drain the unbounded channel (broadcast overflow + disconnect commands)
818            while let Poll::Ready(Some(cmd)) = this.unbounded_rx.poll_recv(cx) {
819                progress = true;
820                match cmd {
821                    SessionCommand::Message(msg) => {
822                        this.unbounded_broadcast_msgs.increment(1);
823                        this.on_internal_peer_message(msg);
824                    }
825                    SessionCommand::Disconnect { reason } => {
826                        let reason = reason.unwrap_or(DisconnectReason::DisconnectRequested);
827                        return this.try_disconnect(reason, cx);
828                    }
829                }
830            }
831
832            let deadline = this.request_deadline();
833
834            while let Poll::Ready(Some(req)) = this.internal_request_rx.poll_next_unpin(cx) {
835                progress = true;
836                this.on_internal_peer_request(req, deadline);
837            }
838
839            // Advance all active requests.
840            // We remove each request one by one and add them back.
841            for idx in (0..this.received_requests_from_remote.len()).rev() {
842                let mut req = this.received_requests_from_remote.swap_remove(idx);
843                match req.rx.poll(cx) {
844                    Poll::Pending => {
845                        // not ready yet
846                        this.received_requests_from_remote.push(req);
847                    }
848                    Poll::Ready(resp) => {
849                        this.handle_outgoing_response(req.request_id, resp);
850                    }
851                }
852            }
853
854            // Send messages by advancing the sink and queuing in buffered messages. The sink only
855            // buffers sent messages; the explicit flush happens once per poll after the main
856            // loop, so messages queued across the loop's passes batch up (the sink still writes
857            // out on its own for control messages and when its write buffer runs full).
858            match this.poll_send_queued(cx) {
859                Ok(sent) => progress |= sent,
860                Err(err) => {
861                    debug!(target: "net::session", %err, remote_peer_id=?this.remote_peer_id, "failed to send message");
862                    // notify the manager
863                    return this.close_on_error(err, cx)
864                }
865            }
866
867            // read incoming messages from the wire
868            'receive: loop {
869                // ensure we still have enough budget for another iteration
870                budget -= 1;
871                if budget == 0 {
872                    // make sure we're woken up again
873                    cx.waker().wake_by_ref();
874                    break 'main
875                }
876
877                // try to resend the pending message that we could not send because the channel was
878                // full. [`PollSender`] will ensure that we're woken up again when the channel is
879                // ready to receive the message, and will only error if the channel is closed.
880                if let Some(msg) = this.pending_message_to_session.take() {
881                    match this.to_session_manager.poll_reserve(cx) {
882                        Poll::Ready(Ok(_)) => {
883                            let _ = this.to_session_manager.send_item(msg);
884                        }
885                        Poll::Ready(Err(_)) => return Poll::Ready(()),
886                        Poll::Pending => {
887                            this.pending_message_to_session = Some(msg);
888                            break 'receive
889                        }
890                    };
891                }
892
893                // check whether we should throttle incoming messages
894                if this.received_requests_from_remote.len() > MAX_QUEUED_OUTGOING_RESPONSES {
895                    // we're currently waiting for the responses to the peer's requests which aren't
896                    // queued as outgoing yet
897                    //
898                    // Note: we don't need to register the waker here because we polled the requests
899                    // above
900                    break 'receive
901                }
902
903                // we also need to check if we have multiple responses queued up
904                if this.queued_outgoing.response_count() > MAX_QUEUED_OUTGOING_RESPONSES {
905                    // if we've queued up more responses than allowed, we don't poll for new
906                    // messages and break the receive loop early
907                    //
908                    // Note: we don't need to register the waker here because we still have
909                    // queued messages and the sink impl registered the waker because we've
910                    // already advanced it to `Pending` earlier
911                    break 'receive
912                }
913
914                match this.conn.poll_next_unpin(cx) {
915                    Poll::Pending => {
916                        receive_pending = true;
917                        break
918                    }
919                    Poll::Ready(None) => {
920                        if this.is_disconnecting() {
921                            break
922                        }
923                        debug!(target: "net::session", remote_peer_id=?this.remote_peer_id, "eth stream completed");
924                        return this.emit_disconnect(cx)
925                    }
926                    Poll::Ready(Some(res)) => {
927                        match res {
928                            Ok(msg) => {
929                                let outcome = match msg {
930                                    EthSnapMessage::Eth(msg) => {
931                                        trace!(target: "net::session", msg_id=?msg.message_id(), remote_peer_id=?this.remote_peer_id, "received eth message");
932                                        // decode and handle message
933                                        this.on_incoming_message(msg)
934                                    }
935                                    EthSnapMessage::Snap(msg) => this.on_incoming_snap_message(msg),
936                                };
937                                match outcome {
938                                    OnIncomingMessageOutcome::Ok => {
939                                        // handled successfully
940                                        progress = true;
941                                    }
942                                    OnIncomingMessageOutcome::BadMessage { error, message } => {
943                                        debug!(target: "net::session", %error, msg=?message, remote_peer_id=?this.remote_peer_id, "received invalid protocol message");
944                                        this.on_bad_message();
945                                        return this
946                                            .try_disconnect(DisconnectReason::ProtocolBreach, cx)
947                                    }
948                                    OnIncomingMessageOutcome::NoCapacity(msg) => {
949                                        // failed to send due to lack of capacity
950                                        this.pending_message_to_session = Some(msg);
951                                    }
952                                }
953                            }
954                            Err(err) => {
955                                debug!(target: "net::session", %err, remote_peer_id=?this.remote_peer_id, "failed to receive message");
956                                if err.is_protocol_breach() {
957                                    this.on_bad_message();
958                                    return this.try_disconnect(DisconnectReason::ProtocolBreach, cx)
959                                }
960                                return this.close_on_error(err, cx)
961                            }
962                        }
963                    }
964                }
965            }
966
967            // Avoid one extra empty outer-loop pass after the wire is pending, unless the receive
968            // pass produced work that should be driven immediately.
969            if receive_pending &&
970                this.queued_outgoing.is_empty() &&
971                this.pending_message_to_session.is_none() &&
972                this.received_requests_from_remote.is_empty()
973            {
974                break 'main
975            }
976
977            if !progress {
978                break 'main
979            }
980        }
981
982        if let Some(interval) = &mut this.range_update_interval {
983            // Check if we should send a range update based on block height changes
984            while interval.poll_tick(cx).is_ready() {
985                let current_latest = this.local_range_info.latest();
986                let should_send = if let Some(last_sent) = this.last_sent_latest_block {
987                    // Only send if block height has advanced by at least one epoch (32 blocks)
988                    current_latest.saturating_sub(last_sent) >= EPOCH_SLOTS
989                } else {
990                    true // First update, always send
991                };
992
993                if should_send {
994                    this.queued_outgoing.push_back(
995                        EthMessage::BlockRangeUpdate(this.local_range_info.to_message()).into(),
996                    );
997                    this.last_sent_latest_block = Some(current_latest);
998                }
999            }
1000        }
1001
1002        if !this.inflight_requests.is_empty() {
1003            while this.internal_request_timeout_interval.poll_tick(cx).is_ready() {
1004                // check for timed out requests
1005                if this.check_timed_out_requests(Instant::now()) &&
1006                    let Poll::Ready(Ok(_)) = this.to_session_manager.poll_reserve(cx)
1007                {
1008                    let msg = ActiveSessionMessage::ProtocolBreach { peer_id: this.remote_peer_id };
1009                    this.pending_message_to_session = Some(msg);
1010                }
1011            }
1012        }
1013
1014        // Send anything the interval handlers above queued, then flush the transport for
1015        // everything buffered during this poll. This also resumes a flush that returned pending
1016        // on an earlier poll; a no-op if nothing is buffered.
1017        if let Err(err) = this.poll_send_queued(cx) {
1018            debug!(target: "net::session", %err, remote_peer_id=?this.remote_peer_id, "failed to send message");
1019            return this.close_on_error(err, cx)
1020        }
1021        match this.conn.poll_flush_unpin(cx) {
1022            Poll::Pending | Poll::Ready(Ok(())) => {}
1023            Poll::Ready(Err(err)) => {
1024                debug!(target: "net::session", %err, remote_peer_id=?this.remote_peer_id, "failed to flush connection");
1025                return this.close_on_error(err, cx)
1026            }
1027        }
1028
1029        this.shrink_to_fit();
1030
1031        Poll::Pending
1032    }
1033}
1034
1035/// Tracks a request received from the peer
1036pub(crate) struct ReceivedRequest<N: NetworkPrimitives> {
1037    /// Protocol Identifier
1038    request_id: u64,
1039    /// Receiver half of the channel that's supposed to receive the proper response.
1040    rx: PeerResponse<N>,
1041    /// Timestamp when we read this msg from the wire.
1042    #[expect(dead_code)]
1043    received: Instant,
1044}
1045
1046/// A request that waits for a response from the peer
1047pub(crate) struct InflightRequest<R> {
1048    /// Request we sent to peer and the internal response channel
1049    request: RequestState<R>,
1050    /// Instant when the request was sent
1051    timestamp: Instant,
1052    /// Time limit for the response
1053    deadline: Instant,
1054}
1055
1056impl<N: NetworkPrimitives> InflightRequest<PeerRequest<N>> {
1057    /// Returns true if the request is timedout
1058    #[inline]
1059    fn is_timed_out(&self, now: Instant) -> bool {
1060        now > self.deadline
1061    }
1062
1063    /// Returns true if we're still waiting for a response
1064    #[inline]
1065    const fn is_waiting(&self) -> bool {
1066        matches!(self.request, RequestState::Waiting(_))
1067    }
1068
1069    /// This will timeout the request by sending an error response to the internal channel
1070    fn timeout(&mut self) {
1071        let mut req = RequestState::TimedOut;
1072        std::mem::swap(&mut self.request, &mut req);
1073
1074        if let RequestState::Waiting(req) = req {
1075            req.send_err_response(RequestError::Timeout);
1076        }
1077    }
1078}
1079
1080/// All outcome variants when handling an incoming message
1081enum OnIncomingMessageOutcome<N: NetworkPrimitives> {
1082    /// Message successfully handled.
1083    Ok,
1084    /// Message is considered to be in violation of the protocol
1085    BadMessage { error: EthStreamError, message: EthMessage<N> },
1086    /// Currently no capacity to handle the message
1087    NoCapacity(ActiveSessionMessage<N>),
1088}
1089
1090impl<N: NetworkPrimitives> From<Result<(), ActiveSessionMessage<N>>>
1091    for OnIncomingMessageOutcome<N>
1092{
1093    fn from(res: Result<(), ActiveSessionMessage<N>>) -> Self {
1094        match res {
1095            Ok(_) => Self::Ok,
1096            Err(msg) => Self::NoCapacity(msg),
1097        }
1098    }
1099}
1100
1101enum RequestState<R> {
1102    /// Waiting for the response
1103    Waiting(R),
1104    /// Request already timed out
1105    TimedOut,
1106}
1107
1108/// Outgoing messages that can be sent over the wire.
1109#[derive(Debug)]
1110pub(crate) enum OutgoingMessage<N: NetworkPrimitives> {
1111    /// A message that is owned.
1112    Eth(EthMessage<N>),
1113    /// A message that may be shared by multiple sessions.
1114    Broadcast(EthBroadcastMessage<N>),
1115    /// A raw capability message
1116    Raw(RawCapabilityMessage),
1117    /// A `snap/2` message to send over the dedicated `eth`+`snap` stream.
1118    Snap(SnapProtocolMessage),
1119}
1120
1121impl<N: NetworkPrimitives> OutgoingMessage<N> {
1122    /// Returns true if this is a response.
1123    const fn is_response(&self) -> bool {
1124        match self {
1125            Self::Eth(msg) => msg.is_response(),
1126            // Served snap responses count toward response backpressure; outbound snap requests do
1127            // not. `SnapProtocolMessage::is_response` distinguishes the two.
1128            Self::Snap(msg) => msg.is_response(),
1129            Self::Broadcast(_) | Self::Raw(_) => false,
1130        }
1131    }
1132
1133    /// Returns the number of broadcast items in this message.
1134    ///
1135    /// For transaction hash announcements this is the number of hashes, for full transaction
1136    /// broadcasts it is the number of transactions, and for blocks it is 1.
1137    /// Request/response messages return 0.
1138    fn broadcast_item_count(&self) -> usize {
1139        match self {
1140            Self::Eth(msg) => match msg {
1141                EthMessage::NewBlockHashes(h) => h.len(),
1142                EthMessage::NewPooledTransactionHashes66(h) => h.len(),
1143                EthMessage::NewPooledTransactionHashes68(h) => h.hashes.len(),
1144                EthMessage::NewPooledTransactionHashes72(h) => h.hashes.len(),
1145                _ => 0,
1146            },
1147            Self::Broadcast(msg) => match msg {
1148                EthBroadcastMessage::NewBlock(_) => 1,
1149                EthBroadcastMessage::Transactions(txs) => txs.len(),
1150                EthBroadcastMessage::BroadcastPoolTransactions(txs) => txs.len(),
1151            },
1152            Self::Raw(_) | Self::Snap(_) => 0,
1153        }
1154    }
1155
1156    /// Tries to merge pooled transaction hash announcements into this message, consuming the
1157    /// incoming hashes. Returns `Some(incoming)` back if the variants don't match.
1158    fn try_merge_hashes(
1159        &mut self,
1160        incoming: NewPooledTransactionHashes,
1161    ) -> Option<NewPooledTransactionHashes> {
1162        let Self::Eth(eth) = self else { return Some(incoming) };
1163        match (eth, incoming) {
1164            (
1165                EthMessage::NewPooledTransactionHashes66(existing),
1166                NewPooledTransactionHashes::Eth66(inc),
1167            ) => {
1168                existing.extend(inc);
1169                None
1170            }
1171            (
1172                EthMessage::NewPooledTransactionHashes68(existing),
1173                NewPooledTransactionHashes::Eth68(inc),
1174            ) => {
1175                existing.hashes.extend(inc.hashes);
1176                existing.sizes.extend(inc.sizes);
1177                existing.types.extend(inc.types);
1178                None
1179            }
1180            (
1181                EthMessage::NewPooledTransactionHashes72(existing),
1182                NewPooledTransactionHashes::Eth72(inc),
1183            ) => {
1184                existing.hashes.extend(inc.hashes);
1185                existing.sizes.extend(inc.sizes);
1186                existing.types.extend(inc.types);
1187                None
1188            }
1189            (_, incoming) => Some(incoming),
1190        }
1191    }
1192}
1193
1194impl<N: NetworkPrimitives> From<EthMessage<N>> for OutgoingMessage<N> {
1195    fn from(value: EthMessage<N>) -> Self {
1196        Self::Eth(value)
1197    }
1198}
1199
1200impl<N: NetworkPrimitives> From<EthBroadcastMessage<N>> for OutgoingMessage<N> {
1201    fn from(value: EthBroadcastMessage<N>) -> Self {
1202        Self::Broadcast(value)
1203    }
1204}
1205
1206/// Returns the interval used to check for timed out requests.
1207///
1208/// Uses delayed missed-tick behavior because the interval is only polled while requests are in
1209/// flight, so ticks that elapsed while the session was idle must not fire in a burst.
1210pub(super) fn request_timeout_interval(timeout: Duration) -> Interval {
1211    let mut interval = tokio::time::interval(timeout);
1212    interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1213    interval
1214}
1215
1216/// Calculates a new timeout using an updated estimation of the RTT
1217#[inline]
1218fn calculate_new_timeout(current_timeout: Duration, estimated_rtt: Duration) -> Duration {
1219    let new_timeout = estimated_rtt.mul_f64(SAMPLE_IMPACT) * TIMEOUT_SCALING;
1220
1221    // this dampens sudden changes by taking a weighted mean of the old and new values
1222    let smoothened_timeout = current_timeout.mul_f64(1.0 - SAMPLE_IMPACT) + new_timeout;
1223
1224    smoothened_timeout.clamp(MINIMUM_TIMEOUT, MAXIMUM_TIMEOUT)
1225}
1226
1227/// A helper struct that wraps the queue of outgoing messages with broadcast-aware tracking.
1228///
1229/// Tracks both the total number of queued messages (via a metric gauge) and the total number of
1230/// broadcast items (tx hashes, transactions, blocks) via a shared atomic counter. The atomic
1231/// counter is shared with [`ActiveSessionHandle`](super::handle::ActiveSessionHandle) so the
1232/// [`SessionManager`](super::SessionManager) can apply size-based backpressure.
1233pub(crate) struct QueuedOutgoingMessages<N: NetworkPrimitives> {
1234    messages: VecDeque<OutgoingMessage<N>>,
1235    /// Number of queued response messages, tracked separately so the session can apply
1236    /// backpressure on incoming requests without scanning the whole queue.
1237    queued_responses: usize,
1238    count: Gauge,
1239    /// Shared counter of buffered broadcast items for size-based backpressure.
1240    broadcast_items: BroadcastItemCounter,
1241}
1242
1243impl<N: NetworkPrimitives> QueuedOutgoingMessages<N> {
1244    pub(crate) const fn new(metric: Gauge, broadcast_items: BroadcastItemCounter) -> Self {
1245        Self { messages: VecDeque::new(), queued_responses: 0, count: metric, broadcast_items }
1246    }
1247
1248    /// Returns the number of queued response messages.
1249    pub(crate) const fn response_count(&self) -> usize {
1250        self.queued_responses
1251    }
1252
1253    pub(crate) fn is_empty(&self) -> bool {
1254        self.messages.is_empty()
1255    }
1256
1257    pub(crate) fn push_back(&mut self, message: OutgoingMessage<N>) {
1258        self.queued_responses += message.is_response() as usize;
1259        self.messages.push_back(message);
1260        self.count.increment(1);
1261    }
1262
1263    pub(crate) fn pop_front(&mut self) -> Option<OutgoingMessage<N>> {
1264        self.messages.pop_front().inspect(|msg| {
1265            self.count.decrement(1);
1266            self.queued_responses -= msg.is_response() as usize;
1267            let items = msg.broadcast_item_count();
1268            if items > 0 {
1269                self.broadcast_items.sub(items);
1270            }
1271        })
1272    }
1273
1274    /// Pushes a pooled transaction hash announcement, merging into the last queued message if
1275    /// it is the same variant (eth66, eth68, or eth72).
1276    pub(crate) fn push_pooled_hashes(&mut self, msg: NewPooledTransactionHashes) {
1277        let msg = if let Some(last) = self.messages.back_mut() {
1278            match last.try_merge_hashes(msg) {
1279                None => return,
1280                Some(msg) => msg,
1281            }
1282        } else {
1283            msg
1284        };
1285        self.messages.push_back(EthMessage::from(msg).into());
1286        self.count.increment(1);
1287    }
1288
1289    /// Shrinks the queue's capacity back to its steady-state size once it is drained, if it grew
1290    /// well beyond it. The threshold avoids a shrink/regrow reallocation cycle on every poll
1291    /// under regular bursty traffic.
1292    pub(crate) fn shrink_to_fit(&mut self) {
1293        if self.messages.is_empty() && self.messages.capacity() > SHRINK_CAPACITY_THRESHOLD {
1294            self.messages.shrink_to(MAX_QUEUED_OUTGOING_RESPONSES);
1295        }
1296    }
1297}
1298
1299impl<N: NetworkPrimitives> Drop for QueuedOutgoingMessages<N> {
1300    fn drop(&mut self) {
1301        // Ensure gauge is decremented for any remaining items to avoid metric leak on teardown.
1302        let remaining = self.messages.len();
1303        if remaining > 0 {
1304            self.count.decrement(remaining as f64);
1305        }
1306    }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311    use super::*;
1312    use crate::session::{handle::PendingSessionEvent, start_pending_incoming_session};
1313    use alloy_eips::eip2124::ForkFilter;
1314    use alloy_primitives::B256;
1315    use futures::task::noop_waker;
1316    use reth_chainspec::MAINNET;
1317    use reth_ecies::stream::ECIESStream;
1318    use reth_eth_wire::{
1319        handshake::EthHandshake, protocol::Protocol, EthNetworkPrimitives, EthStream,
1320        GetBlockAccessLists, GetBlockBodies, HelloMessageWithProtocols, P2PStream, StatusBuilder,
1321        UnauthedEthStream, UnauthedP2PStream, UnifiedStatus,
1322    };
1323    use reth_eth_wire_types::{
1324        message::MAX_MESSAGE_SIZE,
1325        snap::{
1326            AccountRangeMessage, BlockAccessListsMessage, GetAccountRangeMessage,
1327            GetBlockAccessListsMessage,
1328        },
1329        BlockAccessLists, EthMessageID, NewPooledTransactionHashes72,
1330    };
1331    use reth_ethereum_forks::EthereumHardfork;
1332    use reth_network_p2p::error::RequestResult;
1333    use reth_network_peers::pk2id;
1334    use reth_network_types::session::config::PROTOCOL_BREACH_REQUEST_TIMEOUT;
1335    use secp256k1::{SecretKey, SECP256K1};
1336    use tokio::{
1337        net::{TcpListener, TcpStream},
1338        sync::mpsc,
1339    };
1340
1341    /// Returns a testing `HelloMessage` and new secretkey
1342    fn eth_hello(server_key: &SecretKey) -> HelloMessageWithProtocols {
1343        HelloMessageWithProtocols::builder(pk2id(&server_key.public_key(SECP256K1))).build()
1344    }
1345
1346    struct SessionBuilder<N: NetworkPrimitives = EthNetworkPrimitives> {
1347        _remote_capabilities: Arc<Capabilities>,
1348        active_session_tx: mpsc::Sender<ActiveSessionMessage<N>>,
1349        active_session_rx: ReceiverStream<ActiveSessionMessage<N>>,
1350        to_sessions: Vec<mpsc::Sender<SessionCommand<N>>>,
1351        secret_key: SecretKey,
1352        local_peer_id: PeerId,
1353        hello: HelloMessageWithProtocols,
1354        status: UnifiedStatus,
1355        fork_filter: ForkFilter,
1356        next_id: usize,
1357    }
1358
1359    impl<N: NetworkPrimitives> SessionBuilder<N> {
1360        fn next_id(&mut self) -> SessionId {
1361            let id = self.next_id;
1362            self.next_id += 1;
1363            SessionId(id)
1364        }
1365
1366        /// Connects a new Eth stream and executes the given closure with that established stream
1367        fn with_client_stream<F, O>(
1368            &self,
1369            local_addr: SocketAddr,
1370            f: F,
1371        ) -> Pin<Box<dyn Future<Output = ()> + Send>>
1372        where
1373            F: FnOnce(EthStream<P2PStream<ECIESStream<TcpStream>>, N>) -> O + Send + 'static,
1374            O: Future<Output = ()> + Send + Sync,
1375        {
1376            let mut status = self.status;
1377            let fork_filter = self.fork_filter.clone();
1378            let local_peer_id = self.local_peer_id;
1379            let mut hello = self.hello.clone();
1380            let key = SecretKey::new(&mut rand_08::thread_rng());
1381            hello.id = pk2id(&key.public_key(SECP256K1));
1382            Box::pin(async move {
1383                let outgoing = TcpStream::connect(local_addr).await.unwrap();
1384                let sink = ECIESStream::connect(outgoing, key, local_peer_id).await.unwrap();
1385
1386                let (p2p_stream, _) = UnauthedP2PStream::new(sink).handshake(hello).await.unwrap();
1387
1388                let eth_version = p2p_stream.shared_capabilities().eth_version().unwrap();
1389                status.set_eth_version(eth_version);
1390
1391                let (client_stream, _) = UnauthedEthStream::new(p2p_stream)
1392                    .handshake(status, fork_filter)
1393                    .await
1394                    .unwrap();
1395                f(client_stream).await
1396            })
1397        }
1398
1399        async fn connect_incoming(&mut self, stream: TcpStream) -> ActiveSession<N> {
1400            let remote_addr = stream.local_addr().unwrap();
1401            let session_id = self.next_id();
1402            let (_disconnect_tx, disconnect_rx) = oneshot::channel();
1403            let (pending_sessions_tx, pending_sessions_rx) = mpsc::channel(1);
1404
1405            tokio::task::spawn(start_pending_incoming_session(
1406                Arc::new(EthHandshake::default()),
1407                MAX_MESSAGE_SIZE,
1408                disconnect_rx,
1409                session_id,
1410                stream,
1411                pending_sessions_tx,
1412                remote_addr,
1413                self.secret_key,
1414                self.hello.clone(),
1415                self.status,
1416                self.fork_filter.clone(),
1417                Default::default(),
1418            ));
1419
1420            let mut stream = ReceiverStream::new(pending_sessions_rx);
1421
1422            match stream.next().await.unwrap() {
1423                PendingSessionEvent::Established {
1424                    session_id,
1425                    remote_addr,
1426                    peer_id,
1427                    capabilities,
1428                    conn,
1429                    ..
1430                } => {
1431                    let (_to_session_tx, messages_rx) = mpsc::channel(10);
1432                    let (commands_to_session, commands_rx) = mpsc::channel(10);
1433                    let (_unbounded_tx, unbounded_rx) = mpsc::unbounded_channel();
1434                    let poll_sender = PollSender::new(self.active_session_tx.clone());
1435
1436                    self.to_sessions.push(commands_to_session);
1437
1438                    ActiveSession {
1439                        next_id: 0,
1440                        remote_peer_id: peer_id,
1441                        remote_addr,
1442                        remote_capabilities: Arc::clone(&capabilities),
1443                        session_id,
1444                        commands_rx: ReceiverStream::new(commands_rx),
1445                        unbounded_rx,
1446                        unbounded_broadcast_msgs: Counter::noop(),
1447                        to_session_manager: MeteredPollSender::new(
1448                            poll_sender,
1449                            "network_active_session",
1450                        ),
1451                        pending_message_to_session: None,
1452                        internal_request_rx: ReceiverStream::new(messages_rx).fuse(),
1453                        inflight_requests: Default::default(),
1454                        conn,
1455                        queued_outgoing: QueuedOutgoingMessages::new(
1456                            Gauge::noop(),
1457                            BroadcastItemCounter::new(),
1458                        ),
1459                        received_requests_from_remote: Default::default(),
1460                        internal_request_timeout_interval: request_timeout_interval(
1461                            INITIAL_REQUEST_TIMEOUT,
1462                        ),
1463                        internal_request_timeout: Arc::new(AtomicU64::new(
1464                            INITIAL_REQUEST_TIMEOUT.as_millis() as u64,
1465                        )),
1466                        protocol_breach_request_timeout: PROTOCOL_BREACH_REQUEST_TIMEOUT,
1467                        terminate_message: None,
1468                        range_info: None,
1469                        local_range_info: BlockRangeInfo::new(
1470                            0,
1471                            1000,
1472                            alloy_primitives::B256::ZERO,
1473                        ),
1474                        range_update_interval: None,
1475                        last_sent_latest_block: None,
1476                    }
1477                }
1478                ev => {
1479                    panic!("unexpected message {ev:?}")
1480                }
1481            }
1482        }
1483    }
1484
1485    impl Default for SessionBuilder {
1486        fn default() -> Self {
1487            let (active_session_tx, active_session_rx) = mpsc::channel(100);
1488
1489            let (secret_key, pk) = SECP256K1.generate_keypair(&mut rand_08::thread_rng());
1490            let local_peer_id = pk2id(&pk);
1491
1492            Self {
1493                next_id: 0,
1494                _remote_capabilities: Arc::new(Capabilities::from(vec![])),
1495                active_session_tx,
1496                active_session_rx: ReceiverStream::new(active_session_rx),
1497                to_sessions: vec![],
1498                hello: eth_hello(&secret_key),
1499                secret_key,
1500                local_peer_id,
1501                status: StatusBuilder::default().build(),
1502                fork_filter: MAINNET
1503                    .hardfork_fork_filter(EthereumHardfork::Frontier)
1504                    .expect("The Frontier fork filter should exist on mainnet"),
1505            }
1506        }
1507    }
1508
1509    /// Returns a [`SessionBuilder`] whose hello also advertises `snap/2`, so the negotiated
1510    /// session ends up on an [`EthSnapStream`](reth_eth_wire::EthSnapStream) connection instead
1511    /// of a plain `eth`-only one.
1512    fn snap_session_builder() -> SessionBuilder {
1513        let mut builder = SessionBuilder::default();
1514        builder.hello.try_add_protocol(Protocol::snap_2()).unwrap();
1515        builder
1516    }
1517
1518    /// Dispatches a `snap/2` request via [`ActiveSession::on_internal_peer_request`] and returns
1519    /// the session-assigned request id plus the caller's response receiver.
1520    fn dispatch_snap_request(
1521        session: &mut ActiveSession<EthNetworkPrimitives>,
1522        caller_request_id: u64,
1523    ) -> (u64, oneshot::Receiver<RequestResult<SnapResponse>>) {
1524        let (response, rx) = oneshot::channel();
1525        let request = SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
1526            request_id: caller_request_id,
1527            block_hashes: Vec::new(),
1528            response_bytes: 0,
1529        });
1530        let deadline = session.request_deadline();
1531        session.on_internal_peer_request(PeerRequest::GetSnap { request, response }, deadline);
1532        let id = *session.inflight_requests.keys().next().expect("snap request tracked");
1533        (id, rx)
1534    }
1535
1536    #[tokio::test(flavor = "multi_thread")]
1537    async fn test_disconnect() {
1538        let mut builder = SessionBuilder::default();
1539
1540        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1541        let local_addr = listener.local_addr().unwrap();
1542
1543        let expected_disconnect = DisconnectReason::UselessPeer;
1544
1545        let fut = builder.with_client_stream(local_addr, async move |mut client_stream| {
1546            let msg = client_stream.next().await.unwrap().unwrap_err();
1547            assert_eq!(msg.as_disconnected().unwrap(), expected_disconnect);
1548        });
1549
1550        tokio::task::spawn(async move {
1551            let (incoming, _) = listener.accept().await.unwrap();
1552            let mut session = builder.connect_incoming(incoming).await;
1553
1554            session.start_disconnect(expected_disconnect).unwrap();
1555            session.await
1556        });
1557
1558        fut.await;
1559    }
1560
1561    #[tokio::test(flavor = "multi_thread")]
1562    async fn test_invalid_message_disconnects_with_protocol_breach() {
1563        let mut builder = SessionBuilder::default();
1564
1565        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1566        let local_addr = listener.local_addr().unwrap();
1567
1568        let fut = builder.with_client_stream(local_addr, async move |mut client_stream| {
1569            client_stream
1570                .start_send_raw(RawCapabilityMessage::eth(
1571                    EthMessageID::PooledTransactions,
1572                    vec![0xc0].into(),
1573                ))
1574                .unwrap();
1575            client_stream.flush().await.unwrap();
1576
1577            let msg = client_stream.next().await.unwrap().unwrap_err();
1578            assert_eq!(msg.as_disconnected(), Some(DisconnectReason::ProtocolBreach));
1579        });
1580
1581        let (tx, rx) = oneshot::channel();
1582
1583        tokio::task::spawn(async move {
1584            let (incoming, _) = listener.accept().await.unwrap();
1585            let session = builder.connect_incoming(incoming).await;
1586            session.await;
1587
1588            tx.send(()).unwrap();
1589        });
1590
1591        fut.await;
1592        rx.await.unwrap();
1593    }
1594
1595    #[tokio::test(flavor = "multi_thread")]
1596    async fn handle_dropped_stream() {
1597        let mut builder = SessionBuilder::default();
1598
1599        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1600        let local_addr = listener.local_addr().unwrap();
1601
1602        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1603            drop(client_stream);
1604            tokio::time::sleep(Duration::from_secs(1)).await
1605        });
1606
1607        let (tx, rx) = oneshot::channel();
1608
1609        tokio::task::spawn(async move {
1610            let (incoming, _) = listener.accept().await.unwrap();
1611            let session = builder.connect_incoming(incoming).await;
1612            session.await;
1613
1614            tx.send(()).unwrap();
1615        });
1616
1617        tokio::task::spawn(fut);
1618
1619        rx.await.unwrap();
1620    }
1621
1622    #[tokio::test(flavor = "multi_thread")]
1623    async fn test_send_many_messages() {
1624        reth_tracing::init_test_tracing();
1625        let mut builder = SessionBuilder::default();
1626
1627        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1628        let local_addr = listener.local_addr().unwrap();
1629
1630        let num_messages = 100;
1631
1632        let fut = builder.with_client_stream(local_addr, async move |mut client_stream| {
1633            for _ in 0..num_messages {
1634                client_stream
1635                    .send(EthMessage::NewPooledTransactionHashes66(Vec::new().into()))
1636                    .await
1637                    .unwrap();
1638            }
1639        });
1640
1641        let (tx, rx) = oneshot::channel();
1642
1643        tokio::task::spawn(async move {
1644            let (incoming, _) = listener.accept().await.unwrap();
1645            let session = builder.connect_incoming(incoming).await;
1646            session.await;
1647
1648            tx.send(()).unwrap();
1649        });
1650
1651        tokio::task::spawn(fut);
1652
1653        rx.await.unwrap();
1654    }
1655
1656    #[tokio::test(flavor = "multi_thread")]
1657    async fn test_request_timeout() {
1658        reth_tracing::init_test_tracing();
1659
1660        let mut builder = SessionBuilder::default();
1661
1662        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1663        let local_addr = listener.local_addr().unwrap();
1664
1665        let request_timeout = Duration::from_millis(100);
1666        let drop_timeout = Duration::from_millis(1500);
1667
1668        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1669            let _client_stream = client_stream;
1670            tokio::time::sleep(drop_timeout * 60).await;
1671        });
1672        tokio::task::spawn(fut);
1673
1674        let (incoming, _) = listener.accept().await.unwrap();
1675        let mut session = builder.connect_incoming(incoming).await;
1676        session
1677            .internal_request_timeout
1678            .store(request_timeout.as_millis() as u64, Ordering::Relaxed);
1679        session.protocol_breach_request_timeout = drop_timeout;
1680        session.internal_request_timeout_interval =
1681            tokio::time::interval_at(tokio::time::Instant::now(), request_timeout);
1682        session
1683            .internal_request_timeout_interval
1684            .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1685        let (tx, rx) = oneshot::channel();
1686        let req = PeerRequest::GetBlockBodies { request: GetBlockBodies(vec![]), response: tx };
1687        session.on_internal_peer_request(req, Instant::now());
1688        tokio::spawn(session);
1689
1690        let err = rx.await.unwrap().unwrap_err();
1691        assert_eq!(err, RequestError::Timeout);
1692
1693        // wait for protocol breach error
1694        let msg = builder.active_session_rx.next().await.unwrap();
1695        match msg {
1696            ActiveSessionMessage::ProtocolBreach { .. } => {}
1697            ev => unreachable!("{ev:?}"),
1698        }
1699    }
1700
1701    #[tokio::test(flavor = "multi_thread")]
1702    async fn snap_request_is_assigned_unique_id_and_response_correlated() {
1703        let mut builder = snap_session_builder();
1704        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1705        let local_addr = listener.local_addr().unwrap();
1706        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1707            let _client_stream = client_stream;
1708            tokio::time::sleep(Duration::from_secs(60)).await;
1709        });
1710        tokio::task::spawn(fut);
1711        let (incoming, _) = listener.accept().await.unwrap();
1712        let mut session = builder.connect_incoming(incoming).await;
1713
1714        // The session assigns its own request id (not the caller's sentinel) and tracks it.
1715        let (id, rx) = dispatch_snap_request(&mut session, u64::MAX);
1716        assert_ne!(id, u64::MAX, "session must assign its own request id");
1717
1718        // A response carrying that id is correlated back to the caller's future.
1719        let outcome = session.on_incoming_snap_message(SnapProtocolMessage::BlockAccessLists(
1720            BlockAccessListsMessage {
1721                request_id: id,
1722                block_access_lists: BlockAccessLists(Vec::new()),
1723            },
1724        ));
1725        assert!(matches!(outcome, OnIncomingMessageOutcome::Ok));
1726        assert!(!session.inflight_requests.contains_key(&id));
1727
1728        // The delivered response carries the caller's original id again, not the session's.
1729        let response = rx.await.unwrap().unwrap();
1730        assert!(matches!(
1731            response,
1732            SnapResponse::BlockAccessLists(m) if m.request_id == u64::MAX
1733        ));
1734    }
1735
1736    #[tokio::test(flavor = "multi_thread")]
1737    async fn wrong_type_snap_response_is_rejected() {
1738        let mut builder = snap_session_builder();
1739        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1740        let local_addr = listener.local_addr().unwrap();
1741        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1742            let _client_stream = client_stream;
1743            tokio::time::sleep(Duration::from_secs(60)).await;
1744        });
1745        tokio::task::spawn(fut);
1746        let (incoming, _) = listener.accept().await.unwrap();
1747        let mut session = builder.connect_incoming(incoming).await;
1748
1749        let (id, rx) = dispatch_snap_request(&mut session, 0);
1750
1751        // Answering a GetBlockAccessLists with an AccountRange under the same id is a bad message.
1752        let outcome = session.on_incoming_snap_message(SnapProtocolMessage::AccountRange(
1753            AccountRangeMessage { request_id: id, accounts: Vec::new(), proof: Vec::new() },
1754        ));
1755        assert!(matches!(outcome, OnIncomingMessageOutcome::Ok));
1756        assert!(!session.inflight_requests.contains_key(&id));
1757        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::BadResponse);
1758        assert!(matches!(
1759            builder.active_session_rx.next().await,
1760            Some(ActiveSessionMessage::BadMessage { .. })
1761        ));
1762    }
1763
1764    #[tokio::test(flavor = "multi_thread")]
1765    async fn snap_request_times_out() {
1766        let mut builder = snap_session_builder();
1767        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1768        let local_addr = listener.local_addr().unwrap();
1769        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1770            let _client_stream = client_stream;
1771            tokio::time::sleep(Duration::from_secs(60)).await;
1772        });
1773        tokio::task::spawn(fut);
1774        let (incoming, _) = listener.accept().await.unwrap();
1775        let mut session = builder.connect_incoming(incoming).await;
1776
1777        // Tiny timeout so the deadline (computed at insert) is already in the past.
1778        session.internal_request_timeout.store(1, Ordering::Relaxed);
1779        let (id, rx) = dispatch_snap_request(&mut session, 0);
1780
1781        // The first check resolves the caller with a timeout but keeps the entry so the session
1782        // can escalate to a protocol breach.
1783        tokio::time::sleep(Duration::from_millis(20)).await;
1784        assert!(!session.check_timed_out_requests(Instant::now()));
1785        assert!(session.inflight_requests.contains_key(&id));
1786        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::Timeout);
1787
1788        // Once the breach timeout passes without a response, the session flags a protocol breach.
1789        session.protocol_breach_request_timeout = Duration::from_millis(1);
1790        assert!(session.check_timed_out_requests(Instant::now()));
1791    }
1792
1793    #[tokio::test(flavor = "multi_thread")]
1794    async fn late_snap_response_is_consumed_without_penalty() {
1795        let mut builder = snap_session_builder();
1796        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1797        let local_addr = listener.local_addr().unwrap();
1798        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1799            let _client_stream = client_stream;
1800            tokio::time::sleep(Duration::from_secs(60)).await;
1801        });
1802        tokio::task::spawn(fut);
1803        let (incoming, _) = listener.accept().await.unwrap();
1804        let mut session = builder.connect_incoming(incoming).await;
1805
1806        session.internal_request_timeout.store(1, Ordering::Relaxed);
1807        let (id, _rx) = dispatch_snap_request(&mut session, 0);
1808        tokio::time::sleep(Duration::from_millis(20)).await;
1809        assert!(!session.check_timed_out_requests(Instant::now()));
1810
1811        // A response arriving after the timeout clears the entry without a bad-message report.
1812        let outcome = session.on_incoming_snap_message(SnapProtocolMessage::BlockAccessLists(
1813            BlockAccessListsMessage {
1814                request_id: id,
1815                block_access_lists: BlockAccessLists(Vec::new()),
1816            },
1817        ));
1818        assert!(matches!(outcome, OnIncomingMessageOutcome::Ok));
1819        assert!(!session.inflight_requests.contains_key(&id));
1820        assert!(futures::FutureExt::now_or_never(builder.active_session_rx.next())
1821            .flatten()
1822            .is_none());
1823    }
1824
1825    #[tokio::test(flavor = "multi_thread")]
1826    async fn unknown_snap_response_is_penalized() {
1827        let mut builder = snap_session_builder();
1828        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1829        let local_addr = listener.local_addr().unwrap();
1830        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1831            let _client_stream = client_stream;
1832            tokio::time::sleep(Duration::from_secs(60)).await;
1833        });
1834        tokio::task::spawn(fut);
1835        let (incoming, _) = listener.accept().await.unwrap();
1836        let mut session = builder.connect_incoming(incoming).await;
1837
1838        // A response for a request we never sent is dropped and reported as a bad message.
1839        let outcome = session.on_incoming_snap_message(SnapProtocolMessage::BlockAccessLists(
1840            BlockAccessListsMessage {
1841                request_id: 999,
1842                block_access_lists: BlockAccessLists(Vec::new()),
1843            },
1844        ));
1845        assert!(matches!(outcome, OnIncomingMessageOutcome::Ok));
1846        assert!(session.inflight_requests.is_empty());
1847        assert!(session.queued_outgoing.pop_front().is_none());
1848        assert!(matches!(
1849            builder.active_session_rx.next().await,
1850            Some(ActiveSessionMessage::BadMessage { .. })
1851        ));
1852    }
1853
1854    #[tokio::test(flavor = "multi_thread")]
1855    async fn get_snap_request_rejected_without_negotiated_snap() {
1856        // A plain `eth`-only session: no `snap/2` was negotiated.
1857        let mut builder = SessionBuilder::default();
1858        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1859        let local_addr = listener.local_addr().unwrap();
1860        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1861            let _client_stream = client_stream;
1862            tokio::time::sleep(Duration::from_secs(60)).await;
1863        });
1864        tokio::task::spawn(fut);
1865        let (incoming, _) = listener.accept().await.unwrap();
1866        let mut session = builder.connect_incoming(incoming).await;
1867        assert!(!session.conn.supports_snap());
1868
1869        let (response, rx) = oneshot::channel();
1870        let request = SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
1871            request_id: 0,
1872            block_hashes: Vec::new(),
1873            response_bytes: 0,
1874        });
1875        let deadline = session.request_deadline();
1876        session.on_internal_peer_request(PeerRequest::GetSnap { request, response }, deadline);
1877
1878        // Rejected immediately instead of being queued for a connection that can't send it.
1879        assert!(session.inflight_requests.is_empty());
1880        assert!(session.queued_outgoing.pop_front().is_none());
1881        assert_eq!(rx.await.unwrap().unwrap_err(), RequestError::UnsupportedCapability);
1882    }
1883
1884    #[tokio::test(flavor = "multi_thread")]
1885    async fn inbound_snap_request_round_trips_to_a_response() {
1886        let mut builder = snap_session_builder();
1887        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1888        let local_addr = listener.local_addr().unwrap();
1889        let fut = builder.with_client_stream(local_addr, async move |client_stream| {
1890            let _client_stream = client_stream;
1891            tokio::time::sleep(Duration::from_secs(60)).await;
1892        });
1893        tokio::task::spawn(fut);
1894        let (incoming, _) = listener.accept().await.unwrap();
1895        let mut session = builder.connect_incoming(incoming).await;
1896
1897        // The peer sends an inbound GetAccountRange request.
1898        let outcome = session.on_incoming_snap_message(SnapProtocolMessage::GetAccountRange(
1899            GetAccountRangeMessage {
1900                request_id: 7,
1901                root_hash: B256::ZERO,
1902                starting_hash: B256::ZERO,
1903                limit_hash: B256::ZERO,
1904                response_bytes: 1024,
1905            },
1906        ));
1907        assert!(matches!(outcome, OnIncomingMessageOutcome::Ok));
1908        assert_eq!(session.received_requests_from_remote.len(), 1);
1909
1910        // It's routed upward instead of being served inline.
1911        let Some(ActiveSessionMessage::ValidMessage {
1912            message: PeerMessage::EthRequest(PeerRequest::GetSnap { request, response }),
1913            ..
1914        }) = builder.active_session_rx.next().await
1915        else {
1916            panic!("expected an outbound GetSnap request")
1917        };
1918        assert!(matches!(request, SnapProtocolMessage::GetAccountRange(_)));
1919
1920        // The handler answers with an empty-but-valid range.
1921        let _ = response.send(Ok(SnapResponse::AccountRange(AccountRangeMessage {
1922            request_id: 7,
1923            accounts: Vec::new(),
1924            proof: Vec::new(),
1925        })));
1926
1927        // Drive the same conversion the session's main poll loop would.
1928        let mut req = session.received_requests_from_remote.pop().unwrap();
1929        let waker = noop_waker();
1930        let mut cx = Context::from_waker(&waker);
1931        let Poll::Ready(resp) = req.rx.poll(&mut cx) else { panic!("response should be ready") };
1932        session.handle_outgoing_response(req.request_id, resp);
1933
1934        // The reply goes out as a snap/2 message carrying the original request id, not an eth
1935        // message.
1936        let msg = session.queued_outgoing.pop_front().expect("response queued for send");
1937        assert!(matches!(
1938            msg,
1939            OutgoingMessage::Snap(SnapProtocolMessage::AccountRange(AccountRangeMessage {
1940                request_id: 7,
1941                ..
1942            }))
1943        ));
1944    }
1945
1946    #[test]
1947    fn eth72_pooled_hashes_count_broadcast_items() {
1948        let hashes =
1949            vec![alloy_primitives::B256::repeat_byte(1), alloy_primitives::B256::repeat_byte(2)];
1950        let msg: OutgoingMessage<EthNetworkPrimitives> =
1951            EthMessage::NewPooledTransactionHashes72(NewPooledTransactionHashes72 {
1952                types: vec![0; hashes.len()],
1953                sizes: vec![1; hashes.len()],
1954                hashes,
1955                cell_mask: None,
1956            })
1957            .into();
1958
1959        assert_eq!(2, msg.broadcast_item_count());
1960    }
1961
1962    #[test]
1963    fn test_reject_bal_request_for_eth70() {
1964        let (tx, _rx) = oneshot::channel();
1965        let request: PeerRequest<EthNetworkPrimitives> =
1966            PeerRequest::GetBlockAccessLists { request: GetBlockAccessLists(vec![]), response: tx };
1967
1968        assert!(!ActiveSession::<EthNetworkPrimitives>::is_request_supported_for_version(
1969            &request,
1970            EthVersion::Eth70
1971        ));
1972        assert!(ActiveSession::<EthNetworkPrimitives>::is_request_supported_for_version(
1973            &request,
1974            EthVersion::Eth71
1975        ));
1976    }
1977
1978    #[tokio::test(flavor = "multi_thread")]
1979    async fn test_keep_alive() {
1980        let mut builder = SessionBuilder::default();
1981
1982        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1983        let local_addr = listener.local_addr().unwrap();
1984
1985        let fut = builder.with_client_stream(local_addr, async move |mut client_stream| {
1986            let _ = tokio::time::timeout(Duration::from_secs(5), client_stream.next()).await;
1987            client_stream.into_inner().disconnect(DisconnectReason::UselessPeer).await.unwrap();
1988        });
1989
1990        let (tx, rx) = oneshot::channel();
1991
1992        tokio::task::spawn(async move {
1993            let (incoming, _) = listener.accept().await.unwrap();
1994            let session = builder.connect_incoming(incoming).await;
1995            session.await;
1996
1997            tx.send(()).unwrap();
1998        });
1999
2000        tokio::task::spawn(fut);
2001
2002        rx.await.unwrap();
2003    }
2004
2005    // This tests that incoming messages are delivered when there's capacity.
2006    #[tokio::test(flavor = "multi_thread")]
2007    async fn test_send_at_capacity() {
2008        let mut builder = SessionBuilder::default();
2009
2010        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2011        let local_addr = listener.local_addr().unwrap();
2012
2013        let fut = builder.with_client_stream(local_addr, async move |mut client_stream| {
2014            client_stream
2015                .send(EthMessage::NewPooledTransactionHashes68(Default::default()))
2016                .await
2017                .unwrap();
2018            let _ = tokio::time::timeout(Duration::from_secs(100), client_stream.next()).await;
2019        });
2020        tokio::task::spawn(fut);
2021
2022        let (incoming, _) = listener.accept().await.unwrap();
2023        let session = builder.connect_incoming(incoming).await;
2024
2025        // fill the entire message buffer with an unrelated message
2026        let mut num_fill_messages = 0;
2027        loop {
2028            if builder
2029                .active_session_tx
2030                .try_send(ActiveSessionMessage::ProtocolBreach { peer_id: PeerId::random() })
2031                .is_err()
2032            {
2033                break
2034            }
2035            num_fill_messages += 1;
2036        }
2037
2038        tokio::task::spawn(async move {
2039            session.await;
2040        });
2041
2042        tokio::time::sleep(Duration::from_millis(100)).await;
2043
2044        for _ in 0..num_fill_messages {
2045            let message = builder.active_session_rx.next().await.unwrap();
2046            match message {
2047                ActiveSessionMessage::ProtocolBreach { .. } => {}
2048                ev => unreachable!("{ev:?}"),
2049            }
2050        }
2051
2052        let message = builder.active_session_rx.next().await.unwrap();
2053        match message {
2054            ActiveSessionMessage::ValidMessage {
2055                message: PeerMessage::PooledTransactions(_),
2056                ..
2057            } => {}
2058            _ => unreachable!(),
2059        }
2060    }
2061
2062    #[test]
2063    fn timeout_calculation_sanity_tests() {
2064        let rtt = Duration::from_secs(5);
2065        // timeout for an RTT of `rtt`
2066        let timeout = rtt * TIMEOUT_SCALING;
2067
2068        // if rtt hasn't changed, timeout shouldn't change
2069        assert_eq!(calculate_new_timeout(timeout, rtt), timeout);
2070
2071        // if rtt changed, the new timeout should change less than it
2072        assert!(calculate_new_timeout(timeout, rtt / 2) < timeout);
2073        assert!(calculate_new_timeout(timeout, rtt / 2) > timeout / 2);
2074        assert!(calculate_new_timeout(timeout, rtt * 2) > timeout);
2075        assert!(calculate_new_timeout(timeout, rtt * 2) < timeout * 2);
2076    }
2077}