Skip to main content

reth_eth_wire/
p2pstream.rs

1use crate::{
2    capability::SharedCapabilities,
3    disconnect::CanDisconnect,
4    errors::{P2PHandshakeError, P2PStreamError},
5    pinger::{Pinger, PingerEvent},
6    protocol::ProtocolIngressLimits,
7    DisconnectReason, HelloMessage, HelloMessageWithProtocols,
8};
9use alloy_primitives::{
10    bytes::{Buf, BufMut, Bytes, BytesMut},
11    hex,
12};
13use alloy_rlp::{Decodable, Encodable, Error as RlpError, EMPTY_LIST_CODE};
14use futures::{Sink, SinkExt, StreamExt};
15use pin_project::pin_project;
16use reth_codecs::add_arbitrary_tests;
17use reth_metrics::metrics::counter;
18use reth_primitives_traits::GotExpected;
19use std::{
20    collections::VecDeque,
21    future::Future,
22    io,
23    pin::Pin,
24    task::{ready, Context, Poll},
25    time::{Duration, Instant},
26};
27use tokio_stream::Stream;
28use tracing::{debug, trace};
29
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33/// [`MAX_PAYLOAD_SIZE`] is the default maximum size of an uncompressed message payload.
34/// This is defined in [EIP-706](https://eips.ethereum.org/EIPS/eip-706).
35///
36/// See [`UnauthedP2PStream::with_max_payload_size`] to use a different limit.
37pub const MAX_PAYLOAD_SIZE: usize = 16 * 1024 * 1024;
38
39/// Maximum size of a decompressed `p2p` disconnect payload: [`DisconnectReason`] decodes at most
40/// two bytes, a reason code optionally wrapped in a one-item list.
41const MAX_DISCONNECT_PAYLOAD_SIZE: usize = 2;
42
43/// Maximum number of bytes of a malformed peer message included in debug logs, so a peer cannot
44/// turn one message into a multi-megabyte log record.
45const MAX_LOGGED_PAYLOAD_SIZE: usize = 64;
46
47/// [`MAX_RESERVED_MESSAGE_ID`] is the maximum message ID reserved for the `p2p` subprotocol. If
48/// there are any incoming messages with an ID greater than this, they are subprotocol messages.
49pub const MAX_RESERVED_MESSAGE_ID: u8 = 0x0f;
50
51/// [`MAX_P2P_MESSAGE_ID`] is the maximum message ID in use for the `p2p` subprotocol.
52const MAX_P2P_MESSAGE_ID: u8 = P2PMessageID::Pong as u8;
53
54/// Snappy framed RLP empty list payload used by fixed `p2p` ping/pong control messages.
55const SNAPPY_EMPTY_LIST_PAYLOAD: &[u8] = &[0x01, 0x00, EMPTY_LIST_CODE];
56
57/// Wire-encoded `p2p` ping control message.
58const SNAPPY_PING_MESSAGE: &[u8] = &[0x02, 0x01, 0x00, EMPTY_LIST_CODE];
59
60/// Wire-encoded `p2p` pong control message.
61const SNAPPY_PONG_MESSAGE: &[u8] = &[0x03, 0x01, 0x00, EMPTY_LIST_CODE];
62
63/// [`HANDSHAKE_TIMEOUT`] determines the amount of time to wait before determining that a `p2p`
64/// handshake has timed out.
65pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
66
67/// [`PING_TIMEOUT`] determines the amount of time to wait before determining that a `p2p` ping has
68/// timed out.
69const PING_TIMEOUT: Duration = Duration::from_secs(15);
70
71/// [`PING_INTERVAL`] determines the amount of time to wait between sending `p2p` ping messages
72/// when the peer is responsive.
73const PING_INTERVAL: Duration = Duration::from_secs(60);
74
75/// Maximum number of incoming pings that can arrive in a burst.
76const PING_TOKEN_BUCKET_CAPACITY: u8 = 5;
77
78/// [`MAX_P2P_CAPACITY`] is the maximum number of messages that can be buffered to be sent in the
79/// `p2p` stream.
80///
81/// Note: this default is rather low because it is expected that the [`P2PStream`] wraps an
82/// [`ECIESStream`](reth_ecies::stream::ECIESStream) which internally already buffers a few MB of
83/// encoded data.
84const MAX_P2P_CAPACITY: usize = 2;
85
86/// Maximum size of the reusable compression scratch buffer in [`P2PStream`], covering the snappy
87/// worst case of typical broadcast messages (soft-capped around 128KiB).
88///
89/// Messages with a larger compressed worst case are compressed through a one-off allocation
90/// instead, so a single oversized message neither grows the scratch buffer for the connection's
91/// lifetime nor causes shrink/regrow churn, see [`compress_frame`].
92const MAX_COMPRESS_SCRATCH_SIZE: usize = 256 * 1024;
93
94/// An un-authenticated [`P2PStream`]. This is consumed and returns a [`P2PStream`] after the
95/// `Hello` handshake is completed.
96#[pin_project]
97#[derive(Debug)]
98pub struct UnauthedP2PStream<S> {
99    #[pin]
100    inner: S,
101    /// Maximum size of an uncompressed message payload, carried into the [`P2PStream`].
102    max_payload_size: usize,
103}
104
105impl<S> UnauthedP2PStream<S> {
106    /// Create a new `UnauthedP2PStream` from a type `S` which implements `Stream` and `Sink`.
107    pub const fn new(inner: S) -> Self {
108        Self { inner, max_payload_size: MAX_PAYLOAD_SIZE }
109    }
110
111    /// Sets the maximum size of an uncompressed message payload, which defaults to
112    /// [`MAX_PAYLOAD_SIZE`].
113    ///
114    /// The limit applies to the handshake and to every message of the [`P2PStream`] it yields, in
115    /// both directions.
116    pub const fn with_max_payload_size(mut self, max_payload_size: usize) -> Self {
117        self.max_payload_size = max_payload_size;
118        self
119    }
120
121    /// Returns a reference to the inner stream.
122    pub const fn inner(&self) -> &S {
123        &self.inner
124    }
125}
126
127impl<S> UnauthedP2PStream<S>
128where
129    S: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin,
130{
131    /// Consumes the `UnauthedP2PStream` and returns a `P2PStream` after the `Hello` handshake is
132    /// completed successfully. This also returns the `Hello` message sent by the remote peer.
133    pub async fn handshake(
134        mut self,
135        hello: HelloMessageWithProtocols,
136    ) -> Result<(P2PStream<S>, HelloMessage), P2PStreamError> {
137        trace!(?hello, "sending p2p hello to peer");
138
139        // send our hello message with the Sink
140        self.inner.send(alloy_rlp::encode(P2PMessage::Hello(hello.message())).into()).await?;
141
142        let first_message_bytes = tokio::time::timeout(HANDSHAKE_TIMEOUT, self.inner.next())
143            .await
144            .or(Err(P2PStreamError::HandshakeError(P2PHandshakeError::Timeout)))?
145            .ok_or(P2PStreamError::HandshakeError(P2PHandshakeError::NoResponse))??;
146
147        // Check that the uncompressed message length does not exceed the max payload size.
148        // Note: The first message (Hello/Disconnect) is not snappy compressed. We will check the
149        // decompressed length again for subsequent messages after the handshake.
150        if first_message_bytes.len() > self.max_payload_size {
151            return Err(P2PStreamError::MessageTooBig {
152                message_size: first_message_bytes.len(),
153                max_size: self.max_payload_size,
154            })
155        }
156
157        // The first message sent MUST be a hello OR disconnect message
158        //
159        // If the first message is a disconnect message, we should not decode using
160        // Decodable::decode, because the first message (either Disconnect or Hello) is not snappy
161        // compressed, and the Decodable implementation assumes that non-hello messages are snappy
162        // compressed.
163        let their_hello = match P2PMessage::decode(&mut &first_message_bytes[..]) {
164            Ok(P2PMessage::Hello(hello)) => Ok(hello),
165            Ok(P2PMessage::Disconnect(reason)) => {
166                if matches!(reason, DisconnectReason::TooManyPeers) {
167                    // Too many peers is a very common disconnect reason that spams the DEBUG logs
168                    trace!(%reason, "Disconnected by peer during handshake");
169                } else {
170                    debug!(%reason, "Disconnected by peer during handshake");
171                };
172                counter!("p2pstream.disconnected_errors").increment(1);
173                Err(P2PStreamError::HandshakeError(P2PHandshakeError::Disconnected(reason)))
174            }
175            Err(err) => {
176                debug!(
177                    %err,
178                    len = first_message_bytes.len(),
179                    msg = %hex::encode(log_prefix(&first_message_bytes)),
180                    "Failed to decode first message from peer"
181                );
182                Err(P2PStreamError::HandshakeError(err.into()))
183            }
184            Ok(msg) => {
185                debug!(?msg, "expected hello message but received another message");
186                Err(P2PStreamError::HandshakeError(P2PHandshakeError::NonHelloMessageInHandshake))
187            }
188        }?;
189
190        trace!(
191            hello=?their_hello,
192            "validating incoming p2p hello from peer"
193        );
194
195        if (hello.protocol_version as u8) != their_hello.protocol_version as u8 {
196            // send a disconnect message notifying the peer of the protocol version mismatch
197            self.send_disconnect(DisconnectReason::IncompatibleP2PProtocolVersion).await?;
198            return Err(P2PStreamError::MismatchedProtocolVersion(GotExpected {
199                got: their_hello.protocol_version,
200                expected: hello.protocol_version,
201            }))
202        }
203
204        // determine shared capabilities (currently returns only one capability)
205        let capability_res =
206            SharedCapabilities::try_new(hello.protocols, their_hello.capabilities.clone());
207
208        let shared_capability = match capability_res {
209            Err(err) => {
210                // we don't share any capabilities, send a disconnect message
211                self.send_disconnect(DisconnectReason::UselessPeer).await?;
212                Err(err)
213            }
214            Ok(cap) => Ok(cap),
215        }?;
216
217        let mut stream = P2PStream::new(self.inner, shared_capability);
218        stream.set_max_payload_size(self.max_payload_size);
219
220        Ok((stream, their_hello))
221    }
222}
223
224impl<S> UnauthedP2PStream<S>
225where
226    S: Sink<Bytes, Error = io::Error> + Unpin,
227{
228    /// Send a disconnect message during the handshake. This is sent without snappy compression.
229    pub async fn send_disconnect(
230        &mut self,
231        reason: DisconnectReason,
232    ) -> Result<(), P2PStreamError> {
233        trace!(
234            %reason,
235            "Sending disconnect message during the handshake",
236        );
237        self.inner
238            .send(Bytes::from(alloy_rlp::encode(P2PMessage::Disconnect(reason))))
239            .await
240            .map_err(P2PStreamError::Io)
241    }
242}
243
244impl<S> CanDisconnect<Bytes> for P2PStream<S>
245where
246    S: Sink<Bytes, Error = io::Error> + Unpin + Send + Sync,
247{
248    fn disconnect(
249        &mut self,
250        reason: DisconnectReason,
251    ) -> Pin<Box<dyn Future<Output = Result<(), P2PStreamError>> + Send + '_>> {
252        Box::pin(async move { self.disconnect(reason).await })
253    }
254}
255
256/// A `P2PStream` wraps over any `Stream` that yields bytes and makes it compatible with `p2p`
257/// protocol messages.
258///
259/// This stream supports multiple shared capabilities, that were negotiated during the handshake.
260///
261/// ### Message-ID based multiplexing
262///
263/// > Each capability is given as much of the message-ID space as it needs. All such capabilities
264/// > must statically specify how many message IDs they require. On connection and reception of the
265/// > Hello message, both peers have equivalent information about what capabilities they share
266/// > (including versions) and are able to form consensus over the composition of message ID space.
267///
268/// > Message IDs are assumed to be compact from ID 0x10 onwards (0x00-0x0f is reserved for the
269/// > "p2p" capability) and given to each shared (equal-version, equal-name) capability in
270/// > alphabetic order. Capability names are case-sensitive. Capabilities which are not shared are
271/// > ignored. If multiple versions are shared of the same (equal name) capability, the numerically
272/// > highest wins, others are ignored.
273///
274/// See also <https://github.com/ethereum/devp2p/blob/master/rlpx.md#message-id-based-multiplexing>
275///
276/// This stream emits _non-empty_ Bytes that start with the normalized message id, so that the first
277/// byte of each message starts from 0. If this stream only supports a single capability, for
278/// example `eth` then the first byte of each message will match
279/// [EthMessageID](reth_eth_wire_types::message::EthMessageID).
280///
281/// ### Sink behavior
282///
283/// The [`Sink`] impl batches writes: queued messages are drained into the underlying sink
284/// unflushed, and the caller is responsible for driving [`Sink::poll_flush`] to deliver them to
285/// the wire. Queued `p2p` control messages (ping/pong/disconnect) are the exception: they force a
286/// flush from [`Sink::poll_ready`]. Keepalive pings are generated in `poll_ready`, so the sink
287/// half must be polled regularly even if the caller has nothing to send.
288#[pin_project]
289#[derive(Debug)]
290pub struct P2PStream<S> {
291    #[pin]
292    inner: S,
293
294    /// The snappy encoder used for compressing outgoing messages
295    encoder: snap::raw::Encoder,
296
297    /// Reusable scratch buffer for compressing outgoing messages, see [`compress_frame`].
298    ///
299    /// Grow-only and capped at [`MAX_COMPRESS_SCRATCH_SIZE`]; kept fully initialized, so
300    /// zero-initialization is only paid when the buffer grows and each message only copies out
301    /// the exact compressed size instead of zeroing a worst-case sized buffer per message.
302    compress_scratch: Vec<u8>,
303
304    /// The snappy decoder used for decompressing incoming messages
305    decoder: snap::raw::Decoder,
306
307    /// The state machine used for keeping track of the peer's ping status.
308    pinger: Pinger,
309
310    /// Per-connection limit for incoming ping bursts.
311    ping_token_bucket: PingTokenBucket,
312
313    /// The supported capability for this stream.
314    shared_capabilities: SharedCapabilities,
315
316    /// Explicit frame limits registered by installed subprotocol handlers.
317    inbound_protocol_limits: Vec<InboundProtocolLimit>,
318
319    /// Maximum size of an uncompressed message payload, in both directions.
320    max_payload_size: usize,
321
322    /// Outgoing messages buffered for sending to the underlying stream.
323    outgoing_messages: VecDeque<Bytes>,
324
325    /// Maximum number of messages that we can buffer here before the [Sink] impl returns
326    /// [`Poll::Pending`].
327    outgoing_message_buffer_capacity: usize,
328
329    /// Whether this stream is currently in the process of disconnecting by sending a disconnect
330    /// message.
331    disconnecting: bool,
332
333    /// Whether the underlying sink has accepted messages that still need to be flushed.
334    needs_flush: bool,
335
336    /// Whether a queued p2p control message needs to be flushed even if no subprotocol messages
337    /// are sent by the caller.
338    needs_control_flush: bool,
339}
340
341impl<S> P2PStream<S> {
342    /// Create a new [`P2PStream`] from the provided stream.
343    /// New [`P2PStream`]s are assumed to have completed the `p2p` handshake successfully and are
344    /// ready to send and receive subprotocol messages.
345    pub fn new(inner: S, shared_capabilities: SharedCapabilities) -> Self {
346        Self {
347            inner,
348            encoder: snap::raw::Encoder::new(),
349            compress_scratch: Vec::new(),
350            decoder: snap::raw::Decoder::new(),
351            pinger: Pinger::new(PING_INTERVAL, PING_TIMEOUT),
352            ping_token_bucket: PingTokenBucket::new(Instant::now()),
353            shared_capabilities,
354            inbound_protocol_limits: Vec::new(),
355            max_payload_size: MAX_PAYLOAD_SIZE,
356            outgoing_messages: VecDeque::new(),
357            outgoing_message_buffer_capacity: MAX_P2P_CAPACITY,
358            disconnecting: false,
359            needs_flush: false,
360            needs_control_flush: false,
361        }
362    }
363
364    /// Returns a reference to the inner stream.
365    pub const fn inner(&self) -> &S {
366        &self.inner
367    }
368
369    /// Sets a custom outgoing message buffer capacity.
370    ///
371    /// # Panics
372    ///
373    /// If the provided capacity is `0`.
374    pub const fn set_outgoing_message_buffer_capacity(&mut self, capacity: usize) {
375        assert!(capacity != 0);
376        self.outgoing_message_buffer_capacity = capacity;
377    }
378
379    /// Sets the maximum size of an uncompressed message payload, which defaults to
380    /// [`MAX_PAYLOAD_SIZE`]. It applies to both incoming and outgoing messages.
381    pub const fn set_max_payload_size(&mut self, max_payload_size: usize) {
382        self.max_payload_size = max_payload_size;
383    }
384
385    /// Returns the shared capabilities for this stream.
386    ///
387    /// This includes all the shared capabilities that were negotiated during the handshake and
388    /// their offsets based on the number of messages of each capability.
389    pub const fn shared_capabilities(&self) -> &SharedCapabilities {
390        &self.shared_capabilities
391    }
392
393    pub(crate) fn set_protocol_ingress_limits(
394        &mut self,
395        capability: &crate::capability::SharedCapability,
396        limits: ProtocolIngressLimits,
397    ) {
398        let Some(max_frame_bytes) = limits.max_frame_bytes() else { return };
399        if capability.num_messages() == 0 {
400            return
401        }
402
403        let start = capability.message_id_offset();
404        let end = u16::from(start) + u16::from(capability.num_messages());
405        let configured = InboundProtocolLimit {
406            capability: capability.capability().into_owned(),
407            start,
408            end,
409            max_frame_bytes,
410        };
411
412        if let Some(current) = self
413            .inbound_protocol_limits
414            .iter_mut()
415            .find(|current| current.capability == configured.capability)
416        {
417            *current = configured;
418        } else {
419            self.inbound_protocol_limits.push(configured);
420        }
421    }
422
423    fn inbound_protocol_limit(&self, message_id: u8) -> Option<&InboundProtocolLimit> {
424        self.inbound_protocol_limits.iter().find(|limit| limit.contains(message_id))
425    }
426
427    /// Returns `true` if the stream has outgoing capacity.
428    fn has_outgoing_capacity(&self) -> bool {
429        self.outgoing_messages.len() < self.outgoing_message_buffer_capacity
430    }
431
432    /// Queues in a _snappy_ encoded [`P2PMessage::Pong`] message.
433    fn send_pong(&mut self) {
434        self.outgoing_messages.push_back(Bytes::from_static(SNAPPY_PONG_MESSAGE));
435        self.needs_control_flush = true;
436    }
437
438    /// Queues in a _snappy_ encoded [`P2PMessage::Ping`] message.
439    pub fn send_ping(&mut self) {
440        self.outgoing_messages.push_back(Bytes::from_static(SNAPPY_PING_MESSAGE));
441        self.needs_control_flush = true;
442    }
443}
444
445/// Per-connection bucket that restores one incoming ping token per second.
446#[derive(Debug)]
447struct PingTokenBucket {
448    tokens: u8,
449    last_refill: Instant,
450}
451
452impl PingTokenBucket {
453    const fn new(now: Instant) -> Self {
454        Self { tokens: PING_TOKEN_BUCKET_CAPACITY, last_refill: now }
455    }
456
457    fn try_take(&mut self, now: Instant) -> bool {
458        let refill = now.saturating_duration_since(self.last_refill).as_secs();
459        if refill > 0 {
460            self.tokens = u64::from(self.tokens)
461                .saturating_add(refill)
462                .min(u64::from(PING_TOKEN_BUCKET_CAPACITY)) as u8;
463            self.last_refill += Duration::from_secs(refill);
464        }
465
466        if self.tokens == 0 {
467            return false
468        }
469
470        if self.tokens == PING_TOKEN_BUCKET_CAPACITY {
471            self.last_refill = now;
472        }
473        self.tokens -= 1;
474        true
475    }
476}
477
478#[derive(Debug)]
479struct InboundProtocolLimit {
480    capability: crate::Capability,
481    start: u8,
482    end: u16,
483    max_frame_bytes: usize,
484}
485
486impl InboundProtocolLimit {
487    const fn contains(&self, message_id: u8) -> bool {
488        message_id >= self.start && (message_id as u16) < self.end
489    }
490}
491
492/// Gracefully disconnects the connection by sending a disconnect message and stop reading new
493/// messages.
494pub trait DisconnectP2P {
495    /// Starts to gracefully disconnect.
496    fn start_disconnect(&mut self, reason: DisconnectReason) -> Result<(), P2PStreamError>;
497
498    /// Returns `true` if the connection is about to disconnect.
499    fn is_disconnecting(&self) -> bool;
500}
501
502impl<S> DisconnectP2P for P2PStream<S> {
503    /// Starts to gracefully disconnect the connection by sending a Disconnect message and stop
504    /// reading new messages.
505    ///
506    /// Once disconnect process has started, the [`Stream`] will terminate immediately.
507    ///
508    /// # Errors
509    ///
510    /// Returns an error only if the message fails to compress.
511    fn start_disconnect(&mut self, reason: DisconnectReason) -> Result<(), P2PStreamError> {
512        // clear any buffered messages and queue in
513        self.outgoing_messages.clear();
514        let disconnect = P2PMessage::Disconnect(reason);
515        let buf = alloy_rlp::encode(&disconnect);
516
517        // we do not add the capability offset because the disconnect message is a `p2p` reserved
518        // message
519        let compressed =
520            compress_frame(&mut self.encoder, &mut self.compress_scratch, buf[0], &buf[1..])
521                .map_err(|err| {
522                    debug!(
523                        %err,
524                        msg=%hex::encode(&buf[1..]),
525                        "error compressing disconnect"
526                    );
527                    err
528                })?;
529
530        self.outgoing_messages.push_back(compressed);
531        self.needs_control_flush = true;
532        self.disconnecting = true;
533        Ok(())
534    }
535
536    fn is_disconnecting(&self) -> bool {
537        self.disconnecting
538    }
539}
540
541impl<S> P2PStream<S>
542where
543    S: Sink<Bytes, Error = io::Error> + Unpin + Send,
544{
545    /// Disconnects the connection by sending a disconnect message.
546    ///
547    /// This future resolves once the disconnect message has been sent and the stream has been
548    /// closed.
549    pub async fn disconnect(&mut self, reason: DisconnectReason) -> Result<(), P2PStreamError> {
550        self.start_disconnect(reason)?;
551        self.close().await
552    }
553}
554
555impl<S> P2PStream<S>
556where
557    S: Sink<Bytes, Error = io::Error> + Unpin,
558{
559    /// Drains queued p2p frames into the underlying sink without flushing the underlying sink.
560    fn poll_drain_outgoing(
561        mut self: Pin<&mut Self>,
562        cx: &mut Context<'_>,
563    ) -> Poll<Result<(), P2PStreamError>> {
564        let mut this = self.as_mut().project();
565        while !this.outgoing_messages.is_empty() {
566            ready!(this.inner.as_mut().poll_ready(cx))?;
567            let message = this.outgoing_messages.pop_front().expect("checked non-empty");
568            this.inner.as_mut().start_send(message)?;
569            *this.needs_flush = true;
570        }
571
572        Poll::Ready(Ok(()))
573    }
574}
575
576// S must also be `Sink` because we need to be able to respond with ping messages to follow the
577// protocol
578impl<S> Stream for P2PStream<S>
579where
580    S: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin,
581{
582    type Item = Result<BytesMut, P2PStreamError>;
583
584    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
585        let this = self.get_mut();
586
587        if this.disconnecting {
588            // if disconnecting, stop reading messages
589            return Poll::Ready(None)
590        }
591
592        let mut ping_batch_time = None;
593
594        // we should loop here to ensure we don't return Poll::Pending if we have a message to
595        // return behind any pings we need to respond to
596        while let Poll::Ready(res) = this.inner.poll_next_unpin(cx) {
597            let bytes = match res {
598                Some(Ok(bytes)) => bytes,
599                Some(Err(err)) => return Poll::Ready(Some(Err(err.into()))),
600                None => return Poll::Ready(None),
601            };
602
603            if bytes.is_empty() {
604                // empty messages are not allowed
605                return Poll::Ready(Some(Err(P2PStreamError::EmptyProtocolMessage)))
606            }
607
608            // first decode disconnect reasons, because they can be encoded in a variety of forms
609            // over the wire, in both snappy compressed and uncompressed forms.
610            //
611            // see: [crate::disconnect::tests::test_decode_known_reasons]
612            let id = bytes[0];
613            if id == P2PMessageID::Disconnect as u8 {
614                // We can't handle the error here because disconnect reasons are encoded as both:
615                // * snappy compressed, AND
616                // * uncompressed
617                // over the network.
618                //
619                // If the decoding succeeds, we already checked the id and know this is a
620                // disconnect message, so we can return with the reason.
621                //
622                // If the decoding fails, we continue, and will attempt to decode it again if the
623                // message is snappy compressed. Failure handling in that step is the primary point
624                // where an error is returned if the disconnect reason is malformed.
625                if let Ok(reason) = DisconnectReason::decode(&mut &bytes[1..]) {
626                    return Poll::Ready(Some(Err(P2PStreamError::Disconnected(reason))))
627                }
628            }
629
630            if id == P2PMessageID::Ping as u8 || id == P2PMessageID::Pong as u8 {
631                validate_ping_pong_payload(&mut this.decoder, id, &bytes[1..])?;
632
633                if id == P2PMessageID::Ping as u8 {
634                    // Use the timestamp of the first ping for every ping in this poll, so buffered
635                    // pings form one burst. The tradeoff is that a poll that takes more than one
636                    // second can reject a later ping. Considered acceptable.
637                    let now = *ping_batch_time.get_or_insert_with(Instant::now);
638                    if !this.ping_token_bucket.try_take(now) {
639                        return Poll::Ready(Some(Err(P2PStreamError::TooManyPings)))
640                    }
641
642                    trace!("Received Ping, Sending Pong");
643                    this.send_pong();
644                    // This is required because the `Sink` may not be polled externally, and if
645                    // that happens, the pong will never be sent.
646                    cx.waker().wake_by_ref();
647                } else {
648                    // if we were waiting for a pong, this will reset the pinger state
649                    this.pinger.on_pong()?;
650                }
651                continue
652            }
653
654            if id > MAX_RESERVED_MESSAGE_ID && this.shared_capabilities.find_by_offset(id).is_none()
655            {
656                return Poll::Ready(Some(Err(P2PStreamError::UnknownSubprotocolMessageId(id))))
657            }
658
659            // first check that the compressed message length does not exceed the max
660            // payload size
661            let decompressed_len = snap::raw::decompress_len(&bytes[1..])?;
662            if decompressed_len > this.max_payload_size {
663                return Poll::Ready(Some(Err(P2PStreamError::MessageTooBig {
664                    message_size: decompressed_len,
665                    max_size: this.max_payload_size,
666                })))
667            }
668
669            if id == P2PMessageID::Disconnect as u8 &&
670                decompressed_len > MAX_DISCONNECT_PAYLOAD_SIZE
671            {
672                return Poll::Ready(Some(Err(P2PStreamError::MessageTooBig {
673                    message_size: decompressed_len,
674                    max_size: MAX_DISCONNECT_PAYLOAD_SIZE,
675                })))
676            }
677
678            let frame_len = decompressed_len + 1;
679            if let Some(limit) = this.inbound_protocol_limit(id) &&
680                frame_len > limit.max_frame_bytes
681            {
682                counter!("p2pstream.subprotocol_message_too_big").increment(1);
683                return Poll::Ready(Some(Err(P2PStreamError::SubprotocolMessageTooBig {
684                    capability: limit.capability.clone(),
685                    message_size: frame_len,
686                    max_size: limit.max_frame_bytes,
687                })))
688            }
689
690            // create a buffer to hold the decompressed message, adding a byte to the length for
691            // the message ID byte, which is the first byte in this buffer
692            let mut decompress_buf = BytesMut::zeroed(frame_len);
693
694            // each message following a successful handshake is compressed with snappy, so we need
695            // to decompress the message before we can decode it.
696            this.decoder.decompress(&bytes[1..], &mut decompress_buf[1..]).map_err(|err| {
697                debug!(
698                    %err,
699                    len = bytes.len() - 1,
700                    msg = %hex::encode(log_prefix(&bytes[1..])),
701                    "error decompressing p2p message"
702                );
703                err
704            })?;
705
706            match id {
707                _ if id == P2PMessageID::Hello as u8 => {
708                    // we have received a hello message outside of the handshake, so we will return
709                    // an error
710                    return Poll::Ready(Some(Err(P2PStreamError::HandshakeError(
711                        P2PHandshakeError::HelloNotInHandshake,
712                    ))))
713                }
714                _ if id == P2PMessageID::Disconnect as u8 => {
715                    // At this point, the `decompress_buf` contains the snappy decompressed
716                    // disconnect message.
717                    //
718                    // It's possible we already tried to RLP decode this, but it was snappy
719                    // compressed, so we need to RLP decode it again.
720                    let reason = DisconnectReason::decode(&mut &decompress_buf[1..]).inspect_err(|err| {
721                        debug!(
722                            %err, msg=%hex::encode(&decompress_buf[1..]), "Failed to decode disconnect message from peer"
723                        );
724                    })?;
725                    return Poll::Ready(Some(Err(P2PStreamError::Disconnected(reason))))
726                }
727                _ if id > MAX_P2P_MESSAGE_ID && id <= MAX_RESERVED_MESSAGE_ID => {
728                    // we have received an unknown reserved message
729                    return Poll::Ready(Some(Err(P2PStreamError::UnknownReservedMessageId(id))))
730                }
731                _ => {
732                    // we have received a message that is outside the `p2p` reserved message space,
733                    // so it is a subprotocol message.
734
735                    // Peers must be able to identify messages meant for different subprotocols
736                    // using a single message ID byte, and those messages must be distinct from the
737                    // lower-level `p2p` messages.
738                    //
739                    // To ensure that messages for subprotocols are distinct from messages meant
740                    // for the `p2p` capability, message IDs 0x00 - 0x0f are reserved for `p2p`
741                    // messages, so subprotocol messages must have an ID of 0x10 or higher.
742                    //
743                    // To ensure that messages for two different capabilities are distinct from
744                    // each other, all shared capabilities are first ordered lexicographically.
745                    // Message IDs are then reserved in this order, starting at 0x10, reserving a
746                    // message ID for each message the capability supports.
747                    //
748                    // For example, if the shared capabilities are `eth/67` (containing 10
749                    // messages), and "qrs/65" (containing 8 messages):
750                    //
751                    //  * The special case of `p2p`: `p2p` is reserved message IDs 0x00 - 0x0f.
752                    //  * `eth/67` is reserved message IDs 0x10 - 0x19.
753                    //  * `qrs/65` is reserved message IDs 0x1a - 0x21.
754                    //
755                    decompress_buf[0] = bytes[0] - MAX_RESERVED_MESSAGE_ID - 1;
756
757                    return Poll::Ready(Some(Ok(decompress_buf)))
758                }
759            }
760        }
761
762        Poll::Pending
763    }
764}
765
766/// Returns the prefix of a peer message that is included in debug logs.
767pub(crate) fn log_prefix(bytes: &[u8]) -> &[u8] {
768    &bytes[..bytes.len().min(MAX_LOGGED_PAYLOAD_SIZE)]
769}
770
771/// Validates a compressed Ping or Pong payload without allocating from its advertised size.
772fn validate_ping_pong_payload(
773    decoder: &mut snap::raw::Decoder,
774    message_id: u8,
775    compressed_payload: &[u8],
776) -> Result<(), P2PStreamError> {
777    if snap::raw::decompress_len(compressed_payload)? != 1 {
778        return Err(P2PStreamError::InvalidPingPongPayload(message_id))
779    }
780
781    let mut payload = [0u8; 1];
782    decoder.decompress(compressed_payload, &mut payload)?;
783    if payload != [EMPTY_LIST_CODE] {
784        return Err(P2PStreamError::InvalidPingPongPayload(message_id))
785    }
786
787    Ok(())
788}
789
790impl<S> Sink<Bytes> for P2PStream<S>
791where
792    S: Sink<Bytes, Error = io::Error> + Unpin,
793{
794    type Error = P2PStreamError;
795
796    fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
797        let this = self.as_mut().get_mut();
798
799        // Poll the pinger to determine if we should send a ping; `send_ping` and
800        // `start_disconnect` set `needs_control_flush`.
801        match this.pinger.poll_ping(cx) {
802            Poll::Pending => {}
803            Poll::Ready(Ok(PingerEvent::Ping)) => {
804                this.send_ping();
805            }
806            Poll::Ready(Ok(PingerEvent::Timeout) | Err(_)) => {
807                this.start_disconnect(DisconnectReason::PingTimeout)?;
808            }
809        }
810
811        // Control messages (ping/pong/disconnect) must reach the wire even if the caller never
812        // sends a message, so they force a flush. Subprotocol messages are only drained into the
813        // underlying sink (unflushed) once the buffer is full; the caller is responsible for
814        // flushing the batch via `poll_flush`.
815        if self.needs_control_flush {
816            ready!(self.as_mut().poll_flush(cx))?;
817        } else if !self.has_outgoing_capacity() {
818            ready!(self.as_mut().poll_drain_outgoing(cx))?;
819        }
820
821        // both branches above fully drain the queue, and an empty queue always has capacity
822        debug_assert!(self.has_outgoing_capacity());
823        Poll::Ready(Ok(()))
824    }
825
826    fn start_send(self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
827        if item.len() > self.max_payload_size {
828            return Err(P2PStreamError::MessageTooBig {
829                message_size: item.len(),
830                max_size: self.max_payload_size,
831            })
832        }
833
834        if item.is_empty() {
835            // empty messages are not allowed
836            return Err(P2PStreamError::EmptyProtocolMessage)
837        }
838
839        // ensure we have free capacity
840        if !self.has_outgoing_capacity() {
841            return Err(P2PStreamError::SendBufferFull)
842        }
843
844        let this = self.project();
845
846        // all messages sent in this stream are subprotocol messages, so we need to switch the
847        // message id based on the offset
848        let compressed = compress_frame(
849            this.encoder,
850            this.compress_scratch,
851            item[0] + MAX_RESERVED_MESSAGE_ID + 1,
852            &item[1..],
853        )
854        .map_err(|err| {
855            debug!(
856                %err,
857                msg=%hex::encode(&item[1..]),
858                "error compressing p2p message"
859            );
860            err
861        })?;
862        this.outgoing_messages.push_back(compressed);
863
864        Ok(())
865    }
866
867    /// Returns `Poll::Ready(Ok(()))` when no buffered items remain.
868    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
869        ready!(self.as_mut().poll_drain_outgoing(cx))?;
870
871        let mut this = self.project();
872
873        if *this.needs_flush {
874            ready!(this.inner.as_mut().poll_flush(cx))?;
875            *this.needs_flush = false;
876        }
877        *this.needs_control_flush = false;
878
879        Poll::Ready(Ok(()))
880    }
881
882    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
883        ready!(self.as_mut().poll_flush(cx))?;
884        ready!(self.project().inner.poll_close(cx))?;
885
886        Poll::Ready(Ok(()))
887    }
888}
889
890/// This represents only the reserved `p2p` subprotocol messages.
891#[derive(Debug, Clone, PartialEq, Eq)]
892#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
893#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
894#[add_arbitrary_tests(rlp)]
895pub enum P2PMessage {
896    /// The first packet sent over the connection, and sent once by both sides.
897    Hello(HelloMessage),
898
899    /// Inform the peer that a disconnection is imminent; if received, a peer should disconnect
900    /// immediately.
901    Disconnect(DisconnectReason),
902
903    /// Requests an immediate reply of [`P2PMessage::Pong`] from the peer.
904    Ping,
905
906    /// Reply to the peer's [`P2PMessage::Ping`] packet.
907    Pong,
908}
909
910impl P2PMessage {
911    /// Gets the [`P2PMessageID`] for the given message.
912    pub const fn message_id(&self) -> P2PMessageID {
913        match self {
914            Self::Hello(_) => P2PMessageID::Hello,
915            Self::Disconnect(_) => P2PMessageID::Disconnect,
916            Self::Ping => P2PMessageID::Ping,
917            Self::Pong => P2PMessageID::Pong,
918        }
919    }
920}
921
922impl Encodable for P2PMessage {
923    /// The [`Encodable`] implementation for [`P2PMessage::Ping`] and [`P2PMessage::Pong`] encodes
924    /// the message as RLP, and prepends a snappy header to the RLP bytes for all variants except
925    /// the [`P2PMessage::Hello`] variant, because the hello message is never compressed in the
926    /// `p2p` subprotocol.
927    fn encode(&self, out: &mut dyn BufMut) {
928        (self.message_id() as u8).encode(out);
929        match self {
930            Self::Hello(msg) => msg.encode(out),
931            Self::Disconnect(msg) => msg.encode(out),
932            Self::Ping => {
933                // Ping payload is _always_ snappy encoded
934                out.put_slice(SNAPPY_EMPTY_LIST_PAYLOAD);
935            }
936            Self::Pong => {
937                // Pong payload is _always_ snappy encoded
938                out.put_slice(SNAPPY_EMPTY_LIST_PAYLOAD);
939            }
940        }
941    }
942
943    fn length(&self) -> usize {
944        let payload_len = match self {
945            Self::Hello(msg) => msg.length(),
946            Self::Disconnect(msg) => msg.length(),
947            // snappy encoded empty RLP list payload
948            Self::Ping | Self::Pong => SNAPPY_EMPTY_LIST_PAYLOAD.len(),
949        };
950        payload_len + 1 // (1 for length of p2p message id)
951    }
952}
953
954impl Decodable for P2PMessage {
955    /// The [`Decodable`] implementation for [`P2PMessage`] assumes that each of the message
956    /// variants are snappy compressed, except for the [`P2PMessage::Hello`] variant since the
957    /// hello message is never compressed in the `p2p` subprotocol.
958    ///
959    /// The [`Decodable`] implementation for [`P2PMessage::Ping`] and [`P2PMessage::Pong`] expects
960    /// a snappy encoded payload, see [`Encodable`] implementation.
961    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
962        /// Removes the snappy prefix from the Ping/Pong buffer
963        fn advance_snappy_ping_pong_payload(buf: &mut &[u8]) -> alloy_rlp::Result<()> {
964            if buf.len() < 3 {
965                return Err(RlpError::InputTooShort)
966            }
967            if buf[..3] != [0x01, 0x00, EMPTY_LIST_CODE] {
968                return Err(RlpError::Custom("expected snappy payload"))
969            }
970            buf.advance(3);
971            Ok(())
972        }
973
974        let message_id = u8::decode(&mut &buf[..])?;
975        let id = P2PMessageID::try_from(message_id)
976            .or(Err(RlpError::Custom("unknown p2p message id")))?;
977        buf.advance(1);
978        match id {
979            P2PMessageID::Hello => Ok(Self::Hello(HelloMessage::decode(buf)?)),
980            P2PMessageID::Disconnect => Ok(Self::Disconnect(DisconnectReason::decode(buf)?)),
981            P2PMessageID::Ping => {
982                advance_snappy_ping_pong_payload(buf)?;
983                Ok(Self::Ping)
984            }
985            P2PMessageID::Pong => {
986                advance_snappy_ping_pong_payload(buf)?;
987                Ok(Self::Pong)
988            }
989        }
990    }
991}
992
993/// Message IDs for `p2p` subprotocol messages.
994#[derive(Debug, Copy, Clone, Eq, PartialEq)]
995pub enum P2PMessageID {
996    /// Message ID for the [`P2PMessage::Hello`] message.
997    Hello = 0x00,
998
999    /// Message ID for the [`P2PMessage::Disconnect`] message.
1000    Disconnect = 0x01,
1001
1002    /// Message ID for the [`P2PMessage::Ping`] message.
1003    Ping = 0x02,
1004
1005    /// Message ID for the [`P2PMessage::Pong`] message.
1006    Pong = 0x03,
1007}
1008
1009impl From<P2PMessage> for P2PMessageID {
1010    fn from(msg: P2PMessage) -> Self {
1011        match msg {
1012            P2PMessage::Hello(_) => Self::Hello,
1013            P2PMessage::Disconnect(_) => Self::Disconnect,
1014            P2PMessage::Ping => Self::Ping,
1015            P2PMessage::Pong => Self::Pong,
1016        }
1017    }
1018}
1019
1020impl TryFrom<u8> for P2PMessageID {
1021    type Error = P2PStreamError;
1022
1023    fn try_from(id: u8) -> Result<Self, Self::Error> {
1024        match id {
1025            0x00 => Ok(Self::Hello),
1026            0x01 => Ok(Self::Disconnect),
1027            0x02 => Ok(Self::Ping),
1028            0x03 => Ok(Self::Pong),
1029            _ => Err(P2PStreamError::UnknownReservedMessageId(id)),
1030        }
1031    }
1032}
1033
1034/// Snappy-compresses an id-prefixed `p2p` message payload into a frame carrying the given wire
1035/// message id.
1036///
1037/// Frames whose worst-case compressed size fits within [`MAX_COMPRESS_SCRATCH_SIZE`] are
1038/// compressed through the reusable `scratch` buffer and copied out at their exact size; larger
1039/// frames use a one-off allocation, see [`MAX_COMPRESS_SCRATCH_SIZE`].
1040fn compress_frame(
1041    encoder: &mut snap::raw::Encoder,
1042    scratch: &mut Vec<u8>,
1043    wire_id: u8,
1044    payload: &[u8],
1045) -> Result<Bytes, snap::Error> {
1046    let needed = 1 + snap::raw::max_compress_len(payload.len());
1047
1048    if needed > MAX_COMPRESS_SCRATCH_SIZE {
1049        let mut compressed = vec![0u8; needed];
1050        let compressed_size = encoder.compress(payload, &mut compressed[1..])?;
1051        compressed[0] = wire_id;
1052        compressed.truncate(compressed_size + 1);
1053        return Ok(compressed.into())
1054    }
1055
1056    if scratch.len() < needed {
1057        scratch.resize(needed, 0);
1058    }
1059    let compressed_size = encoder.compress(payload, &mut scratch[1..])?;
1060    scratch[0] = wire_id;
1061    Ok(Bytes::copy_from_slice(&scratch[..compressed_size + 1]))
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066    use super::*;
1067    use crate::{
1068        capability::SharedCapability, protocol::Protocol, test_utils::eth_hello, Capability,
1069        EthVersion, ProtocolVersion,
1070    };
1071    use futures::task::noop_waker_ref;
1072    use tokio::net::{TcpListener, TcpStream};
1073    use tokio_util::codec::Decoder;
1074
1075    /// A sink that records started frames and counts flushes, to observe batching behavior.
1076    #[derive(Default)]
1077    struct FlushCountingTransport {
1078        incoming: VecDeque<io::Result<BytesMut>>,
1079        sent: Vec<Bytes>,
1080        flushes: usize,
1081    }
1082
1083    #[derive(Default)]
1084    struct InboundTransport {
1085        incoming: VecDeque<BytesMut>,
1086    }
1087
1088    impl Stream for InboundTransport {
1089        type Item = io::Result<BytesMut>;
1090
1091        fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1092            Poll::Ready(self.incoming.pop_front().map(Ok))
1093        }
1094    }
1095
1096    impl Sink<Bytes> for InboundTransport {
1097        type Error = io::Error;
1098
1099        fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1100            Poll::Ready(Ok(()))
1101        }
1102
1103        fn start_send(self: Pin<&mut Self>, _item: Bytes) -> Result<(), Self::Error> {
1104            Ok(())
1105        }
1106
1107        fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1108            Poll::Ready(Ok(()))
1109        }
1110
1111        fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1112            Poll::Ready(Ok(()))
1113        }
1114    }
1115
1116    impl Stream for FlushCountingTransport {
1117        type Item = io::Result<BytesMut>;
1118
1119        fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1120            match self.incoming.pop_front() {
1121                Some(item) => Poll::Ready(Some(item)),
1122                None => Poll::Pending,
1123            }
1124        }
1125    }
1126
1127    impl Sink<Bytes> for FlushCountingTransport {
1128        type Error = io::Error;
1129
1130        fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1131            Poll::Ready(Ok(()))
1132        }
1133
1134        fn start_send(mut self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
1135            self.sent.push(item);
1136            Ok(())
1137        }
1138
1139        fn poll_flush(
1140            mut self: Pin<&mut Self>,
1141            _: &mut Context<'_>,
1142        ) -> Poll<Result<(), Self::Error>> {
1143            self.flushes += 1;
1144            Poll::Ready(Ok(()))
1145        }
1146
1147        fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1148            Poll::Ready(Ok(()))
1149        }
1150    }
1151
1152    fn eth_shared_capabilities() -> SharedCapabilities {
1153        SharedCapabilities::try_new(vec![EthVersion::Eth68.into()], vec![Capability::eth_68()])
1154            .unwrap()
1155    }
1156
1157    fn stream_with_incoming(frame: BytesMut) -> P2PStream<FlushCountingTransport> {
1158        let mut transport = FlushCountingTransport::default();
1159        transport.incoming.push_back(Ok(frame));
1160        P2PStream::new(transport, eth_shared_capabilities())
1161    }
1162
1163    fn stream_with_incoming_pings(count: usize) -> P2PStream<FlushCountingTransport> {
1164        let mut transport = FlushCountingTransport::default();
1165        transport.incoming.extend((0..count).map(|_| Ok(BytesMut::from(SNAPPY_PING_MESSAGE))));
1166        P2PStream::new(transport, eth_shared_capabilities())
1167    }
1168
1169    fn compressed_p2p_message(message_id: P2PMessageID, payload: &[u8]) -> BytesMut {
1170        let mut encoder = snap::raw::Encoder::new();
1171        let mut scratch = Vec::new();
1172        let message =
1173            compress_frame(&mut encoder, &mut scratch, message_id as u8, payload).unwrap();
1174        BytesMut::from(message.as_ref())
1175    }
1176
1177    #[tokio::test]
1178    async fn rejects_ping_pong_with_oversized_payload_before_decompression() {
1179        const SIXTEEN_MIB_SNAPPY_HEADER: [u8; 4] = [0x80, 0x80, 0x80, 0x08];
1180
1181        for message_id in [P2PMessageID::Ping, P2PMessageID::Pong] {
1182            let frame = BytesMut::from(
1183                [
1184                    message_id as u8,
1185                    SIXTEEN_MIB_SNAPPY_HEADER[0],
1186                    SIXTEEN_MIB_SNAPPY_HEADER[1],
1187                    SIXTEEN_MIB_SNAPPY_HEADER[2],
1188                    SIXTEEN_MIB_SNAPPY_HEADER[3],
1189                ]
1190                .as_slice(),
1191            );
1192            assert_eq!(snap::raw::decompress_len(&frame[1..]).unwrap(), MAX_PAYLOAD_SIZE);
1193
1194            let mut stream = stream_with_incoming(frame);
1195            let waker = noop_waker_ref();
1196            let mut cx = Context::from_waker(waker);
1197
1198            match Pin::new(&mut stream).poll_next(&mut cx) {
1199                Poll::Ready(Some(Err(P2PStreamError::InvalidPingPongPayload(id)))) => {
1200                    assert_eq!(id, message_id as u8)
1201                }
1202                result => panic!("unexpected poll result: {result:?}"),
1203            }
1204            assert!(stream.outgoing_messages.is_empty());
1205        }
1206    }
1207
1208    #[tokio::test]
1209    async fn rejects_oversized_disconnect_before_decompression() {
1210        // A snappy header advertising a 16 MiB payload, with no compressed body behind it.
1211        let frame =
1212            BytesMut::from([P2PMessageID::Disconnect as u8, 0x80, 0x80, 0x80, 0x08].as_slice());
1213        let mut stream = stream_with_incoming(frame);
1214
1215        assert!(matches!(
1216            stream.next().await.unwrap().unwrap_err(),
1217            P2PStreamError::MessageTooBig {
1218                message_size: MAX_PAYLOAD_SIZE,
1219                max_size: MAX_DISCONNECT_PAYLOAD_SIZE,
1220            }
1221        ));
1222    }
1223
1224    #[tokio::test]
1225    async fn rejects_ping_pong_with_non_list_payload() {
1226        for message_id in [P2PMessageID::Ping, P2PMessageID::Pong] {
1227            let frame = compressed_p2p_message(message_id, &[alloy_rlp::EMPTY_STRING_CODE]);
1228            let mut stream = stream_with_incoming(frame);
1229            let waker = noop_waker_ref();
1230            let mut cx = Context::from_waker(waker);
1231
1232            assert!(matches!(
1233                Pin::new(&mut stream).poll_next(&mut cx),
1234                Poll::Ready(Some(Err(P2PStreamError::InvalidPingPongPayload(id))))
1235                    if id == message_id as u8
1236            ));
1237            assert!(stream.outgoing_messages.is_empty());
1238        }
1239    }
1240
1241    #[tokio::test]
1242    async fn accepts_ping_with_empty_list_payload() {
1243        let mut stream = stream_with_incoming(BytesMut::from(SNAPPY_PING_MESSAGE));
1244        let waker = noop_waker_ref();
1245        let mut cx = Context::from_waker(waker);
1246
1247        assert!(Pin::new(&mut stream).poll_next(&mut cx).is_pending());
1248        assert_eq!(stream.outgoing_messages.len(), 1);
1249        assert_eq!(stream.outgoing_messages.front().unwrap().as_ref(), SNAPPY_PONG_MESSAGE);
1250    }
1251
1252    #[test]
1253    fn ping_token_bucket_limits_bursts_and_refills() {
1254        let now = Instant::now();
1255        let mut bucket = PingTokenBucket::new(now);
1256
1257        for _ in 0..PING_TOKEN_BUCKET_CAPACITY {
1258            assert!(bucket.try_take(now));
1259        }
1260        assert!(!bucket.try_take(now));
1261
1262        let one_refill = now + Duration::from_secs(1);
1263        assert!(bucket.try_take(one_refill));
1264        assert!(!bucket.try_take(one_refill));
1265
1266        // The extra half second checks that refill time accumulated while the bucket is full is
1267        // discarded. Otherwise, the next token could arrive less than one second after the burst.
1268        let full_refill = one_refill +
1269            Duration::from_secs(u64::from(PING_TOKEN_BUCKET_CAPACITY)) +
1270            Duration::from_millis(500);
1271        for _ in 0..PING_TOKEN_BUCKET_CAPACITY {
1272            assert!(bucket.try_take(full_refill));
1273        }
1274        assert!(!bucket.try_take(full_refill));
1275        assert!(!bucket.try_take(full_refill + Duration::from_millis(999)));
1276        assert!(bucket.try_take(full_refill + Duration::from_secs(1)));
1277    }
1278
1279    #[tokio::test]
1280    async fn accepts_ping_burst_at_token_bucket_capacity() {
1281        let mut stream = stream_with_incoming_pings(usize::from(PING_TOKEN_BUCKET_CAPACITY));
1282        let waker = noop_waker_ref();
1283        let mut cx = Context::from_waker(waker);
1284
1285        assert!(Pin::new(&mut stream).poll_next(&mut cx).is_pending());
1286        assert_eq!(stream.outgoing_messages.len(), usize::from(PING_TOKEN_BUCKET_CAPACITY));
1287    }
1288
1289    #[tokio::test]
1290    async fn rejects_ping_burst_over_token_bucket_capacity() {
1291        let mut stream = stream_with_incoming_pings(usize::from(PING_TOKEN_BUCKET_CAPACITY) + 1);
1292        let waker = noop_waker_ref();
1293        let mut cx = Context::from_waker(waker);
1294
1295        assert!(matches!(
1296            Pin::new(&mut stream).poll_next(&mut cx),
1297            Poll::Ready(Some(Err(P2PStreamError::TooManyPings)))
1298        ));
1299        assert_eq!(stream.outgoing_messages.len(), usize::from(PING_TOKEN_BUCKET_CAPACITY));
1300    }
1301
1302    #[tokio::test]
1303    async fn rejects_subprotocol_frame_before_decompression_when_declared_size_exceeds_limit() {
1304        let cap = Capability::new_static("test", 1);
1305        let shared_capabilities =
1306            SharedCapabilities::try_new(vec![Protocol::new(cap.clone(), 1)], vec![cap.clone()])
1307                .unwrap();
1308        let shared_capability = shared_capabilities.find(&cap).unwrap().clone();
1309        let wire_id = shared_capability.message_id_offset();
1310        let mut encoder = snap::raw::Encoder::new();
1311        let mut scratch = Vec::new();
1312        let accepted = compress_frame(&mut encoder, &mut scratch, wire_id, &[0; 3]).unwrap();
1313        let oversized = compress_frame(&mut encoder, &mut scratch, wire_id, &[0; 4]).unwrap();
1314        let transport = InboundTransport {
1315            incoming: [accepted, oversized]
1316                .into_iter()
1317                .map(|frame| BytesMut::from(frame.as_ref()))
1318                .collect(),
1319        };
1320        let mut stream = P2PStream::new(transport, shared_capabilities);
1321        stream.set_protocol_ingress_limits(&shared_capability, ProtocolIngressLimits::new(4));
1322
1323        let frame = stream.next().await.unwrap().unwrap();
1324        assert_eq!(frame.len(), 4);
1325
1326        assert!(matches!(
1327            stream.next().await.unwrap().unwrap_err(),
1328            P2PStreamError::SubprotocolMessageTooBig {
1329                capability,
1330                message_size: 5,
1331                max_size: 4,
1332            } if capability == cap
1333        ));
1334    }
1335
1336    #[tokio::test]
1337    async fn zero_message_protocol_does_not_replace_neighboring_frame_limit() {
1338        let zero = Capability::new_static("aaa", 1);
1339        let limited = Capability::new_static("bbb", 1);
1340        let shared_capabilities = SharedCapabilities::try_new(
1341            vec![Protocol::new(zero.clone(), 0), Protocol::new(limited.clone(), 1)],
1342            vec![zero.clone(), limited.clone()],
1343        )
1344        .unwrap();
1345        let zero_shared = shared_capabilities.find(&zero).unwrap().clone();
1346        let limited_shared = shared_capabilities.find(&limited).unwrap().clone();
1347        assert_eq!(zero_shared.message_id_offset(), limited_shared.message_id_offset());
1348
1349        let mut encoder = snap::raw::Encoder::new();
1350        let mut scratch = Vec::new();
1351        let oversized =
1352            compress_frame(&mut encoder, &mut scratch, limited_shared.message_id_offset(), &[0; 4])
1353                .unwrap();
1354        let transport =
1355            InboundTransport { incoming: VecDeque::from([BytesMut::from(oversized.as_ref())]) };
1356        let mut stream = P2PStream::new(transport, shared_capabilities);
1357        stream.set_protocol_ingress_limits(&limited_shared, ProtocolIngressLimits::new(4));
1358        stream.set_protocol_ingress_limits(&zero_shared, ProtocolIngressLimits::new(1));
1359
1360        assert!(matches!(
1361            stream.next().await.unwrap().unwrap_err(),
1362            P2PStreamError::SubprotocolMessageTooBig {
1363                capability,
1364                message_size: 5,
1365                max_size: 4,
1366            } if capability == limited
1367        ));
1368    }
1369
1370    #[tokio::test]
1371    async fn poll_ready_drains_full_subprotocol_queue_without_flushing_inner() {
1372        let mut stream =
1373            P2PStream::new(FlushCountingTransport::default(), eth_shared_capabilities());
1374        stream.set_outgoing_message_buffer_capacity(1);
1375        Pin::new(&mut stream).start_send(Bytes::from_static(&[0x00, EMPTY_LIST_CODE])).unwrap();
1376
1377        let waker = noop_waker_ref();
1378        let mut cx = Context::from_waker(waker);
1379        assert!(Pin::new(&mut stream).poll_ready(&mut cx).is_ready());
1380
1381        // the full queue was drained into the inner sink to make room, but not flushed
1382        assert_eq!(stream.inner().sent.len(), 1);
1383        assert_eq!(stream.inner().flushes, 0);
1384
1385        // the caller-driven flush pushes the batch out with a single inner flush
1386        assert!(Pin::new(&mut stream).poll_flush(&mut cx).is_ready());
1387        assert_eq!(stream.inner().flushes, 1);
1388
1389        // flushing again is a no-op on the inner sink
1390        assert!(Pin::new(&mut stream).poll_flush(&mut cx).is_ready());
1391        assert_eq!(stream.inner().flushes, 1);
1392    }
1393
1394    #[tokio::test]
1395    async fn poll_ready_flushes_queued_control_messages() {
1396        let mut stream =
1397            P2PStream::new(FlushCountingTransport::default(), eth_shared_capabilities());
1398        stream.send_ping();
1399
1400        let waker = noop_waker_ref();
1401        let mut cx = Context::from_waker(waker);
1402        assert!(Pin::new(&mut stream).poll_ready(&mut cx).is_ready());
1403
1404        // control messages must not wait for a caller-driven flush
1405        assert_eq!(stream.inner().sent.len(), 1);
1406        assert_eq!(stream.inner().flushes, 1);
1407    }
1408
1409    #[tokio::test]
1410    async fn test_can_disconnect() {
1411        reth_tracing::init_test_tracing();
1412        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1413        let local_addr = listener.local_addr().unwrap();
1414
1415        let expected_disconnect = DisconnectReason::UselessPeer;
1416
1417        let handle = tokio::spawn(async move {
1418            // roughly based off of the design of tokio::net::TcpListener
1419            let (incoming, _) = listener.accept().await.unwrap();
1420            let stream = crate::PassthroughCodec::default().framed(incoming);
1421
1422            let (server_hello, _) = eth_hello();
1423
1424            let (mut p2p_stream, _) =
1425                UnauthedP2PStream::new(stream).handshake(server_hello).await.unwrap();
1426
1427            p2p_stream.disconnect(expected_disconnect).await.unwrap();
1428        });
1429
1430        let outgoing = TcpStream::connect(local_addr).await.unwrap();
1431        let sink = crate::PassthroughCodec::default().framed(outgoing);
1432
1433        let (client_hello, _) = eth_hello();
1434
1435        let (mut p2p_stream, _) =
1436            UnauthedP2PStream::new(sink).handshake(client_hello).await.unwrap();
1437
1438        let err = p2p_stream.next().await.unwrap().unwrap_err();
1439        match err {
1440            P2PStreamError::Disconnected(reason) => assert_eq!(reason, expected_disconnect),
1441            e => panic!("unexpected err: {e}"),
1442        }
1443
1444        handle.await.unwrap();
1445    }
1446
1447    #[tokio::test]
1448    async fn test_can_disconnect_weird_disconnect_encoding() {
1449        reth_tracing::init_test_tracing();
1450        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1451        let local_addr = listener.local_addr().unwrap();
1452
1453        let expected_disconnect = DisconnectReason::SubprotocolSpecific;
1454
1455        let handle = tokio::spawn(async move {
1456            // roughly based off of the design of tokio::net::TcpListener
1457            let (incoming, _) = listener.accept().await.unwrap();
1458            let stream = crate::PassthroughCodec::default().framed(incoming);
1459
1460            let (server_hello, _) = eth_hello();
1461
1462            let (mut p2p_stream, _) =
1463                UnauthedP2PStream::new(stream).handshake(server_hello).await.unwrap();
1464
1465            // Unrolled `disconnect` method, without compression
1466            p2p_stream.outgoing_messages.clear();
1467
1468            p2p_stream.outgoing_messages.push_back(Bytes::from(alloy_rlp::encode(
1469                P2PMessage::Disconnect(DisconnectReason::SubprotocolSpecific),
1470            )));
1471            p2p_stream.disconnecting = true;
1472            p2p_stream.close().await.unwrap();
1473        });
1474
1475        let outgoing = TcpStream::connect(local_addr).await.unwrap();
1476        let sink = crate::PassthroughCodec::default().framed(outgoing);
1477
1478        let (client_hello, _) = eth_hello();
1479
1480        let (mut p2p_stream, _) =
1481            UnauthedP2PStream::new(sink).handshake(client_hello).await.unwrap();
1482
1483        let err = p2p_stream.next().await.unwrap().unwrap_err();
1484        match err {
1485            P2PStreamError::Disconnected(reason) => assert_eq!(reason, expected_disconnect),
1486            e => panic!("unexpected err: {e}"),
1487        }
1488
1489        handle.await.unwrap();
1490    }
1491
1492    #[tokio::test]
1493    async fn test_handshake_passthrough() {
1494        // create a p2p stream and server, then confirm that the two are authed
1495        // create tcpstream
1496        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1497        let local_addr = listener.local_addr().unwrap();
1498
1499        let handle = tokio::spawn(async move {
1500            // roughly based off of the design of tokio::net::TcpListener
1501            let (incoming, _) = listener.accept().await.unwrap();
1502            let stream = crate::PassthroughCodec::default().framed(incoming);
1503
1504            let (server_hello, _) = eth_hello();
1505
1506            let unauthed_stream = UnauthedP2PStream::new(stream);
1507            let (p2p_stream, _) = unauthed_stream.handshake(server_hello).await.unwrap();
1508
1509            // ensure that the two share a single capability, eth67
1510            assert_eq!(
1511                *p2p_stream.shared_capabilities.iter_caps().next().unwrap(),
1512                SharedCapability::Eth {
1513                    version: EthVersion::Eth67,
1514                    offset: MAX_RESERVED_MESSAGE_ID + 1
1515                }
1516            );
1517        });
1518
1519        let outgoing = TcpStream::connect(local_addr).await.unwrap();
1520        let sink = crate::PassthroughCodec::default().framed(outgoing);
1521
1522        let (client_hello, _) = eth_hello();
1523
1524        let unauthed_stream = UnauthedP2PStream::new(sink);
1525        let (p2p_stream, _) = unauthed_stream.handshake(client_hello).await.unwrap();
1526
1527        // ensure that the two share a single capability, eth67
1528        assert_eq!(
1529            *p2p_stream.shared_capabilities.iter_caps().next().unwrap(),
1530            SharedCapability::Eth {
1531                version: EthVersion::Eth67,
1532                offset: MAX_RESERVED_MESSAGE_ID + 1
1533            }
1534        );
1535
1536        // make sure the server receives the message and asserts before ending the test
1537        handle.await.unwrap();
1538    }
1539
1540    /// An `eth` message larger than [`MAX_PAYLOAD_SIZE`], as the stream yields and accepts it: a
1541    /// relative message id (0) followed by the payload.
1542    fn oversized_eth_message() -> Vec<u8> {
1543        vec![0u8; MAX_PAYLOAD_SIZE + 4 * 1024 * 1024]
1544    }
1545
1546    #[tokio::test]
1547    async fn max_payload_size_bounds_both_directions() {
1548        let message = oversized_eth_message();
1549        let mut encoder = snap::raw::Encoder::new();
1550        let wire_id = MAX_RESERVED_MESSAGE_ID + 1;
1551        let frame = compress_frame(&mut encoder, &mut Vec::new(), wire_id, &message[1..])
1552            .map(|frame| BytesMut::from(frame.as_ref()))
1553            .unwrap();
1554        let payload_len = message.len() - 1;
1555
1556        let mut default_stream = stream_with_incoming(frame.clone());
1557        assert!(matches!(
1558            default_stream.next().await.unwrap().unwrap_err(),
1559            P2PStreamError::MessageTooBig { message_size, max_size: MAX_PAYLOAD_SIZE }
1560                if message_size == payload_len
1561        ));
1562        assert!(matches!(
1563            Pin::new(&mut default_stream).start_send(Bytes::from(message.clone())).unwrap_err(),
1564            P2PStreamError::MessageTooBig { max_size: MAX_PAYLOAD_SIZE, .. }
1565        ));
1566
1567        let mut raised_stream = stream_with_incoming(frame);
1568        raised_stream.set_max_payload_size(2 * MAX_PAYLOAD_SIZE);
1569        assert!(raised_stream.next().await.unwrap().unwrap().as_ref() == message.as_slice());
1570        Pin::new(&mut raised_stream).start_send(Bytes::from(message)).unwrap();
1571    }
1572
1573    #[tokio::test]
1574    async fn handshake_carries_max_payload_size_into_the_stream() {
1575        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1576        let local_addr = listener.local_addr().unwrap();
1577        let message = oversized_eth_message();
1578        let expected = message.clone();
1579
1580        let handle = tokio::spawn(async move {
1581            let (incoming, _) = listener.accept().await.unwrap();
1582            let stream = crate::PassthroughCodec::default().framed(incoming);
1583            let (server_hello, _) = eth_hello();
1584            let (mut p2p_stream, _) = UnauthedP2PStream::new(stream)
1585                .with_max_payload_size(2 * MAX_PAYLOAD_SIZE)
1586                .handshake(server_hello)
1587                .await
1588                .unwrap();
1589            assert!(p2p_stream.next().await.unwrap().unwrap().as_ref() == expected.as_slice());
1590        });
1591
1592        let outgoing = TcpStream::connect(local_addr).await.unwrap();
1593        let sink = crate::PassthroughCodec::default().framed(outgoing);
1594        let (client_hello, _) = eth_hello();
1595        let (mut p2p_stream, _) = UnauthedP2PStream::new(sink)
1596            .with_max_payload_size(2 * MAX_PAYLOAD_SIZE)
1597            .handshake(client_hello)
1598            .await
1599            .unwrap();
1600        p2p_stream.send(Bytes::from(message)).await.unwrap();
1601
1602        handle.await.unwrap();
1603    }
1604
1605    #[tokio::test]
1606    async fn test_handshake_disconnect() {
1607        // create a p2p stream and server, then confirm that the two are authed
1608        // create tcpstream
1609        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1610        let local_addr = listener.local_addr().unwrap();
1611
1612        let handle = tokio::spawn(async move {
1613            // roughly based off of the design of tokio::net::TcpListener
1614            let (incoming, _) = listener.accept().await.unwrap();
1615            let stream = crate::PassthroughCodec::default().framed(incoming);
1616
1617            let (server_hello, _) = eth_hello();
1618
1619            let unauthed_stream = UnauthedP2PStream::new(stream);
1620            match unauthed_stream.handshake(server_hello.clone()).await {
1621                Ok((_, hello)) => {
1622                    panic!("expected handshake to fail, instead got a successful Hello: {hello:?}")
1623                }
1624                Err(P2PStreamError::MismatchedProtocolVersion(GotExpected { got, expected })) => {
1625                    assert_ne!(expected, got);
1626                    assert_eq!(expected, server_hello.protocol_version);
1627                }
1628                Err(other_err) => {
1629                    panic!("expected mismatched protocol version error, got {other_err:?}")
1630                }
1631            }
1632        });
1633
1634        let outgoing = TcpStream::connect(local_addr).await.unwrap();
1635        let sink = crate::PassthroughCodec::default().framed(outgoing);
1636
1637        let (mut client_hello, _) = eth_hello();
1638
1639        // modify the hello to include an incompatible p2p protocol version
1640        client_hello.protocol_version = ProtocolVersion::V4;
1641
1642        let unauthed_stream = UnauthedP2PStream::new(sink);
1643        match unauthed_stream.handshake(client_hello.clone()).await {
1644            Ok((_, hello)) => {
1645                panic!("expected handshake to fail, instead got a successful Hello: {hello:?}")
1646            }
1647            Err(P2PStreamError::MismatchedProtocolVersion(GotExpected { got, expected })) => {
1648                assert_ne!(expected, got);
1649                assert_eq!(expected, client_hello.protocol_version);
1650            }
1651            Err(other_err) => {
1652                panic!("expected mismatched protocol version error, got {other_err:?}")
1653            }
1654        }
1655
1656        // make sure the server receives the message and asserts before ending the test
1657        handle.await.unwrap();
1658    }
1659
1660    #[test]
1661    fn snappy_ping_pong_consts_match_rlp_encoding() {
1662        assert_eq!(alloy_rlp::encode(P2PMessage::Ping).as_slice(), SNAPPY_PING_MESSAGE);
1663        assert_eq!(alloy_rlp::encode(P2PMessage::Pong).as_slice(), SNAPPY_PONG_MESSAGE);
1664    }
1665
1666    #[test]
1667    fn snappy_decode_encode_ping() {
1668        let snappy_ping = b"\x02\x01\0\xc0";
1669        let ping = P2PMessage::decode(&mut &snappy_ping[..]).unwrap();
1670        assert!(matches!(ping, P2PMessage::Ping));
1671        assert_eq!(alloy_rlp::encode(ping), &snappy_ping[..]);
1672    }
1673
1674    #[test]
1675    fn snappy_decode_encode_pong() {
1676        let snappy_pong = b"\x03\x01\0\xc0";
1677        let pong = P2PMessage::decode(&mut &snappy_pong[..]).unwrap();
1678        assert!(matches!(pong, P2PMessage::Pong));
1679        assert_eq!(alloy_rlp::encode(pong), &snappy_pong[..]);
1680    }
1681}