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