Skip to main content

ethrex_p2p/rlpx/connection/
server.rs

1#[cfg(feature = "l2")]
2use crate::rlpx::l2::{
3    PERIODIC_BATCH_BROADCAST_INTERVAL, PERIODIC_BLOCK_BROADCAST_INTERVAL,
4    l2_connection::{
5        self, L2Cast, L2ConnState, handle_based_capability_message, handle_l2_broadcast,
6    },
7};
8use crate::{
9    backend,
10    metrics::METRICS,
11    network::P2PContext,
12    peer_table::{PeerTable, PeerTableServerProtocol as _},
13    rlpx::{
14        Message,
15        connection::{codec::RLPxCodec, handshake},
16        error::PeerConnectionError,
17        eth::{
18            block_access_lists::{BlockAccessLists, GetBlockAccessLists},
19            blocks::{BlockBodies, BlockHeaders},
20            receipts::{
21                GetReceipts68, GetReceipts70, Receipts68, Receipts69, Receipts70,
22                SOFT_RESPONSE_LIMIT,
23            },
24            status::{StatusMessage68, StatusMessage69, StatusMessage70, StatusMessage71},
25            transactions::{GetPooledTransactions, NewPooledTransactionHashes},
26            update::BlockRangeUpdate,
27        },
28        message::EthCapVersion,
29        p2p::{
30            self, Capability, DisconnectMessage, DisconnectReason, PingMessage, PongMessage,
31            SUPPORTED_ETH_CAPABILITIES, SUPPORTED_SNAP_CAPABILITIES,
32        },
33        snap::TrieNodes,
34    },
35    snap::{
36        process_account_range_request, process_byte_codes_request, process_storage_ranges_request,
37        process_trie_nodes_request,
38    },
39    tx_broadcaster::{TxBroadcaster, TxBroadcasterProtocol as _, send_tx_hashes},
40    types::Node,
41};
42use ethrex_blockchain::Blockchain;
43use ethrex_common::H256;
44#[cfg(feature = "l2")]
45use ethrex_common::types::Transaction;
46use ethrex_common::types::{MempoolTransaction, P2PTransaction, Receipt};
47use ethrex_crypto::NativeCrypto;
48use ethrex_rlp::encode::RLPEncode;
49use ethrex_storage::{Store, error::StoreError};
50use ethrex_trie::TrieError;
51use futures::{SinkExt as _, Stream, stream::SplitSink};
52use rand::random;
53use rustc_hash::FxHashMap;
54use secp256k1::{PublicKey, SecretKey};
55use spawned_concurrency::{
56    actor,
57    error::ActorError,
58    protocol,
59    tasks::{Actor, ActorRef, ActorStart as _, Context, Handler, send_interval, spawn_listener},
60};
61use spawned_rt::tasks::BroadcastStream;
62use std::{
63    collections::HashMap,
64    net::SocketAddr,
65    sync::{Arc, RwLock},
66    time::{Duration, Instant},
67};
68use tokio::{
69    net::TcpStream,
70    sync::{broadcast, oneshot},
71    task::{self, Id},
72};
73use tokio_stream::StreamExt;
74use tokio_util::codec::Framed;
75use tracing::{debug, error, trace, warn};
76
77const PING_INTERVAL: Duration = Duration::from_secs(10);
78const BLOCK_RANGE_UPDATE_INTERVAL: Duration = Duration::from_secs(60);
79const INFLIGHT_TX_SWEEP_INTERVAL: Duration = Duration::from_secs(15);
80const INFLIGHT_TX_TIMEOUT: Duration = Duration::from_secs(30);
81/// Max time a single outbound frame may take to flush to a peer's socket before we
82/// consider the peer wedged and drop it. Without this bound, a slow/half-dead peer
83/// (TCP send window full, never draining) blocks the connection actor's serial drain
84/// indefinitely while its unbounded mailbox keeps growing — the timer/broadcast
85/// producers below enqueue regardless of drain progress — leaking memory that is
86/// never reclaimed (the actor cannot be stopped while a handler is wedged in `.await`).
87const OUTBOUND_SEND_TIMEOUT: Duration = Duration::from_secs(30);
88/// Max time the RLPx handshake may take before we abandon the connection. Without
89/// this, an inbound peer that opens TCP and then stalls before/at the auth read
90/// parks a connection actor + socket indefinitely (the handshake runs inside
91/// `started()` with no timeout), so established sockets accumulate far beyond the
92/// peer target.
93const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
94/// If a peer fails to answer this many consecutive pings, consider it dead and drop it.
95/// Catches application-dead-but-TCP-alive peers that would otherwise never be removed.
96const MAX_MISSED_PONGS: u8 = 3;
97/// Capacity of the per-connection bounded outbound queue. The writer task drains this into
98/// the socket; if a peer can't keep up the queue fills and the peer is dropped, so outbound
99/// buffering per connection is bounded to this many messages instead of growing unbounded.
100const OUTBOUND_QUEUE_CAP: usize = 1024;
101/// How often to flush buffered transaction hash requests into a single
102/// batched GetPooledTransactions message.
103const TX_REQUEST_BATCH_INTERVAL: Duration = Duration::from_millis(50);
104/// Fixed (tumbling) time window for incoming request rate limiting.
105const SERVE_REQUEST_WINDOW: Duration = Duration::from_secs(60);
106/// Maximum number of data-serving requests allowed per peer within the rate-limit window.
107const MAX_SERVE_REQUESTS_PER_WINDOW: u64 = 500;
108/// Number of transactions sent to a peer before checking for leeching behaviour.
109const LEECH_TX_SENT_THRESHOLD: u64 = 10_000;
110
111pub(crate) type PeerConnBroadcastSender = broadcast::Sender<(tokio::task::Id, Arc<Message>)>;
112
113#[protocol]
114pub trait PeerConnectionServerProtocol: Send + Sync {
115    fn incoming_message(&self, message: Message) -> Result<(), ActorError>;
116    fn outgoing_message(&self, message: Message) -> Result<(), ActorError>;
117    fn outgoing_request(
118        &self,
119        message: Message,
120        sender: Arc<oneshot::Sender<Message>>,
121    ) -> Result<(), ActorError>;
122    fn request_timeout(&self, id: u64) -> Result<(), ActorError>;
123    fn send_ping(&self) -> Result<(), ActorError>;
124    fn block_range_update(&self) -> Result<(), ActorError>;
125    fn broadcast_message(&self, task_id: Id, msg: Arc<Message>) -> Result<(), ActorError>;
126    fn sweep_inflight_txs(&self) -> Result<(), ActorError>;
127    fn flush_pending_tx_requests(&self) -> Result<(), ActorError>;
128    fn enqueue_tx_requests(
129        &self,
130        announcement: NewPooledTransactionHashes,
131        hashes: Vec<H256>,
132    ) -> Result<(), ActorError>;
133}
134
135#[cfg(feature = "l2")]
136#[derive(Clone)]
137pub struct L2Message {
138    pub msg: L2Cast,
139}
140
141#[cfg(feature = "l2")]
142impl spawned_concurrency::message::Message for L2Message {
143    type Result = ();
144}
145
146#[derive(Clone, Debug)]
147pub struct PeerConnection {
148    handle: ActorRef<PeerConnectionServer>,
149}
150
151impl PeerConnection {
152    pub fn spawn_as_receiver(
153        context: P2PContext,
154        peer_addr: SocketAddr,
155        stream: TcpStream,
156        admission_permit: tokio::sync::OwnedSemaphorePermit,
157    ) -> PeerConnection {
158        let state = ConnectionState::Receiver(Receiver {
159            context,
160            peer_addr,
161            stream: Arc::new(stream),
162        });
163        let connection = PeerConnectionServer {
164            state,
165            _admission_permit: Some(admission_permit),
166        };
167        Self {
168            handle: connection.start(),
169        }
170    }
171
172    pub fn spawn_as_initiator(context: P2PContext, node: &Node) -> PeerConnection {
173        let state = ConnectionState::Initiator(Initiator {
174            context,
175            node: node.clone(),
176        });
177        // Outbound dials are not admission-capped (we initiate them); inbound is the attack surface.
178        let connection = PeerConnectionServer {
179            state,
180            _admission_permit: None,
181        };
182        Self {
183            handle: connection.start(),
184        }
185    }
186
187    pub async fn outgoing_message(&mut self, message: Message) -> Result<(), PeerConnectionError> {
188        self.handle
189            .outgoing_message(message)
190            .map_err(|err| PeerConnectionError::InternalError(err.to_string()))
191    }
192
193    /// Queue tx hashes (with the originating announcement metadata) to be
194    /// requested on the next flush tick. Used as a fallback when an in-flight
195    /// request on another peer fails.
196    pub fn enqueue_tx_requests(
197        &self,
198        announcement: NewPooledTransactionHashes,
199        hashes: Vec<H256>,
200    ) -> Result<(), PeerConnectionError> {
201        self.handle
202            .enqueue_tx_requests(announcement, hashes)
203            .map_err(|err| PeerConnectionError::InternalError(err.to_string()))
204    }
205
206    pub async fn outgoing_request(
207        &mut self,
208        message: Message,
209        timeout: Duration,
210    ) -> Result<Message, PeerConnectionError> {
211        let id = message
212            .request_id()
213            .expect("Cannot wait on request without id");
214        let (oneshot_tx, oneshot_rx) = oneshot::channel::<Message>();
215
216        self.handle
217            .outgoing_request(message, Arc::new(oneshot_tx))
218            .map_err(|err| PeerConnectionError::InternalError(err.to_string()))?;
219
220        // Wait for the response or timeout. This blocks the calling task (and not the ConnectionServer task)
221        match tokio::time::timeout(timeout, oneshot_rx).await {
222            Ok(Ok(response)) => Ok(response),
223            Ok(Err(error)) => Err(PeerConnectionError::RecvError(error.to_string())),
224            Err(_timeout) => {
225                // Notify timeout on request id
226                self.handle
227                    .request_timeout(id)
228                    .map_err(|err| PeerConnectionError::InternalError(err.to_string()))?;
229                // Return timeout error
230                Err(PeerConnectionError::Timeout)
231            }
232        }
233    }
234}
235
236#[derive(Debug)]
237pub struct Initiator {
238    pub(crate) context: P2PContext,
239    pub(crate) node: Node,
240}
241
242#[derive(Debug)]
243pub struct Receiver {
244    pub(crate) context: P2PContext,
245    pub(crate) peer_addr: SocketAddr,
246    pub(crate) stream: Arc<TcpStream>,
247}
248
249#[derive(Debug)]
250pub struct Established {
251    pub(crate) signer: SecretKey,
252    // Bounded outbound queue to the per-connection writer task (which owns the socket sink).
253    // A bounded queue + dropping the peer on overflow keeps per-connection outbound buffering
254    // bounded instead of letting the actor mailbox grow when a peer can't keep up. The receiving
255    // part of the TcpStream is owned by the stream listen loop task. See `spawn_outbound_writer`.
256    pub(crate) outbound_tx: tokio::sync::mpsc::Sender<Message>,
257    /// Set by the writer task when it exits because a single send exceeded `OUTBOUND_SEND_TIMEOUT`
258    /// (a wedged peer), so `send` can surface `OutboundSendTimeout` once the bounded queue closes.
259    pub(crate) outbound_writer_timed_out: std::sync::Arc<std::sync::atomic::AtomicBool>,
260    pub(crate) node: Node,
261    pub(crate) storage: Store,
262    pub(crate) blockchain: Arc<Blockchain>,
263    pub(crate) capabilities: Vec<Capability>,
264    pub(crate) negotiated_eth_capability: Option<Capability>,
265    pub(crate) negotiated_snap_capability: Option<Capability>,
266    pub(crate) last_block_range_update_block: u64,
267    /// Maps request ID to (original announcement, actually requested hashes, request time).
268    /// The announcement is kept for response validation; the hashes track in-flight state.
269    pub(crate) requested_pooled_txs: HashMap<u64, (NewPooledTransactionHashes, Vec<H256>, Instant)>,
270    /// Buffered transaction requests waiting to be flushed as a single batch.
271    /// Accumulated between flush ticks (TX_REQUEST_BATCH_INTERVAL).
272    pub(crate) pending_tx_requests: Vec<(NewPooledTransactionHashes, Vec<H256>)>,
273    pub(crate) client_version: String,
274    //// Send end of the channel used to broadcast messages
275    //// to other connected peers, is ok to have it here,
276    //// since internally it's an Arc.
277    //// The ID is to ignore the message sent from the same task.
278    //// This is used both to send messages and to received broadcasted
279    //// messages from other connections (sent from other peers).
280    //// The receive end is instantiated after the handshake is completed
281    //// under `handle_peer`.
282    /// TODO: Improve this mechanism
283    /// See https://github.com/lambdaclass/ethrex/issues/3388
284    pub(crate) connection_broadcast_send: PeerConnBroadcastSender,
285    pub(crate) peer_table: PeerTable,
286    #[cfg(feature = "l2")]
287    pub(crate) l2_state: L2ConnState,
288    pub(crate) tx_broadcaster: ActorRef<TxBroadcaster>,
289    pub(crate) current_requests: HashMap<u64, (String, oneshot::Sender<Message>)>,
290    // We store the disconnection reason to handle it in the teardown
291    pub(crate) disconnect_reason: Option<DisconnectReason>,
292    // Indicates if the peer has been validated (ie. the connection was established successfully)
293    pub(crate) is_validated: bool,
294    // Rate limiting: start of the current incoming-request window
295    pub(crate) serve_request_window_start: Instant,
296    // Rate limiting: number of data-serving requests received in the current window
297    pub(crate) serve_requests_in_window: u64,
298    // Leech detection: total transactions sent to this peer via GetPooledTransactions responses
299    pub(crate) txs_sent_to_peer: u64,
300    // Leech detection: whether we have received any transactions from this peer
301    pub(crate) received_txs_from_peer: bool,
302    // Liveness: consecutive pings sent without a matching pong. Reset to 0 on every Pong,
303    // incremented on each Ping we send; at MAX_MISSED_PONGS the peer is considered dead
304    // and dropped (catches application-dead-but-TCP-alive peers that otherwise linger).
305    pub(crate) missed_pongs: u8,
306}
307
308impl Established {
309    async fn teardown(&mut self) {
310        // Clear any in-flight transaction hashes so other connections can re-request them,
311        // then try to re-issue each pending request to an alternate announcer.
312        // Order matters: clear first so the alternate's reserve_unknown_hashes sees the
313        // hashes as free; otherwise the actor handler can race with clear_in_flight_txs
314        // and silently no-op the retry while consuming an alternates slot.
315        for (_, (_announced, requested_hashes, _)) in self.requested_pooled_txs.drain() {
316            if let Err(e) = self
317                .blockchain
318                .mempool
319                .clear_in_flight_txs(&requested_hashes)
320            {
321                warn!(error = %e, "Failed to clear in-flight transaction tracking during peer teardown");
322            }
323            retry_on_alternates(&self.blockchain, &self.peer_table, &requested_hashes).await;
324        }
325        // Also clear hashes that were buffered but not yet sent.
326        for (_announced, pending_hashes) in self.pending_tx_requests.drain(..) {
327            if let Err(e) = self.blockchain.mempool.clear_in_flight_txs(&pending_hashes) {
328                warn!(error = %e, "Failed to clear in-flight transaction tracking during peer teardown");
329            }
330            retry_on_alternates(&self.blockchain, &self.peer_table, &pending_hashes).await;
331        }
332        // The socket sink is owned by the per-connection writer task (`spawn_outbound_writer`),
333        // which closes it when this `Established` is dropped (its `outbound_tx` is the only sender).
334    }
335}
336
337#[derive(Debug)]
338pub enum ConnectionState {
339    HandshakeFailed,
340    Initiator(Initiator),
341    Receiver(Receiver),
342    Established(Box<Established>),
343}
344
345#[derive(Debug)]
346pub struct PeerConnectionServer {
347    state: ConnectionState,
348    /// Inbound-admission permit (Some for inbound connections, None for outbound dials we
349    /// initiate). Held for the actor's lifetime and released when the actor is dropped, so the
350    /// inbound connection count stays bounded. See `P2PContext::inbound_admission`.
351    _admission_permit: Option<tokio::sync::OwnedSemaphorePermit>,
352}
353
354#[actor(protocol = PeerConnectionServerProtocol)]
355impl PeerConnectionServer {
356    #[started]
357    async fn started(&mut self, ctx: &Context<Self>) {
358        // Set a default eth version that we can update after we negotiate peer capabilities
359        // This eth version will only be used to encode & decode the initial `Hello` messages.
360        let eth_version = Arc::new(RwLock::new(EthCapVersion::default()));
361        // Take ownership of the state, replacing with HandshakeFailed as placeholder
362        let state = std::mem::replace(&mut self.state, ConnectionState::HandshakeFailed);
363        // Bound the handshake: a peer that opens TCP and then stalls must not park this
364        // actor + socket forever (otherwise established sockets accumulate above the peer
365        // target). On timeout the handshake future is dropped, closing the socket.
366        let handshake_result = match tokio::time::timeout(
367            HANDSHAKE_TIMEOUT,
368            handshake::perform(state, eth_version.clone()),
369        )
370        .await
371        {
372            Ok(result) => result,
373            Err(_elapsed) => {
374                debug!("Handshake timed out on RLPx connection");
375                self.state = ConnectionState::HandshakeFailed;
376                ctx.stop();
377                return;
378            }
379        };
380        match handshake_result {
381            Ok((mut established_state, stream)) => {
382                trace!(peer=%established_state.node, "Starting RLPx connection");
383                if let Err(reason) =
384                    initialize_connection(ctx, &mut established_state, stream, eth_version).await
385                {
386                    match &reason {
387                        PeerConnectionError::NoMatchingCapabilities
388                        | PeerConnectionError::HandshakeError(_) => {
389                            if let Err(e) = established_state
390                                .peer_table
391                                .set_unwanted(established_state.node.node_id())
392                            {
393                                debug!("Failed to set peer as unwanted: {e}");
394                            }
395                        }
396                        _ => {}
397                    }
398                    connection_failed(
399                        &mut established_state,
400                        "Failed to initialize RLPx connection",
401                        &reason,
402                    )
403                    .await;
404
405                    METRICS.record_new_rlpx_conn_failure(reason).await;
406
407                    self.state = ConnectionState::Established(Box::new(established_state));
408                    ctx.stop();
409                } else {
410                    METRICS
411                        .record_new_rlpx_conn_established(
412                            &established_state
413                                .node
414                                .version
415                                .clone()
416                                .unwrap_or("Unknown".to_string()),
417                        )
418                        .await;
419                    established_state.is_validated = true;
420                    // New state
421                    self.state = ConnectionState::Established(Box::new(established_state));
422                }
423            }
424            Err(err) => {
425                // Handshake failed, just log a debug message.
426                // No connection was established so no need to perform any other action
427                debug!("Failed Handshake on RLPx connection {err}");
428                self.state = ConnectionState::HandshakeFailed;
429                ctx.stop();
430            }
431        }
432    }
433
434    #[stopped]
435    async fn stopped(&mut self, _ctx: &Context<Self>) {
436        match std::mem::replace(&mut self.state, ConnectionState::HandshakeFailed) {
437            ConnectionState::Established(mut established_state) => {
438                trace!(peer=%established_state.node, "Closing connection with established peer");
439                if established_state.is_validated {
440                    // If its validated the peer was connected, so we record the disconnection.
441                    let reason = established_state
442                        .disconnect_reason
443                        .unwrap_or(DisconnectReason::NetworkError);
444                    METRICS
445                        .record_new_rlpx_conn_disconnection(
446                            &established_state
447                                .node
448                                .version
449                                .clone()
450                                .unwrap_or("Unknown".to_string()),
451                            reason,
452                        )
453                        .await;
454                }
455                if let Err(e) = established_state
456                    .peer_table
457                    .remove_peer(established_state.node.node_id())
458                {
459                    debug!("Failed to remove peer from table: {e}");
460                }
461                // Free the peer's tx-broadcaster index (and clear its bit across known txs) so
462                // the broadcaster's per-peer index map / PeerMask widths stay bounded to live peers.
463                if let Err(e) = established_state
464                    .tx_broadcaster
465                    .remove_peer(established_state.node.node_id())
466                {
467                    debug!("Failed to remove peer from tx broadcaster: {e}");
468                }
469                established_state.teardown().await;
470            }
471            _ => {
472                // Nothing to do if the connection was not established
473            }
474        };
475    }
476
477    #[send_handler]
478    async fn handle_incoming_message(
479        &mut self,
480        msg: peer_connection_server_protocol::IncomingMessage,
481        ctx: &Context<Self>,
482    ) {
483        if let ConnectionState::Established(ref mut established_state) = self.state {
484            trace!(
485                peer=%established_state.node,
486                message=%msg.message,
487                "Received incoming message",
488            );
489            let result = handle_incoming_message(established_state, msg.message).await;
490            Self::process_cast_error(&self.state, result, ctx);
491        } else {
492            debug!("Connection not yet established");
493        }
494    }
495
496    #[send_handler]
497    async fn handle_outgoing_message(
498        &mut self,
499        msg: peer_connection_server_protocol::OutgoingMessage,
500        ctx: &Context<Self>,
501    ) {
502        if let ConnectionState::Established(ref mut established_state) = self.state {
503            trace!(
504                peer=%established_state.node,
505                message=%msg.message,
506                "Received outgoing request",
507            );
508            let result = handle_outgoing_message(established_state, msg.message).await;
509            Self::process_cast_error(&self.state, result, ctx);
510        } else {
511            debug!("Connection not yet established");
512        }
513    }
514
515    #[send_handler]
516    async fn handle_outgoing_request(
517        &mut self,
518        msg: peer_connection_server_protocol::OutgoingRequest,
519        ctx: &Context<Self>,
520    ) {
521        if let ConnectionState::Established(ref mut established_state) = self.state {
522            trace!(
523                peer=%established_state.node,
524                message=%msg.message,
525                "Received outgoing request",
526            );
527            let Some(sender) = Arc::<oneshot::Sender<Message>>::into_inner(msg.sender) else {
528                debug!("Could not obtain sender channel: Arc has multiple references");
529                return;
530            };
531            let result = handle_outgoing_request(established_state, msg.message, sender).await;
532            Self::process_cast_error(&self.state, result, ctx);
533        } else {
534            debug!("Connection not yet established");
535        }
536    }
537
538    #[send_handler]
539    async fn handle_request_timeout(
540        &mut self,
541        msg: peer_connection_server_protocol::RequestTimeout,
542        _ctx: &Context<Self>,
543    ) {
544        if let ConnectionState::Established(ref mut established_state) = self.state {
545            // Discard the request from current requests
546            if let Some((msg_type, _)) = established_state.current_requests.remove(&msg.id) {
547                debug!(
548                    peer=%established_state.node,
549                    %msg_type,
550                    id=%msg.id,
551                    "Request timedout",
552                );
553            }
554        } else {
555            debug!("Connection not yet established");
556        }
557    }
558
559    #[send_handler]
560    async fn handle_send_ping(
561        &mut self,
562        _msg: peer_connection_server_protocol::SendPing,
563        ctx: &Context<Self>,
564    ) {
565        if let ConnectionState::Established(ref mut established_state) = self.state {
566            // Liveness: if the peer hasn't answered the last MAX_MISSED_PONGS pings, treat it
567            // as dead and drop it instead of pinging a corpse (and keeping its actor) forever.
568            if established_state.missed_pongs >= MAX_MISSED_PONGS {
569                debug!(peer=%established_state.node, missed=MAX_MISSED_PONGS, "Peer missed max pongs, dropping");
570                // Attribute the drop as `PingTimeout` so an unresponsive-peer drop is
571                // distinguishable from a generic `NetworkError` in `ethrex_p2p_disconnections`.
572                established_state.disconnect_reason = Some(DisconnectReason::PingTimeout);
573                ctx.stop();
574                return;
575            }
576            established_state.missed_pongs = established_state.missed_pongs.saturating_add(1);
577            let result = send(established_state, Message::Ping(PingMessage {})).await;
578            Self::process_cast_error(&self.state, result, ctx);
579        } else {
580            debug!("Connection not yet established");
581        }
582    }
583
584    #[send_handler]
585    async fn handle_block_range_update(
586        &mut self,
587        _msg: peer_connection_server_protocol::BlockRangeUpdate,
588        ctx: &Context<Self>,
589    ) {
590        if let ConnectionState::Established(ref mut established_state) = self.state {
591            trace!(
592                peer=%established_state.node,
593                "Block Range Update"
594            );
595            let result = handle_block_range_update(established_state).await;
596            Self::process_cast_error(&self.state, result, ctx);
597        } else {
598            debug!("Connection not yet established");
599        }
600    }
601
602    #[send_handler]
603    async fn handle_sweep_inflight_txs(
604        &mut self,
605        _msg: peer_connection_server_protocol::SweepInflightTxs,
606        _ctx: &Context<Self>,
607    ) {
608        if let ConnectionState::Established(ref mut state) = self.state {
609            let now = Instant::now();
610            let stale_ids: Vec<u64> = state
611                .requested_pooled_txs
612                .iter()
613                .filter(|(_, (_, _, ts))| now.duration_since(*ts) > INFLIGHT_TX_TIMEOUT)
614                .map(|(id, _)| *id)
615                .collect();
616            for id in stale_ids {
617                if let Some((_announced, hashes, _)) = state.requested_pooled_txs.remove(&id) {
618                    // Clear in-flight before retry so the alternate's reserve_unknown_hashes
619                    // doesn't race against still-in-flight state and silently no-op.
620                    if let Err(e) = state.blockchain.mempool.clear_in_flight_txs(&hashes) {
621                        warn!(error = %e, "Failed to clear in-flight transaction tracking while sweeping stale requests");
622                    }
623                    retry_on_alternates(&state.blockchain, &state.peer_table, &hashes).await;
624                }
625            }
626        }
627    }
628
629    #[send_handler]
630    async fn handle_flush_pending_tx_requests(
631        &mut self,
632        _msg: peer_connection_server_protocol::FlushPendingTxRequests,
633        ctx: &Context<Self>,
634    ) {
635        if let ConnectionState::Established(ref mut established_state) = self.state {
636            let result = flush_pending_tx_requests(established_state).await;
637            Self::process_cast_error(&self.state, result, ctx);
638        }
639    }
640
641    #[send_handler]
642    async fn handle_enqueue_tx_requests(
643        &mut self,
644        msg: peer_connection_server_protocol::EnqueueTxRequests,
645        _ctx: &Context<Self>,
646    ) {
647        if let ConnectionState::Established(ref mut state) = self.state {
648            // Re-reserve in-flight against this peer. If any hashes are already
649            // in-flight (race), drop them; we don't want duplicate requests.
650            let to_request: Vec<H256> = match state.blockchain.mempool.reserve_unknown_hashes(
651                &msg.announcement.transaction_hashes,
652                &msg.announcement.transaction_types,
653                &msg.announcement.transaction_sizes,
654                state.node.node_id(),
655            ) {
656                Ok(unknown) => unknown,
657                Err(_) => return,
658            };
659            if to_request.is_empty() {
660                return;
661            }
662            let trimmed = msg.announcement.filter_to(&to_request);
663            state.pending_tx_requests.push((trimmed, to_request));
664        }
665    }
666
667    #[send_handler]
668    async fn handle_broadcast_message(
669        &mut self,
670        msg: peer_connection_server_protocol::BroadcastMessage,
671        ctx: &Context<Self>,
672    ) {
673        if let ConnectionState::Established(ref mut established_state) = self.state {
674            trace!(
675                peer=%established_state.node,
676                message=%msg.msg,
677                "Received broadcasted message",
678            );
679            let result = handle_broadcast(established_state, (msg.task_id, msg.msg)).await;
680            Self::process_cast_error(&self.state, result, ctx);
681        } else {
682            debug!("Connection not yet established");
683        }
684    }
685
686    #[cfg(feature = "l2")]
687    #[send_handler]
688    async fn handle_l2_message(&mut self, msg: L2Message, ctx: &Context<Self>) {
689        if let ConnectionState::Established(ref mut established_state) = self.state {
690            let peer_supports_l2 = established_state.l2_state.connection_state().is_ok();
691            let result = if peer_supports_l2 {
692                trace!(
693                    peer=%established_state.node,
694                    message=?msg.msg,
695                    "Handling cast for L2 msg"
696                );
697                match msg.msg {
698                    L2Cast::BatchBroadcast => {
699                        let res = l2_connection::send_sealed_batch(established_state).await;
700                        res.and(l2_connection::process_batches_on_queue(established_state).await)
701                    }
702                    L2Cast::BlockBroadcast => {
703                        let res = l2_connection::send_new_block(established_state).await;
704                        res.and(l2_connection::process_blocks_on_queue(established_state).await)
705                    }
706                }
707            } else {
708                Err(PeerConnectionError::MessageNotHandled(
709                    "Unknown message or capability not handled".to_string(),
710                ))
711            };
712            Self::process_cast_error(&self.state, result, ctx);
713        } else {
714            debug!("Connection not yet established");
715        }
716    }
717
718    fn process_cast_error(
719        state: &ConnectionState,
720        result: Result<(), PeerConnectionError>,
721        ctx: &Context<Self>,
722    ) {
723        if let Err(e) = result
724            && let ConnectionState::Established(established_state) = state
725        {
726            match e {
727                PeerConnectionError::Disconnected
728                | PeerConnectionError::DisconnectReceived(_)
729                | PeerConnectionError::DisconnectSent(_)
730                | PeerConnectionError::HandshakeError(_)
731                | PeerConnectionError::NoMatchingCapabilities
732                | PeerConnectionError::InvalidPeerId
733                | PeerConnectionError::InvalidMessageLength
734                | PeerConnectionError::StateError(_)
735                | PeerConnectionError::InvalidRecoveryId
736                | PeerConnectionError::OutboundSendTimeout
737                | PeerConnectionError::OutboundQueueFull => {
738                    trace!(peer=%established_state.node, error=e.to_string(), "Peer connection error");
739                    ctx.stop();
740                }
741                PeerConnectionError::IoError(ref io_e)
742                    if io_e.kind() == std::io::ErrorKind::BrokenPipe =>
743                {
744                    // TODO: we need to check if this message is ocurring commonly due to a problem
745                    // with our concurrency model
746                    debug!(peer=%established_state.node, "Broken pipe with peer, disconnected");
747                    ctx.stop();
748                }
749                PeerConnectionError::StoreError(StoreError::Trie(TrieError::InconsistentTree(
750                    _,
751                ))) => {
752                    if established_state.blockchain.is_synced() {
753                        // If we're responding with inconsistent trie while synced, our trie may be broken
754                        // If this error is non sporadic we should investigate
755                        error!(
756                            peer=%established_state.node,
757                            error=%e,
758                            "Inconsistent trie while serving peer request; local state may be corrupted",
759                        );
760                    } else {
761                        // If we're not synced, we expect to have inconsistent trie errors
762                        trace!(
763                            peer=%established_state.node,
764                            error=%e,
765                            "Error handling cast message",
766                        );
767                    }
768                }
769                _ => {
770                    // We should check why we're failling to handle the cast message
771                    debug!(
772                        peer=%established_state.node,
773                        capabilities=?established_state.capabilities,
774                        error=%e,
775                        "Error handling cast message",
776                    );
777                }
778            }
779        }
780    }
781}
782
783async fn initialize_connection<S>(
784    ctx: &Context<PeerConnectionServer>,
785    state: &mut Established,
786    mut stream: S,
787    eth_version: Arc<RwLock<EthCapVersion>>,
788) -> Result<(), PeerConnectionError>
789where
790    S: Unpin + Send + Stream<Item = Result<Message, PeerConnectionError>> + 'static,
791{
792    if state.peer_table.target_peers_reached().await? {
793        debug!(peer=%state.node, "Reached target peer connections, discarding.");
794        return Err(PeerConnectionError::TooManyPeers);
795    }
796    exchange_hello_messages(state, &mut stream).await?;
797
798    // Update eth capability version to the negotiated version for further message decoding
799    let version = match &state.negotiated_eth_capability {
800        Some(cap) if cap == &Capability::eth(68) => EthCapVersion::V68,
801        Some(cap) if cap == &Capability::eth(69) => EthCapVersion::V69,
802        Some(cap) if cap == &Capability::eth(70) => EthCapVersion::V70,
803        Some(cap) if cap == &Capability::eth(71) => EthCapVersion::V71,
804        _ => EthCapVersion::default(),
805    };
806    *eth_version
807        .write()
808        .map_err(|err| PeerConnectionError::InternalError(err.to_string()))? = version;
809
810    init_capabilities(state, &mut stream).await?;
811
812    let mut connection = PeerConnection {
813        handle: ctx.actor_ref(),
814    };
815
816    state.peer_table.new_connected_peer(
817        state.node.clone(),
818        connection.clone(),
819        state.capabilities.clone(),
820    )?;
821
822    trace!(peer=%state.node, "Peer connection initialized.");
823
824    // Send transactions transaction hashes from mempool at connection start
825    send_all_pooled_tx_hashes(state, &mut connection).await?;
826
827    // Periodic Pings repeated events.
828    send_interval(
829        PING_INTERVAL,
830        ctx.clone(),
831        peer_connection_server_protocol::SendPing,
832    );
833
834    // Periodic block range update.
835    send_interval(
836        BLOCK_RANGE_UPDATE_INTERVAL,
837        ctx.clone(),
838        peer_connection_server_protocol::BlockRangeUpdate,
839    );
840
841    // Periodic sweep of stale in-flight transaction requests.
842    send_interval(
843        INFLIGHT_TX_SWEEP_INTERVAL,
844        ctx.clone(),
845        peer_connection_server_protocol::SweepInflightTxs,
846    );
847
848    // Periodic flush of buffered transaction requests.
849    send_interval(
850        TX_REQUEST_BATCH_INTERVAL,
851        ctx.clone(),
852        peer_connection_server_protocol::FlushPendingTxRequests,
853    );
854
855    #[cfg(feature = "l2")]
856    // Periodic L2 messages events.
857    if state.l2_state.connection_state().is_ok() {
858        send_interval(
859            PERIODIC_BLOCK_BROADCAST_INTERVAL,
860            ctx.clone(),
861            L2Message {
862                msg: L2Cast::BlockBroadcast,
863            },
864        );
865        send_interval(
866            PERIODIC_BATCH_BROADCAST_INTERVAL,
867            ctx.clone(),
868            L2Message {
869                msg: L2Cast::BatchBroadcast,
870            },
871        );
872    }
873
874    spawn_listener(
875        ctx.clone(),
876        stream.filter_map(|result| match result {
877            Ok(msg) => Some(peer_connection_server_protocol::IncomingMessage { message: msg }),
878            Err(e) => {
879                debug!(error=?e, "Error receiving RLPx message");
880                // Skipping invalid data
881                None
882            }
883        }),
884    );
885
886    if state.negotiated_eth_capability.is_some() {
887        let stream: BroadcastStream<(Id, Arc<Message>)> =
888            BroadcastStream::new(state.connection_broadcast_send.subscribe());
889        let message_stream = stream.filter_map(|result| {
890            result.ok().map(
891                |(id, msg)| peer_connection_server_protocol::BroadcastMessage { task_id: id, msg },
892            )
893        });
894        spawn_listener(ctx.clone(), message_stream);
895    }
896
897    Ok(())
898}
899
900async fn send_all_pooled_tx_hashes(
901    state: &mut Established,
902    connection: &mut PeerConnection,
903) -> Result<(), PeerConnectionError> {
904    let txs: Vec<MempoolTransaction> = state
905        .blockchain
906        .mempool
907        .get_all_txs_by_sender()?
908        .into_values()
909        .flatten()
910        .filter(|tx| !tx.is_privileged())
911        .collect();
912    if !txs.is_empty() {
913        state
914            .tx_broadcaster
915            .add_txs(
916                txs.iter().map(|tx| tx.hash(&NativeCrypto)).collect(),
917                state.node.node_id(),
918            )
919            .map_err(|e| PeerConnectionError::BroadcastError(e.to_string()))?;
920        send_tx_hashes(
921            txs,
922            state.capabilities.clone(),
923            connection,
924            state.node.node_id(),
925            &state.blockchain,
926        )
927        .await
928        .map_err(|e| PeerConnectionError::SendMessage(e.to_string()))?;
929    }
930    Ok(())
931}
932
933async fn send_block_range_update(state: &mut Established) -> Result<(), PeerConnectionError> {
934    // BlockRangeUpdate was introduced in eth/69
935    if state
936        .negotiated_eth_capability
937        .as_ref()
938        .is_some_and(|eth| eth.version >= 69)
939    {
940        trace!(peer=%state.node, "Sending BlockRangeUpdate");
941        let update = BlockRangeUpdate::new(&state.storage).await?;
942        let lastet_block = update.latest_block;
943        send(state, Message::BlockRangeUpdate(update)).await?;
944        state.last_block_range_update_block = lastet_block - (lastet_block % 32);
945    }
946    Ok(())
947}
948
949async fn should_send_block_range_update(state: &Established) -> Result<bool, PeerConnectionError> {
950    let latest_block = state.storage.get_latest_block_number().await?;
951    if latest_block < state.last_block_range_update_block
952        || latest_block - state.last_block_range_update_block >= 32
953    {
954        return Ok(true);
955    }
956    Ok(false)
957}
958
959async fn init_capabilities<S>(
960    state: &mut Established,
961    stream: &mut S,
962) -> Result<(), PeerConnectionError>
963where
964    S: Unpin + Stream<Item = Result<Message, PeerConnectionError>>,
965{
966    // Sending eth Status if peer supports it
967    if let Some(eth) = state.negotiated_eth_capability.clone() {
968        let status = match eth.version {
969            68 => Message::Status68(StatusMessage68::new(&state.storage).await?),
970            69 => Message::Status69(StatusMessage69::new(&state.storage).await?),
971            70 => Message::Status70(StatusMessage70::new(&state.storage).await?),
972            71 => Message::Status71(StatusMessage71::new(&state.storage).await?),
973            ver => {
974                return Err(PeerConnectionError::HandshakeError(format!(
975                    "Invalid eth version {ver}"
976                )));
977            }
978        };
979        trace!(peer=%state.node, "Sending status");
980        send(state, status).await?;
981        // The next immediate message in the ETH protocol is the
982        // status, reference here:
983        // https://github.com/ethereum/devp2p/blob/master/caps/eth.md#status-0x00
984        let msg = match receive(stream).await {
985            Some(msg) => msg?,
986            None => return Err(PeerConnectionError::Disconnected),
987        };
988        match msg {
989            Message::Status68(msg_data) => {
990                trace!(peer=%state.node, "Received Status(68)");
991                backend::validate_status(msg_data, &state.storage, &eth).await?
992            }
993            Message::Status69(msg_data) => {
994                trace!(peer=%state.node, "Received Status(69)");
995                backend::validate_status(msg_data, &state.storage, &eth).await?
996            }
997            Message::Status70(msg_data) => {
998                trace!(peer=%state.node, "Received Status(70)");
999                backend::validate_status(msg_data, &state.storage, &eth).await?
1000            }
1001            Message::Status71(msg_data) => {
1002                trace!(peer=%state.node, "Received Status(71)");
1003                backend::validate_status(msg_data, &state.storage, &eth).await?
1004            }
1005            Message::Disconnect(disconnect) => {
1006                return Err(PeerConnectionError::HandshakeError(format!(
1007                    "Peer disconnected due to: {}",
1008                    disconnect.reason()
1009                )));
1010            }
1011            _ => {
1012                return Err(PeerConnectionError::HandshakeError(
1013                    "Expected a Status message".to_string(),
1014                ));
1015            }
1016        }
1017    }
1018    Ok(())
1019}
1020
1021async fn send_disconnect_message(state: &mut Established, reason: Option<DisconnectReason>) {
1022    send(state, Message::Disconnect(DisconnectMessage { reason }))
1023        .await
1024        .unwrap_or_else(|_| {
1025            debug!(
1026                peer=%state.node,
1027                ?reason,
1028                "Could not send Disconnect message",
1029            );
1030        });
1031}
1032
1033async fn connection_failed(state: &mut Established, error_text: &str, error: &PeerConnectionError) {
1034    debug!(
1035        peer=%state.node,
1036        %error_text,
1037        %error,
1038        "connection failure"
1039    );
1040
1041    // Send disconnect message only if error is different than RLPxError::DisconnectRequested
1042    // because if it is a DisconnectRequested error it means that the peer requested the disconnection, not us.
1043    if !matches!(error, PeerConnectionError::DisconnectReceived(_)) {
1044        send_disconnect_message(state, match_disconnect_reason(error)).await;
1045    }
1046
1047    // Discard peer from kademlia table in some cases
1048    match error {
1049        // already connected, don't discard it
1050        PeerConnectionError::DisconnectReceived(DisconnectReason::AlreadyConnected)
1051        | PeerConnectionError::DisconnectSent(DisconnectReason::AlreadyConnected) => {
1052            debug!(
1053                peer=%state.node,
1054                %error_text,
1055                %error,
1056                "Peer already connected, don't replace it"
1057            );
1058        }
1059        _ => {
1060            debug!(
1061                peer=%state.node,
1062                %error_text,
1063                %error,
1064                remote_public_key=%state.node.public_key,
1065                "discarding peer",
1066            );
1067        }
1068    }
1069}
1070
1071fn match_disconnect_reason(error: &PeerConnectionError) -> Option<DisconnectReason> {
1072    match error {
1073        PeerConnectionError::DisconnectSent(reason) => Some(*reason),
1074        PeerConnectionError::DisconnectReceived(reason) => Some(*reason),
1075        PeerConnectionError::RLPDecodeError(_) => Some(DisconnectReason::NetworkError),
1076        PeerConnectionError::TooManyPeers => Some(DisconnectReason::TooManyPeers),
1077        // TODO build a proper matching between error types and disconnection reasons
1078        _ => None,
1079    }
1080}
1081
1082async fn exchange_hello_messages<S>(
1083    state: &mut Established,
1084    stream: &mut S,
1085) -> Result<(), PeerConnectionError>
1086where
1087    S: Unpin + Stream<Item = Result<Message, PeerConnectionError>>,
1088{
1089    // This allow is because in l2 we mut the capabilities
1090    // to include the l2 cap
1091    #[allow(unused_mut)]
1092    let mut supported_capabilities: Vec<Capability> = [
1093        &SUPPORTED_ETH_CAPABILITIES[..],
1094        &SUPPORTED_SNAP_CAPABILITIES[..],
1095    ]
1096    .concat();
1097    #[cfg(feature = "l2")]
1098    if state.l2_state.is_supported() {
1099        supported_capabilities.push(crate::rlpx::l2::SUPPORTED_BASED_CAPABILITIES[0].clone());
1100    }
1101    let hello_msg = Message::Hello(p2p::HelloMessage::new(
1102        supported_capabilities,
1103        PublicKey::from_secret_key(secp256k1::SECP256K1, &state.signer),
1104        state.client_version.clone(),
1105    ));
1106
1107    send(state, hello_msg).await?;
1108
1109    // Receive Hello message
1110    let msg = match receive(stream).await {
1111        Some(msg) => msg?,
1112        None => return Err(PeerConnectionError::Disconnected),
1113    };
1114
1115    match msg {
1116        Message::Hello(hello_message) => {
1117            let mut negotiated_eth_version = 0;
1118            let mut negotiated_snap_version = 0;
1119
1120            trace!(
1121                peer=%state.node,
1122                capabilities=?hello_message.capabilities,
1123                "Hello message capabilities",
1124            );
1125
1126            // Check if we have any capability in common and store the highest version
1127            for cap in &hello_message.capabilities {
1128                match cap.protocol() {
1129                    "eth" => {
1130                        if SUPPORTED_ETH_CAPABILITIES.contains(cap)
1131                            && cap.version > negotiated_eth_version
1132                        {
1133                            negotiated_eth_version = cap.version;
1134                        }
1135                    }
1136                    "snap" => {
1137                        if SUPPORTED_SNAP_CAPABILITIES.contains(cap)
1138                            && cap.version > negotiated_snap_version
1139                        {
1140                            negotiated_snap_version = cap.version;
1141                        }
1142                    }
1143                    #[cfg(feature = "l2")]
1144                    "based" if state.l2_state.is_supported() => {
1145                        state.l2_state.set_established()?;
1146                    }
1147                    _ => {}
1148                }
1149            }
1150
1151            state.capabilities = hello_message.capabilities;
1152
1153            if negotiated_eth_version == 0 {
1154                return Err(PeerConnectionError::NoMatchingCapabilities);
1155            }
1156            debug!("Negotiated eth version: eth/{}", negotiated_eth_version);
1157            state.negotiated_eth_capability = Some(Capability::eth(negotiated_eth_version));
1158
1159            if negotiated_snap_version != 0 {
1160                debug!("Negotiated snap version: snap/{}", negotiated_snap_version);
1161                state.negotiated_snap_capability = Some(Capability::snap(negotiated_snap_version));
1162            }
1163
1164            state.node.version = Some(hello_message.client_id);
1165
1166            Ok(())
1167        }
1168        Message::Disconnect(disconnect) => {
1169            Err(PeerConnectionError::DisconnectReceived(disconnect.reason()))
1170        }
1171        _ => {
1172            // Fail if it is not a hello message
1173            Err(PeerConnectionError::BadRequest(
1174                "Expected Hello message".to_string(),
1175            ))
1176        }
1177    }
1178}
1179
1180pub(crate) async fn send(
1181    state: &mut Established,
1182    message: Message,
1183) -> Result<(), PeerConnectionError> {
1184    #[cfg(feature = "metrics")]
1185    {
1186        use ethrex_metrics::p2p::METRICS_P2P;
1187        METRICS_P2P.inc_outgoing_message(message.metric_label());
1188    }
1189    // Hand off to the per-connection writer task via a BOUNDED queue (non-blocking). This
1190    // decouples the actor's serial drain from the network write, so a slow/wedged peer can
1191    // never back up the (unbounded) actor mailbox. If the bounded queue is full the peer
1192    // can't keep up: surface OutboundQueueFull so `process_cast_error` drops it.
1193    match state.outbound_tx.try_send(message) {
1194        Ok(()) => Ok(()),
1195        Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
1196            // The peer can't drain our outbound fast enough. Attribute the drop as `UselessPeer`
1197            // so it is distinguishable from a generic `NetworkError` in `ethrex_p2p_disconnections`
1198            // (a spike here flags slow peers or our own over-sending, rather than hiding it).
1199            state
1200                .disconnect_reason
1201                .get_or_insert(DisconnectReason::UselessPeer);
1202            Err(PeerConnectionError::OutboundQueueFull)
1203        }
1204        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
1205            // The writer task is gone. If it exited because a single send exceeded
1206            // `OUTBOUND_SEND_TIMEOUT`, the peer was wedged: surface `OutboundSendTimeout` and
1207            // attribute it likewise; otherwise the socket simply closed (`Disconnected`).
1208            if state
1209                .outbound_writer_timed_out
1210                .load(std::sync::atomic::Ordering::Acquire)
1211            {
1212                state
1213                    .disconnect_reason
1214                    .get_or_insert(DisconnectReason::UselessPeer);
1215                Err(PeerConnectionError::OutboundSendTimeout)
1216            } else {
1217                Err(PeerConnectionError::Disconnected)
1218            }
1219        }
1220    }
1221}
1222
1223/// Spawns the per-connection writer task that owns the `sink` and drains a bounded outbound
1224/// queue into it. Keeping the network write off the actor thread means the actor's mailbox is
1225/// never gated on a slow peer; the only outbound buffer is this bounded channel (capacity
1226/// `OUTBOUND_QUEUE_CAP`). The task exits — closing the socket — when the connection is dropped
1227/// (all senders gone), when a send errors, or when a single send exceeds `OUTBOUND_SEND_TIMEOUT`
1228/// (a wedged peer). Once it exits, further `send()`s observe a closed queue and the peer is dropped.
1229pub(crate) fn spawn_outbound_writer(
1230    mut sink: SplitSink<Framed<TcpStream, RLPxCodec>, Message>,
1231) -> (
1232    tokio::sync::mpsc::Sender<Message>,
1233    std::sync::Arc<std::sync::atomic::AtomicBool>,
1234) {
1235    let (tx, mut rx) = tokio::sync::mpsc::channel::<Message>(OUTBOUND_QUEUE_CAP);
1236    // Set when the writer exits because a single send exceeded `OUTBOUND_SEND_TIMEOUT` (a wedged
1237    // peer), so `send` can surface `OutboundSendTimeout` instead of a generic `Disconnected` once
1238    // the bounded queue closes.
1239    let timed_out = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1240    let writer_timed_out = timed_out.clone();
1241    tokio::spawn(async move {
1242        while let Some(message) = rx.recv().await {
1243            match tokio::time::timeout(OUTBOUND_SEND_TIMEOUT, sink.send(message)).await {
1244                Ok(Ok(())) => {}
1245                // Send error: the socket is gone; let the closed queue surface as `Disconnected`.
1246                Ok(Err(_)) => break,
1247                // The send itself timed out: the peer is wedged. Flag it so the drop is attributed.
1248                Err(_) => {
1249                    writer_timed_out.store(true, std::sync::atomic::Ordering::Release);
1250                    break;
1251                }
1252            }
1253        }
1254        let _ = sink.close().await;
1255    });
1256    (tx, timed_out)
1257}
1258
1259/// Reads from the frame until a frame is available.
1260///
1261/// Returns `None` when the stream buffer is 0. This could indicate that the client has disconnected,
1262/// but we cannot safely assume an EOF, as per the Tokio documentation.
1263///
1264/// If the handshake has not been established, it is reasonable to terminate the connection.
1265///
1266/// For an established connection, [`check_periodic_task`] will detect actual disconnections
1267/// while sending pings and you should not assume a disconnection.
1268///
1269/// See [`Framed::new`] for more details.
1270async fn receive<S>(stream: &mut S) -> Option<Result<Message, PeerConnectionError>>
1271where
1272    S: Unpin + Stream<Item = Result<Message, PeerConnectionError>>,
1273{
1274    stream.next().await
1275}
1276
1277/// Returns true if the peer is within its rate limit for data-serving requests, false if exceeded.
1278/// Resets the window counter when the window duration has elapsed.
1279fn check_serve_request_rate(state: &mut Established) -> bool {
1280    let now = Instant::now();
1281    if now.duration_since(state.serve_request_window_start) >= SERVE_REQUEST_WINDOW {
1282        state.serve_request_window_start = now;
1283        state.serve_requests_in_window = 0;
1284    }
1285    state.serve_requests_in_window += 1;
1286    state.serve_requests_in_window <= MAX_SERVE_REQUESTS_PER_WINDOW
1287}
1288
1289async fn handle_incoming_message(
1290    state: &mut Established,
1291    message: Message,
1292) -> Result<(), PeerConnectionError> {
1293    #[cfg(feature = "metrics")]
1294    {
1295        use ethrex_metrics::p2p::METRICS_P2P;
1296        METRICS_P2P.inc_incoming_message(message.metric_label());
1297    }
1298
1299    // Rate-limit incoming data-serving requests to prevent resource exhaustion.
1300    let is_data_request = matches!(
1301        message,
1302        Message::GetBlockHeaders(_)
1303            | Message::GetBlockBodies(_)
1304            | Message::GetReceipts68(_)
1305            | Message::GetReceipts69(_)
1306            | Message::GetReceipts70(_)
1307            | Message::GetPooledTransactions(_)
1308            | Message::GetAccountRange(_)
1309            | Message::GetStorageRanges(_)
1310            | Message::GetByteCodes(_)
1311            | Message::GetTrieNodes(_)
1312    );
1313    if is_data_request && !check_serve_request_rate(state) {
1314        debug!(
1315            peer = %state.node,
1316            window_requests = state.serve_requests_in_window,
1317            "Disconnecting peer: exceeded incoming request rate limit",
1318        );
1319        send_disconnect_message(state, Some(DisconnectReason::UselessPeer)).await;
1320        return Err(PeerConnectionError::DisconnectSent(
1321            DisconnectReason::UselessPeer,
1322        ));
1323    }
1324
1325    let peer_supports_eth = state.negotiated_eth_capability.is_some();
1326    #[cfg(feature = "l2")]
1327    let peer_supports_l2 = state.l2_state.connection_state().is_ok();
1328    match message {
1329        Message::Disconnect(msg_data) => {
1330            let reason = msg_data.reason();
1331            trace!(
1332                peer=%state.node,
1333                ?reason,
1334                "Received Disconnect"
1335            );
1336            state.disconnect_reason = Some(reason);
1337
1338            // TODO handle the disconnection request
1339
1340            return Err(PeerConnectionError::DisconnectReceived(reason));
1341        }
1342        Message::Ping(_) => {
1343            trace!(peer=%state.node, "Sending pong message");
1344            send(state, Message::Pong(PongMessage {})).await?;
1345        }
1346        Message::Pong(_) => {
1347            // Liveness: the peer answered our ping; reset the missed-pong counter.
1348            state.missed_pongs = 0;
1349        }
1350        Message::Status68(msg_data) => {
1351            if let Some(eth) = &state.negotiated_eth_capability {
1352                backend::validate_status(msg_data, &state.storage, eth).await?
1353            };
1354        }
1355        Message::Status69(msg_data) => {
1356            if let Some(eth) = &state.negotiated_eth_capability {
1357                backend::validate_status(msg_data, &state.storage, eth).await?
1358            };
1359        }
1360        Message::Status70(msg_data) => {
1361            if let Some(eth) = &state.negotiated_eth_capability {
1362                backend::validate_status(msg_data, &state.storage, eth).await?
1363            };
1364        }
1365        Message::Status71(msg_data) => {
1366            if let Some(eth) = &state.negotiated_eth_capability {
1367                backend::validate_status(msg_data, &state.storage, eth).await?
1368            };
1369        }
1370        Message::GetAccountRange(req) => {
1371            let response = process_account_range_request(req, state.storage.clone()).await?;
1372            send(state, Message::AccountRange(response)).await?
1373        }
1374        Message::Transactions(txs) if peer_supports_eth => {
1375            // https://github.com/ethereum/devp2p/blob/master/caps/eth.md#transactions-0x02
1376            if !txs.transactions.is_empty() {
1377                state.received_txs_from_peer = true;
1378            }
1379            if state.blockchain.is_synced() {
1380                let tx_hashes: Vec<_> = txs
1381                    .transactions
1382                    .iter()
1383                    .map(|tx| tx.hash(&NativeCrypto))
1384                    .collect();
1385
1386                // Offload pool insertion to a background task so we don't block
1387                // the ConnectionServer (validation + signature recovery are expensive).
1388                let blockchain = state.blockchain.clone();
1389                let peer = state.node.to_string();
1390                #[cfg(feature = "l2")]
1391                let is_l2_mode = state.l2_state.is_supported();
1392                tokio::spawn(async move {
1393                    for tx in txs.transactions {
1394                        #[cfg(feature = "l2")]
1395                        if (is_l2_mode && matches!(tx, Transaction::EIP4844Transaction(_)))
1396                            || tx.is_privileged()
1397                        {
1398                            let tx_type = tx.tx_type();
1399                            debug!(peer=%peer, "Rejecting transaction in L2 mode - {tx_type} transactions are not broadcasted in L2");
1400                            continue;
1401                        }
1402
1403                        if let Err(e) = blockchain.add_transaction_to_pool(tx).await {
1404                            debug!(
1405                                peer=%peer,
1406                                error=%e,
1407                                "Error adding transaction"
1408                            );
1409                        }
1410                    }
1411                });
1412
1413                // Notify the broadcaster immediately — it only tracks hashes
1414                // to avoid re-broadcasting to the sender. The actual broadcast
1415                // happens on a periodic timer that queries the mempool directly.
1416                state
1417                    .tx_broadcaster
1418                    .add_txs(tx_hashes, state.node.node_id())
1419                    .map_err(|e| PeerConnectionError::BroadcastError(e.to_string()))?;
1420            }
1421        }
1422        Message::GetBlockHeaders(msg_data) if peer_supports_eth => {
1423            let response = BlockHeaders {
1424                id: msg_data.id,
1425                block_headers: msg_data.fetch_headers(&state.storage).await,
1426            };
1427            send(state, Message::BlockHeaders(response)).await?;
1428        }
1429        Message::GetBlockBodies(msg_data) if peer_supports_eth => {
1430            let response = BlockBodies {
1431                id: msg_data.id,
1432                block_bodies: msg_data.fetch_blocks(&state.storage).await,
1433            };
1434            send(state, Message::BlockBodies(response)).await?;
1435        }
1436        Message::GetBlockAccessLists(GetBlockAccessLists { id, block_hashes })
1437            if peer_supports_eth =>
1438        {
1439            use crate::rlpx::eth::block_access_lists::BLOCK_ACCESS_LIST_LIMIT;
1440            let mut block_access_lists =
1441                Vec::with_capacity(block_hashes.len().min(BLOCK_ACCESS_LIST_LIMIT));
1442            for hash in &block_hashes {
1443                // EIP-8159: only serve a BAL that matches the block header's
1444                // commitment. A stored BAL that doesn't hash to the header's
1445                // `block_access_list_hash` (e.g. a stale/empty entry from a prior
1446                // regeneration) must be reported as unavailable (`0x80`) rather
1447                // than served as a wrong BAL, which receiving peers would reject.
1448                let bal = match state.storage.get_block_access_list(*hash) {
1449                    Ok(Some(bal)) => {
1450                        let commitment = match state.storage.get_block_header_by_hash(*hash) {
1451                            Ok(Some(header)) => header.block_access_list_hash,
1452                            Ok(None) => None,
1453                            Err(err) => {
1454                                // Don't serve an unverified BAL: degrade to 0x80
1455                                // (unavailable), but log so an operator can tell a
1456                                // committed BAL was refused due to a DB error.
1457                                warn!(
1458                                    "Failed to read header for BAL commitment check (hash {hash:#x}): {err}; reporting BAL unavailable"
1459                                );
1460                                None
1461                            }
1462                        };
1463                        bal.matches_commitment(commitment, &NativeCrypto)
1464                            .then_some(bal)
1465                    }
1466                    Ok(None) => None,
1467                    Err(err) => {
1468                        error!("Error accessing DB while building BAL response for peer: {err}");
1469                        None
1470                    }
1471                };
1472                block_access_lists.push(bal);
1473                if block_access_lists.len() >= BLOCK_ACCESS_LIST_LIMIT {
1474                    break;
1475                }
1476            }
1477            let response = BlockAccessLists::new(id, block_access_lists);
1478            send(state, Message::BlockAccessLists(response)).await?;
1479        }
1480        Message::GetReceipts68(GetReceipts68 { id, block_hashes }) if peer_supports_eth => {
1481            let mut receipts = Vec::new();
1482            for hash in block_hashes.iter() {
1483                receipts.push(state.storage.get_receipts_for_block(hash).await?);
1484            }
1485            send(state, Message::Receipts68(Receipts68::new(id, receipts))).await?;
1486        }
1487        Message::GetReceipts69(GetReceipts68 { id, block_hashes }) if peer_supports_eth => {
1488            let mut receipts = Vec::new();
1489            for hash in block_hashes.iter() {
1490                receipts.push(state.storage.get_receipts_for_block(hash).await?);
1491            }
1492            send(state, Message::Receipts69(Receipts69::new(id, receipts))).await?;
1493        }
1494        // EIP-7975: eth/70 partial receipt requests
1495        Message::GetReceipts70(GetReceipts70 {
1496            id,
1497            first_block_receipt_index,
1498            block_hashes,
1499        }) if peer_supports_eth => {
1500            let block_hashes = &block_hashes[..block_hashes.len().min(256)];
1501            let mut all_receipts: Vec<Vec<Receipt>> = Vec::new();
1502            let mut total_size: usize = 0;
1503            let mut last_block_incomplete = false;
1504
1505            for (i, hash) in block_hashes.iter().enumerate() {
1506                let start_index = if i == 0 { first_block_receipt_index } else { 0 };
1507                let block_receipts = state
1508                    .storage
1509                    .get_receipts_for_block_from_index(hash, start_index, None)
1510                    .await?;
1511
1512                let mut block_receipt_list = Vec::new();
1513                let mut hit_limit = false;
1514                for receipt in block_receipts {
1515                    let receipt_size = receipt.length();
1516                    if total_size + receipt_size > SOFT_RESPONSE_LIMIT
1517                        && (!block_receipt_list.is_empty() || !all_receipts.is_empty())
1518                    {
1519                        hit_limit = true;
1520                        // Only mark incomplete when the current block actually
1521                        // has a partial receipt list. When the limit is hit
1522                        // before any receipt from this block fits, the previous
1523                        // block is complete — setting the flag would cause the
1524                        // peer to re-request an already-complete block.
1525                        if !block_receipt_list.is_empty() {
1526                            last_block_incomplete = true;
1527                        }
1528                        break;
1529                    }
1530                    total_size += receipt_size;
1531                    block_receipt_list.push(receipt);
1532                }
1533
1534                // Don't push an empty list when the limit was hit before any
1535                // receipt from this block could be included — an empty trailing
1536                // list would mislead the peer into thinking the block has no
1537                // transactions.
1538                if !block_receipt_list.is_empty() || !hit_limit {
1539                    all_receipts.push(block_receipt_list);
1540                }
1541
1542                if hit_limit {
1543                    break;
1544                }
1545            }
1546
1547            let response =
1548                Message::Receipts70(Receipts70::new(id, last_block_incomplete, all_receipts));
1549            send(state, response).await?;
1550        }
1551        Message::BlockRangeUpdate(update) => {
1552            trace!(
1553                peer=%state.node,
1554                range_from=update.earliest_block,
1555                range_to=update.latest_block,
1556                "Block range update",
1557            );
1558            // We will only validate the incoming update, we may decide to store and use this information in the future
1559            if let Err(err) = update.validate() {
1560                debug!(
1561                    peer=%state.node,
1562                    reason=%err,
1563                    "Disconnecting peer: invalid block range update",
1564                );
1565                send_disconnect_message(state, Some(DisconnectReason::SubprotocolError)).await;
1566                return Err(PeerConnectionError::DisconnectSent(
1567                    DisconnectReason::SubprotocolError,
1568                ));
1569            }
1570        }
1571        Message::NewPooledTransactionHashes(new_pooled_transaction_hashes) if peer_supports_eth => {
1572            // Don't request transactions if we're not synced — we won't be building blocks soon.
1573            if state.blockchain.is_synced() {
1574                let hashes = new_pooled_transaction_hashes
1575                    .get_transactions_to_request(&state.blockchain, state.node.node_id())?;
1576                if !hashes.is_empty() {
1577                    // Buffer hashes for batched requesting instead of sending immediately.
1578                    // The periodic flush_pending_tx_requests handler will send them.
1579                    state
1580                        .pending_tx_requests
1581                        .push((new_pooled_transaction_hashes, hashes));
1582                }
1583            }
1584        }
1585        Message::GetPooledTransactions(msg) => {
1586            let response = msg.handle(&state.blockchain)?;
1587            let batch_size = response.pooled_transactions.len() as u64;
1588            // Leech detection: disconnect peers that drain transactions but never contribute any.
1589            if state.txs_sent_to_peer + batch_size > LEECH_TX_SENT_THRESHOLD
1590                && !state.received_txs_from_peer
1591            {
1592                debug!(
1593                    peer = %state.node,
1594                    txs_sent = state.txs_sent_to_peer,
1595                    "Disconnecting peer: leech detected (sent many txs but received none)",
1596                );
1597                send_disconnect_message(state, Some(DisconnectReason::UselessPeer)).await;
1598                return Err(PeerConnectionError::DisconnectSent(
1599                    DisconnectReason::UselessPeer,
1600                ));
1601            }
1602            send(state, Message::PooledTransactions(response)).await?;
1603            state.txs_sent_to_peer += batch_size;
1604        }
1605        Message::PooledTransactions(msg) if peer_supports_eth => {
1606            if !msg.pooled_transactions.is_empty() {
1607                state.received_txs_from_peer = true;
1608            }
1609            // Always clear in-flight tracking for this response, regardless of sync status,
1610            // so other connections can re-request these hashes if needed.
1611            let removed_request = state.requested_pooled_txs.remove(&msg.id);
1612            if let Some((_, ref requested_hashes, _)) = removed_request {
1613                state
1614                    .blockchain
1615                    .mempool
1616                    .clear_in_flight_txs(requested_hashes)?;
1617            }
1618            // If we receive a blob transaction without blobs or with blobs that don't match the versioned hashes we must disconnect from the peer
1619            for tx in &msg.pooled_transactions {
1620                if let P2PTransaction::EIP4844TransactionWithBlobs(itx) = tx
1621                    && (itx.blobs_bundle.is_empty()
1622                        || itx
1623                            .blobs_bundle
1624                            .validate_blob_commitment_hashes(&itx.tx.blob_versioned_hashes)
1625                            .is_err())
1626                {
1627                    debug!(
1628                        peer=%state.node,
1629                        "Disconnecting peer: invalid or missing blobs",
1630                    );
1631                    if let Some((_announced, requested_hashes, _)) = &removed_request {
1632                        retry_on_alternates(&state.blockchain, &state.peer_table, requested_hashes)
1633                            .await;
1634                    }
1635                    send_disconnect_message(state, Some(DisconnectReason::SubprotocolError)).await;
1636                    return Err(PeerConnectionError::DisconnectSent(
1637                        DisconnectReason::SubprotocolError,
1638                    ));
1639                }
1640            }
1641            if state.blockchain.is_synced() {
1642                if let Some((announced, requested_hashes, _)) = &removed_request {
1643                    let fork = state.blockchain.current_fork().await?;
1644                    if let Err(error) = msg.validate_requested(announced, fork) {
1645                        debug!(
1646                            peer=%state.node,
1647                            reason=%error,
1648                            "Disconnecting peer: invalid pooled transactions response",
1649                        );
1650                        retry_on_alternates(&state.blockchain, &state.peer_table, requested_hashes)
1651                            .await;
1652                        send_disconnect_message(state, Some(DisconnectReason::SubprotocolError))
1653                            .await;
1654                        return Err(PeerConnectionError::DisconnectSent(
1655                            DisconnectReason::SubprotocolError,
1656                        ));
1657                    }
1658                }
1659                #[cfg(feature = "l2")]
1660                let is_l2_mode = state.l2_state.is_supported();
1661
1662                #[cfg(not(feature = "l2"))]
1663                let is_l2_mode = false;
1664                if let Err(error) = msg.handle(&state.node, &state.blockchain, is_l2_mode).await {
1665                    if matches!(
1666                        error,
1667                        ethrex_blockchain::error::MempoolError::BlobsBundleError(_)
1668                    ) {
1669                        debug!(
1670                            peer=%state.node,
1671                            reason=%error,
1672                            "Disconnecting peer: invalid pooled transactions response",
1673                        );
1674                        if let Some((_announced, requested_hashes, _)) = &removed_request {
1675                            retry_on_alternates(
1676                                &state.blockchain,
1677                                &state.peer_table,
1678                                requested_hashes,
1679                            )
1680                            .await;
1681                        }
1682                        send_disconnect_message(state, Some(DisconnectReason::SubprotocolError))
1683                            .await;
1684                        return Err(PeerConnectionError::DisconnectSent(
1685                            DisconnectReason::SubprotocolError,
1686                        ));
1687                    }
1688                    return Err(error.into());
1689                }
1690            }
1691        }
1692        Message::GetStorageRanges(req) => {
1693            let response = process_storage_ranges_request(req, state.storage.clone()).await?;
1694            send(state, Message::StorageRanges(response)).await?
1695        }
1696        Message::GetByteCodes(req) => {
1697            let storage_clone = state.storage.clone();
1698            let response = process_byte_codes_request(req, storage_clone)
1699                .await
1700                .map_err(|_| {
1701                    PeerConnectionError::InternalError(
1702                        "Failed to execute bytecode retrieval task".to_string(),
1703                    )
1704                })?;
1705            send(state, Message::ByteCodes(response)).await?
1706        }
1707        Message::GetTrieNodes(req) => {
1708            let id = req.id;
1709            match process_trie_nodes_request(req, state.storage.clone()).await {
1710                Ok(response) => send(state, Message::TrieNodes(response)).await?,
1711                Err(_) => send(state, Message::TrieNodes(TrieNodes { id, nodes: vec![] })).await?,
1712            }
1713        }
1714        #[cfg(feature = "l2")]
1715        Message::L2(req) if peer_supports_l2 => {
1716            handle_based_capability_message(state, req).await?;
1717        }
1718        // Send response messages to the backend
1719        message @ Message::AccountRange(_)
1720        | message @ Message::StorageRanges(_)
1721        | message @ Message::ByteCodes(_)
1722        | message @ Message::TrieNodes(_)
1723        | message @ Message::BlockBodies(_)
1724        | message @ Message::BlockHeaders(_)
1725        | message @ Message::Receipts68(_)
1726        | message @ Message::Receipts69(_)
1727        | message @ Message::Receipts70(_)
1728        | message @ Message::BlockAccessLists(_) => {
1729            if let Some((_, tx)) = message
1730                .request_id()
1731                .and_then(|id| state.current_requests.remove(&id))
1732            {
1733                tx.send(message)
1734                    .map_err(|e| PeerConnectionError::SendMessage(e.to_string()))?
1735            } else {
1736                return Err(PeerConnectionError::ExpectedRequestId(format!("{message}")));
1737            }
1738        }
1739        // TODO: Add new message types and handlers as they are implemented
1740        message => return Err(PeerConnectionError::MessageNotHandled(format!("{message}"))),
1741    };
1742    Ok(())
1743}
1744
1745async fn handle_outgoing_message(
1746    state: &mut Established,
1747    message: Message,
1748) -> Result<(), PeerConnectionError> {
1749    trace!(
1750        peer=%state.node,
1751        %message,
1752        "Sending message"
1753    );
1754    send(state, message).await?;
1755    Ok(())
1756}
1757
1758async fn handle_outgoing_request(
1759    state: &mut Established,
1760    message: Message,
1761    sender: oneshot::Sender<Message>,
1762) -> Result<(), PeerConnectionError> {
1763    // Insert the request in the request map if it supports a request id.
1764    message.request_id().and_then(|id| {
1765        state
1766            .current_requests
1767            .insert(id, (format!("{message}"), sender))
1768    });
1769    trace!(
1770        peer=%state.node,
1771        %message,
1772        "Sending request"
1773    );
1774    send(state, message).await?;
1775    Ok(())
1776}
1777
1778async fn handle_broadcast(
1779    state: &mut Established,
1780    (id, broadcasted_msg): (task::Id, Arc<Message>),
1781) -> Result<(), PeerConnectionError> {
1782    if id != tokio::task::id() {
1783        match broadcasted_msg.as_ref() {
1784            #[cfg(feature = "l2")]
1785            l2_msg @ Message::L2(_) => {
1786                handle_l2_broadcast(state, l2_msg).await?;
1787            }
1788            msg => {
1789                error!(
1790                    peer=%state.node,
1791                    message=%msg,
1792                    "Non-supported message broadcasted"
1793                );
1794                let error_message = format!("Non-supported message broadcasted: {msg}");
1795                return Err(PeerConnectionError::BroadcastError(error_message));
1796            }
1797        }
1798    }
1799    Ok(())
1800}
1801
1802async fn handle_block_range_update(state: &mut Established) -> Result<(), PeerConnectionError> {
1803    if should_send_block_range_update(state).await? {
1804        send_block_range_update(state).await
1805    } else {
1806        Ok(())
1807    }
1808}
1809
1810/// Drains the pending transaction request buffer and sends batched
1811/// GetPooledTransactions requests, respecting the 256-hash-per-request
1812/// limit from the devp2p ETH spec.
1813async fn flush_pending_tx_requests(state: &mut Established) -> Result<(), PeerConnectionError> {
1814    if state.pending_tx_requests.is_empty() {
1815        return Ok(());
1816    }
1817
1818    let pending = std::mem::take(&mut state.pending_tx_requests);
1819
1820    // Build a trimmed announcement containing only the hashes we're actually requesting,
1821    // with their original types and sizes for response validation.
1822    let mut all_hashes: Vec<H256> = Vec::new();
1823    let mut all_types: Vec<u8> = Vec::new();
1824    let mut all_sizes: Vec<usize> = Vec::new();
1825
1826    for (announcement, hashes) in &pending {
1827        let trimmed = announcement.filter_to(hashes);
1828        all_hashes.extend_from_slice(&trimmed.transaction_hashes);
1829        all_types.extend_from_slice(&trimmed.transaction_types);
1830        all_sizes.extend(trimmed.transaction_sizes);
1831    }
1832
1833    // Send in chunks of MAX_HASHES_PER_REQUEST per the devp2p spec.
1834    const MAX_HASHES_PER_REQUEST: usize = 256;
1835    for (i, chunk) in all_hashes.chunks(MAX_HASHES_PER_REQUEST).enumerate() {
1836        let offset = i * MAX_HASHES_PER_REQUEST;
1837        let chunk_types = &all_types[offset..offset + chunk.len()];
1838        let chunk_sizes = &all_sizes[offset..offset + chunk.len()];
1839
1840        let announcement = NewPooledTransactionHashes::from_raw(
1841            chunk_types.to_vec().into(),
1842            chunk_sizes.to_vec(),
1843            chunk.to_vec(),
1844        );
1845        let request = GetPooledTransactions::new(random(), chunk.to_vec());
1846        let request_id = request.id;
1847        // Send first, only register in requested_pooled_txs on success.
1848        // This ensures we never track hashes for messages that were not transmitted.
1849        if let Err(e) = send(state, Message::GetPooledTransactions(request)).await {
1850            // Clear in-flight for the current chunk (failed to send) and all remaining chunks,
1851            // then try alternate announcers. Order matters: clear first so the alternate's
1852            // reserve_unknown_hashes sees the hashes as free.
1853            // Build an announcement covering every unsent hash (later chunks too) so the
1854            // alternate can validate its response against the original type/size metadata.
1855            let unsent = &all_hashes[offset..];
1856            if !unsent.is_empty() {
1857                if let Err(clear_err) = state.blockchain.mempool.clear_in_flight_txs(unsent) {
1858                    warn!(error = %clear_err, "Failed to clear in-flight transaction tracking after send error");
1859                }
1860                retry_on_alternates(&state.blockchain, &state.peer_table, unsent).await;
1861            }
1862            return Err(e);
1863        }
1864        state
1865            .requested_pooled_txs
1866            .insert(request_id, (announcement, chunk.to_vec(), Instant::now()));
1867    }
1868
1869    Ok(())
1870}
1871
1872/// For each hash that has a remaining alternate announcer, look up that
1873/// peer's connection and enqueue the request there. Each alternate carries
1874/// the (type, size) metadata it originally announced, so the retry request
1875/// is built from the alternate's own announcement rather than the failing
1876/// peer's; otherwise validation against the failing peer's sizes would
1877/// reject the alternate's response when the two announcements differ (e.g.
1878/// bare blob tx vs full sidecar).
1879///
1880/// If a popped alternate is no longer reachable, keep popping until a live
1881/// peer is found or alternates for that hash are exhausted, so a disconnected
1882/// alternate doesn't burn the only fallback slot.
1883async fn retry_on_alternates(
1884    blockchain: &Arc<Blockchain>,
1885    peer_table: &PeerTable,
1886    hashes: &[H256],
1887) {
1888    if hashes.is_empty() {
1889        return;
1890    }
1891    // Group hashes by chosen live alternate, carrying their own type/size.
1892    // We walk per-hash so a dead alternate for hash X doesn't consume the
1893    // slot that hash Y could use. The `PeerConnection` handle from the
1894    // liveness probe is stashed in `by_peer` and reused at enqueue time,
1895    // so there's no second lookup (and no race where the connection drops
1896    // between probe and use).
1897    type AltGroup = (PeerConnection, Vec<(H256, u8, usize)>);
1898    let mut by_peer: FxHashMap<H256, AltGroup> = FxHashMap::default();
1899    for hash in hashes {
1900        loop {
1901            let alt = match blockchain.mempool.pop_alternate(*hash) {
1902                Ok(Some(a)) => a,
1903                Ok(None) => break,
1904                Err(e) => {
1905                    warn!(error = %e, "pop_alternate failed");
1906                    break;
1907                }
1908            };
1909            // Reuse the connection we already grabbed for this peer.
1910            if let Some((_, list)) = by_peer.get_mut(&alt.peer_id) {
1911                list.push((*hash, alt.tx_type, alt.tx_size));
1912                break;
1913            }
1914            match peer_table.get_peer_connection(alt.peer_id).await {
1915                Ok(Some(conn)) => {
1916                    by_peer.insert(alt.peer_id, (conn, vec![(*hash, alt.tx_type, alt.tx_size)]));
1917                    break;
1918                }
1919                Ok(None) => continue, // dead peer, try next alternate
1920                Err(e) => {
1921                    warn!(error = %e, "get_peer_connection failed");
1922                    break;
1923                }
1924            }
1925        }
1926    }
1927
1928    for (_, (conn, entries)) in by_peer {
1929        let mut types = Vec::with_capacity(entries.len());
1930        let mut sizes = Vec::with_capacity(entries.len());
1931        let mut hash_list = Vec::with_capacity(entries.len());
1932        for (h, t, s) in &entries {
1933            hash_list.push(*h);
1934            types.push(*t);
1935            sizes.push(*s);
1936        }
1937        let announcement =
1938            NewPooledTransactionHashes::from_raw(types.into(), sizes, hash_list.clone());
1939        if let Err(e) = conn.enqueue_tx_requests(announcement, hash_list) {
1940            debug!(error = %e, "Failed to enqueue tx requests on alternate peer");
1941        }
1942    }
1943}