Skip to main content

reth_network/session/
active.rs

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