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