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    DisconnectReason, HelloMessage, HelloMessageWithProtocols,
7};
8use alloy_primitives::{
9    bytes::{Buf, BufMut, Bytes, BytesMut},
10    hex,
11};
12use alloy_rlp::{Decodable, Encodable, Error as RlpError, EMPTY_LIST_CODE};
13use futures::{Sink, SinkExt, StreamExt};
14use pin_project::pin_project;
15use reth_codecs::add_arbitrary_tests;
16use reth_metrics::metrics::counter;
17use reth_primitives_traits::GotExpected;
18use std::{
19    collections::VecDeque,
20    future::Future,
21    io,
22    pin::Pin,
23    task::{ready, Context, Poll},
24    time::Duration,
25};
26use tokio_stream::Stream;
27use tracing::{debug, trace};
28
29#[cfg(feature = "serde")]
30use serde::{Deserialize, Serialize};
31
32/// [`MAX_PAYLOAD_SIZE`] is the maximum size of an uncompressed message payload.
33/// This is defined in [EIP-706](https://eips.ethereum.org/EIPS/eip-706).
34const MAX_PAYLOAD_SIZE: usize = 16 * 1024 * 1024;
35
36/// [`MAX_RESERVED_MESSAGE_ID`] is the maximum message ID reserved for the `p2p` subprotocol. If
37/// there are any incoming messages with an ID greater than this, they are subprotocol messages.
38pub const MAX_RESERVED_MESSAGE_ID: u8 = 0x0f;
39
40/// [`MAX_P2P_MESSAGE_ID`] is the maximum message ID in use for the `p2p` subprotocol.
41const MAX_P2P_MESSAGE_ID: u8 = P2PMessageID::Pong as u8;
42
43/// Snappy framed RLP empty list payload used by fixed `p2p` ping/pong control messages.
44const SNAPPY_EMPTY_LIST_PAYLOAD: &[u8] = &[0x01, 0x00, EMPTY_LIST_CODE];
45
46/// Wire-encoded `p2p` ping control message.
47const SNAPPY_PING_MESSAGE: &[u8] = &[0x02, 0x01, 0x00, EMPTY_LIST_CODE];
48
49/// Wire-encoded `p2p` pong control message.
50const SNAPPY_PONG_MESSAGE: &[u8] = &[0x03, 0x01, 0x00, EMPTY_LIST_CODE];
51
52/// [`HANDSHAKE_TIMEOUT`] determines the amount of time to wait before determining that a `p2p`
53/// handshake has timed out.
54pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
55
56/// [`PING_TIMEOUT`] determines the amount of time to wait before determining that a `p2p` ping has
57/// timed out.
58const PING_TIMEOUT: Duration = Duration::from_secs(15);
59
60/// [`PING_INTERVAL`] determines the amount of time to wait between sending `p2p` ping messages
61/// when the peer is responsive.
62const PING_INTERVAL: Duration = Duration::from_secs(60);
63
64/// [`MAX_P2P_CAPACITY`] is the maximum number of messages that can be buffered to be sent in the
65/// `p2p` stream.
66///
67/// Note: this default is rather low because it is expected that the [`P2PStream`] wraps an
68/// [`ECIESStream`](reth_ecies::stream::ECIESStream) which internally already buffers a few MB of
69/// encoded data.
70const MAX_P2P_CAPACITY: usize = 2;
71
72/// Maximum size of the reusable compression scratch buffer in [`P2PStream`], covering the snappy
73/// worst case of typical broadcast messages (soft-capped around 128KiB).
74///
75/// Messages with a larger compressed worst case are compressed through a one-off allocation
76/// instead, so a single oversized message neither grows the scratch buffer for the connection's
77/// lifetime nor causes shrink/regrow churn, see [`compress_frame`].
78const MAX_COMPRESS_SCRATCH_SIZE: usize = 256 * 1024;
79
80/// An un-authenticated [`P2PStream`]. This is consumed and returns a [`P2PStream`] after the
81/// `Hello` handshake is completed.
82#[pin_project]
83#[derive(Debug)]
84pub struct UnauthedP2PStream<S> {
85    #[pin]
86    inner: S,
87}
88
89impl<S> UnauthedP2PStream<S> {
90    /// Create a new `UnauthedP2PStream` from a type `S` which implements `Stream` and `Sink`.
91    pub const fn new(inner: S) -> Self {
92        Self { inner }
93    }
94
95    /// Returns a reference to the inner stream.
96    pub const fn inner(&self) -> &S {
97        &self.inner
98    }
99}
100
101impl<S> UnauthedP2PStream<S>
102where
103    S: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin,
104{
105    /// Consumes the `UnauthedP2PStream` and returns a `P2PStream` after the `Hello` handshake is
106    /// completed successfully. This also returns the `Hello` message sent by the remote peer.
107    pub async fn handshake(
108        mut self,
109        hello: HelloMessageWithProtocols,
110    ) -> Result<(P2PStream<S>, HelloMessage), P2PStreamError> {
111        trace!(?hello, "sending p2p hello to peer");
112
113        // send our hello message with the Sink
114        self.inner.send(alloy_rlp::encode(P2PMessage::Hello(hello.message())).into()).await?;
115
116        let first_message_bytes = tokio::time::timeout(HANDSHAKE_TIMEOUT, self.inner.next())
117            .await
118            .or(Err(P2PStreamError::HandshakeError(P2PHandshakeError::Timeout)))?
119            .ok_or(P2PStreamError::HandshakeError(P2PHandshakeError::NoResponse))??;
120
121        // Check that the uncompressed message length does not exceed the max payload size.
122        // Note: The first message (Hello/Disconnect) is not snappy compressed. We will check the
123        // decompressed length again for subsequent messages after the handshake.
124        if first_message_bytes.len() > MAX_PAYLOAD_SIZE {
125            return Err(P2PStreamError::MessageTooBig {
126                message_size: first_message_bytes.len(),
127                max_size: MAX_PAYLOAD_SIZE,
128            })
129        }
130
131        // The first message sent MUST be a hello OR disconnect message
132        //
133        // If the first message is a disconnect message, we should not decode using
134        // Decodable::decode, because the first message (either Disconnect or Hello) is not snappy
135        // compressed, and the Decodable implementation assumes that non-hello messages are snappy
136        // compressed.
137        let their_hello = match P2PMessage::decode(&mut &first_message_bytes[..]) {
138            Ok(P2PMessage::Hello(hello)) => Ok(hello),
139            Ok(P2PMessage::Disconnect(reason)) => {
140                if matches!(reason, DisconnectReason::TooManyPeers) {
141                    // Too many peers is a very common disconnect reason that spams the DEBUG logs
142                    trace!(%reason, "Disconnected by peer during handshake");
143                } else {
144                    debug!(%reason, "Disconnected by peer during handshake");
145                };
146                counter!("p2pstream.disconnected_errors").increment(1);
147                Err(P2PStreamError::HandshakeError(P2PHandshakeError::Disconnected(reason)))
148            }
149            Err(err) => {
150                debug!(%err, msg=%hex::encode(&first_message_bytes), "Failed to decode first message from peer");
151                Err(P2PStreamError::HandshakeError(err.into()))
152            }
153            Ok(msg) => {
154                debug!(?msg, "expected hello message but received another message");
155                Err(P2PStreamError::HandshakeError(P2PHandshakeError::NonHelloMessageInHandshake))
156            }
157        }?;
158
159        trace!(
160            hello=?their_hello,
161            "validating incoming p2p hello from peer"
162        );
163
164        if (hello.protocol_version as u8) != their_hello.protocol_version as u8 {
165            // send a disconnect message notifying the peer of the protocol version mismatch
166            self.send_disconnect(DisconnectReason::IncompatibleP2PProtocolVersion).await?;
167            return Err(P2PStreamError::MismatchedProtocolVersion(GotExpected {
168                got: their_hello.protocol_version,
169                expected: hello.protocol_version,
170            }))
171        }
172
173        // determine shared capabilities (currently returns only one capability)
174        let capability_res =
175            SharedCapabilities::try_new(hello.protocols, their_hello.capabilities.clone());
176
177        let shared_capability = match capability_res {
178            Err(err) => {
179                // we don't share any capabilities, send a disconnect message
180                self.send_disconnect(DisconnectReason::UselessPeer).await?;
181                Err(err)
182            }
183            Ok(cap) => Ok(cap),
184        }?;
185
186        let stream = P2PStream::new(self.inner, shared_capability);
187
188        Ok((stream, their_hello))
189    }
190}
191
192impl<S> UnauthedP2PStream<S>
193where
194    S: Sink<Bytes, Error = io::Error> + Unpin,
195{
196    /// Send a disconnect message during the handshake. This is sent without snappy compression.
197    pub async fn send_disconnect(
198        &mut self,
199        reason: DisconnectReason,
200    ) -> Result<(), P2PStreamError> {
201        trace!(
202            %reason,
203            "Sending disconnect message during the handshake",
204        );
205        self.inner
206            .send(Bytes::from(alloy_rlp::encode(P2PMessage::Disconnect(reason))))
207            .await
208            .map_err(P2PStreamError::Io)
209    }
210}
211
212impl<S> CanDisconnect<Bytes> for P2PStream<S>
213where
214    S: Sink<Bytes, Error = io::Error> + Unpin + Send + Sync,
215{
216    fn disconnect(
217        &mut self,
218        reason: DisconnectReason,
219    ) -> Pin<Box<dyn Future<Output = Result<(), P2PStreamError>> + Send + '_>> {
220        Box::pin(async move { self.disconnect(reason).await })
221    }
222}
223
224/// A `P2PStream` wraps over any `Stream` that yields bytes and makes it compatible with `p2p`
225/// protocol messages.
226///
227/// This stream supports multiple shared capabilities, that were negotiated during the handshake.
228///
229/// ### Message-ID based multiplexing
230///
231/// > Each capability is given as much of the message-ID space as it needs. All such capabilities
232/// > must statically specify how many message IDs they require. On connection and reception of the
233/// > Hello message, both peers have equivalent information about what capabilities they share
234/// > (including versions) and are able to form consensus over the composition of message ID space.
235///
236/// > Message IDs are assumed to be compact from ID 0x10 onwards (0x00-0x0f is reserved for the
237/// > "p2p" capability) and given to each shared (equal-version, equal-name) capability in
238/// > alphabetic order. Capability names are case-sensitive. Capabilities which are not shared are
239/// > ignored. If multiple versions are shared of the same (equal name) capability, the numerically
240/// > highest wins, others are ignored.
241///
242/// See also <https://github.com/ethereum/devp2p/blob/master/rlpx.md#message-id-based-multiplexing>
243///
244/// This stream emits _non-empty_ Bytes that start with the normalized message id, so that the first
245/// byte of each message starts from 0. If this stream only supports a single capability, for
246/// example `eth` then the first byte of each message will match
247/// [EthMessageID](reth_eth_wire_types::message::EthMessageID).
248///
249/// ### Sink behavior
250///
251/// The [`Sink`] impl batches writes: queued messages are drained into the underlying sink
252/// unflushed, and the caller is responsible for driving [`Sink::poll_flush`] to deliver them to
253/// the wire. Queued `p2p` control messages (ping/pong/disconnect) are the exception: they force a
254/// flush from [`Sink::poll_ready`]. Keepalive pings are generated in `poll_ready`, so the sink
255/// half must be polled regularly even if the caller has nothing to send.
256#[pin_project]
257#[derive(Debug)]
258pub struct P2PStream<S> {
259    #[pin]
260    inner: S,
261
262    /// The snappy encoder used for compressing outgoing messages
263    encoder: snap::raw::Encoder,
264
265    /// Reusable scratch buffer for compressing outgoing messages, see [`compress_frame`].
266    ///
267    /// Grow-only and capped at [`MAX_COMPRESS_SCRATCH_SIZE`]; kept fully initialized, so
268    /// zero-initialization is only paid when the buffer grows and each message only copies out
269    /// the exact compressed size instead of zeroing a worst-case sized buffer per message.
270    compress_scratch: Vec<u8>,
271
272    /// The snappy decoder used for decompressing incoming messages
273    decoder: snap::raw::Decoder,
274
275    /// The state machine used for keeping track of the peer's ping status.
276    pinger: Pinger,
277
278    /// The supported capability for this stream.
279    shared_capabilities: SharedCapabilities,
280
281    /// Outgoing messages buffered for sending to the underlying stream.
282    outgoing_messages: VecDeque<Bytes>,
283
284    /// Maximum number of messages that we can buffer here before the [Sink] impl returns
285    /// [`Poll::Pending`].
286    outgoing_message_buffer_capacity: usize,
287
288    /// Whether this stream is currently in the process of disconnecting by sending a disconnect
289    /// message.
290    disconnecting: bool,
291
292    /// Whether the underlying sink has accepted messages that still need to be flushed.
293    needs_flush: bool,
294
295    /// Whether a queued p2p control message needs to be flushed even if no subprotocol messages
296    /// are sent by the caller.
297    needs_control_flush: bool,
298}
299
300impl<S> P2PStream<S> {
301    /// Create a new [`P2PStream`] from the provided stream.
302    /// New [`P2PStream`]s are assumed to have completed the `p2p` handshake successfully and are
303    /// ready to send and receive subprotocol messages.
304    pub fn new(inner: S, shared_capabilities: SharedCapabilities) -> Self {
305        Self {
306            inner,
307            encoder: snap::raw::Encoder::new(),
308            compress_scratch: Vec::new(),
309            decoder: snap::raw::Decoder::new(),
310            pinger: Pinger::new(PING_INTERVAL, PING_TIMEOUT),
311            shared_capabilities,
312            outgoing_messages: VecDeque::new(),
313            outgoing_message_buffer_capacity: MAX_P2P_CAPACITY,
314            disconnecting: false,
315            needs_flush: false,
316            needs_control_flush: false,
317        }
318    }
319
320    /// Returns a reference to the inner stream.
321    pub const fn inner(&self) -> &S {
322        &self.inner
323    }
324
325    /// Sets a custom outgoing message buffer capacity.
326    ///
327    /// # Panics
328    ///
329    /// If the provided capacity is `0`.
330    pub const fn set_outgoing_message_buffer_capacity(&mut self, capacity: usize) {
331        assert!(capacity != 0);
332        self.outgoing_message_buffer_capacity = capacity;
333    }
334
335    /// Returns the shared capabilities for this stream.
336    ///
337    /// This includes all the shared capabilities that were negotiated during the handshake and
338    /// their offsets based on the number of messages of each capability.
339    pub const fn shared_capabilities(&self) -> &SharedCapabilities {
340        &self.shared_capabilities
341    }
342
343    /// Returns `true` if the stream has outgoing capacity.
344    fn has_outgoing_capacity(&self) -> bool {
345        self.outgoing_messages.len() < self.outgoing_message_buffer_capacity
346    }
347
348    /// Queues in a _snappy_ encoded [`P2PMessage::Pong`] message.
349    fn send_pong(&mut self) {
350        self.outgoing_messages.push_back(Bytes::from_static(SNAPPY_PONG_MESSAGE));
351        self.needs_control_flush = true;
352    }
353
354    /// Queues in a _snappy_ encoded [`P2PMessage::Ping`] message.
355    pub fn send_ping(&mut self) {
356        self.outgoing_messages.push_back(Bytes::from_static(SNAPPY_PING_MESSAGE));
357        self.needs_control_flush = true;
358    }
359}
360
361/// Gracefully disconnects the connection by sending a disconnect message and stop reading new
362/// messages.
363pub trait DisconnectP2P {
364    /// Starts to gracefully disconnect.
365    fn start_disconnect(&mut self, reason: DisconnectReason) -> Result<(), P2PStreamError>;
366
367    /// Returns `true` if the connection is about to disconnect.
368    fn is_disconnecting(&self) -> bool;
369}
370
371impl<S> DisconnectP2P for P2PStream<S> {
372    /// Starts to gracefully disconnect the connection by sending a Disconnect message and stop
373    /// reading new messages.
374    ///
375    /// Once disconnect process has started, the [`Stream`] will terminate immediately.
376    ///
377    /// # Errors
378    ///
379    /// Returns an error only if the message fails to compress.
380    fn start_disconnect(&mut self, reason: DisconnectReason) -> Result<(), P2PStreamError> {
381        // clear any buffered messages and queue in
382        self.outgoing_messages.clear();
383        let disconnect = P2PMessage::Disconnect(reason);
384        let mut buf = Vec::with_capacity(disconnect.length());
385        disconnect.encode(&mut buf);
386
387        // we do not add the capability offset because the disconnect message is a `p2p` reserved
388        // message
389        let compressed =
390            compress_frame(&mut self.encoder, &mut self.compress_scratch, buf[0], &buf[1..])
391                .map_err(|err| {
392                    debug!(
393                        %err,
394                        msg=%hex::encode(&buf[1..]),
395                        "error compressing disconnect"
396                    );
397                    err
398                })?;
399
400        self.outgoing_messages.push_back(compressed);
401        self.needs_control_flush = true;
402        self.disconnecting = true;
403        Ok(())
404    }
405
406    fn is_disconnecting(&self) -> bool {
407        self.disconnecting
408    }
409}
410
411impl<S> P2PStream<S>
412where
413    S: Sink<Bytes, Error = io::Error> + Unpin + Send,
414{
415    /// Disconnects the connection by sending a disconnect message.
416    ///
417    /// This future resolves once the disconnect message has been sent and the stream has been
418    /// closed.
419    pub async fn disconnect(&mut self, reason: DisconnectReason) -> Result<(), P2PStreamError> {
420        self.start_disconnect(reason)?;
421        self.close().await
422    }
423}
424
425impl<S> P2PStream<S>
426where
427    S: Sink<Bytes, Error = io::Error> + Unpin,
428{
429    /// Drains queued p2p frames into the underlying sink without flushing the underlying sink.
430    fn poll_drain_outgoing(
431        mut self: Pin<&mut Self>,
432        cx: &mut Context<'_>,
433    ) -> Poll<Result<(), P2PStreamError>> {
434        let mut this = self.as_mut().project();
435        while !this.outgoing_messages.is_empty() {
436            ready!(this.inner.as_mut().poll_ready(cx))?;
437            let message = this.outgoing_messages.pop_front().expect("checked non-empty");
438            this.inner.as_mut().start_send(message)?;
439            *this.needs_flush = true;
440        }
441
442        Poll::Ready(Ok(()))
443    }
444}
445
446// S must also be `Sink` because we need to be able to respond with ping messages to follow the
447// protocol
448impl<S> Stream for P2PStream<S>
449where
450    S: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin,
451{
452    type Item = Result<BytesMut, P2PStreamError>;
453
454    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
455        let this = self.get_mut();
456
457        if this.disconnecting {
458            // if disconnecting, stop reading messages
459            return Poll::Ready(None)
460        }
461
462        // we should loop here to ensure we don't return Poll::Pending if we have a message to
463        // return behind any pings we need to respond to
464        while let Poll::Ready(res) = this.inner.poll_next_unpin(cx) {
465            let bytes = match res {
466                Some(Ok(bytes)) => bytes,
467                Some(Err(err)) => return Poll::Ready(Some(Err(err.into()))),
468                None => return Poll::Ready(None),
469            };
470
471            if bytes.is_empty() {
472                // empty messages are not allowed
473                return Poll::Ready(Some(Err(P2PStreamError::EmptyProtocolMessage)))
474            }
475
476            // first decode disconnect reasons, because they can be encoded in a variety of forms
477            // over the wire, in both snappy compressed and uncompressed forms.
478            //
479            // see: [crate::disconnect::tests::test_decode_known_reasons]
480            let id = bytes[0];
481            if id == P2PMessageID::Disconnect as u8 {
482                // We can't handle the error here because disconnect reasons are encoded as both:
483                // * snappy compressed, AND
484                // * uncompressed
485                // over the network.
486                //
487                // If the decoding succeeds, we already checked the id and know this is a
488                // disconnect message, so we can return with the reason.
489                //
490                // If the decoding fails, we continue, and will attempt to decode it again if the
491                // message is snappy compressed. Failure handling in that step is the primary point
492                // where an error is returned if the disconnect reason is malformed.
493                if let Ok(reason) = DisconnectReason::decode(&mut &bytes[1..]) {
494                    return Poll::Ready(Some(Err(P2PStreamError::Disconnected(reason))))
495                }
496            }
497
498            // first check that the compressed message length does not exceed the max
499            // payload size
500            let decompressed_len = snap::raw::decompress_len(&bytes[1..])?;
501            if decompressed_len > MAX_PAYLOAD_SIZE {
502                return Poll::Ready(Some(Err(P2PStreamError::MessageTooBig {
503                    message_size: decompressed_len,
504                    max_size: MAX_PAYLOAD_SIZE,
505                })))
506            }
507
508            // create a buffer to hold the decompressed message, adding a byte to the length for
509            // the message ID byte, which is the first byte in this buffer
510            let mut decompress_buf = BytesMut::zeroed(decompressed_len + 1);
511
512            // each message following a successful handshake is compressed with snappy, so we need
513            // to decompress the message before we can decode it.
514            this.decoder.decompress(&bytes[1..], &mut decompress_buf[1..]).map_err(|err| {
515                debug!(
516                    %err,
517                    msg=%hex::encode(&bytes[1..]),
518                    "error decompressing p2p message"
519                );
520                err
521            })?;
522
523            match id {
524                _ if id == P2PMessageID::Ping as u8 => {
525                    trace!("Received Ping, Sending Pong");
526                    this.send_pong();
527                    // This is required because the `Sink` may not be polled externally, and if
528                    // that happens, the pong will never be sent.
529                    cx.waker().wake_by_ref();
530                }
531                _ if id == P2PMessageID::Hello as u8 => {
532                    // we have received a hello message outside of the handshake, so we will return
533                    // an error
534                    return Poll::Ready(Some(Err(P2PStreamError::HandshakeError(
535                        P2PHandshakeError::HelloNotInHandshake,
536                    ))))
537                }
538                _ if id == P2PMessageID::Pong as u8 => {
539                    // if we were waiting for a pong, this will reset the pinger state
540                    this.pinger.on_pong()?
541                }
542                _ if id == P2PMessageID::Disconnect as u8 => {
543                    // At this point, the `decompress_buf` contains the snappy decompressed
544                    // disconnect message.
545                    //
546                    // It's possible we already tried to RLP decode this, but it was snappy
547                    // compressed, so we need to RLP decode it again.
548                    let reason = DisconnectReason::decode(&mut &decompress_buf[1..]).inspect_err(|err| {
549                        debug!(
550                            %err, msg=%hex::encode(&decompress_buf[1..]), "Failed to decode disconnect message from peer"
551                        );
552                    })?;
553                    return Poll::Ready(Some(Err(P2PStreamError::Disconnected(reason))))
554                }
555                _ if id > MAX_P2P_MESSAGE_ID && id <= MAX_RESERVED_MESSAGE_ID => {
556                    // we have received an unknown reserved message
557                    return Poll::Ready(Some(Err(P2PStreamError::UnknownReservedMessageId(id))))
558                }
559                _ => {
560                    // we have received a message that is outside the `p2p` reserved message space,
561                    // so it is a subprotocol message.
562
563                    // Peers must be able to identify messages meant for different subprotocols
564                    // using a single message ID byte, and those messages must be distinct from the
565                    // lower-level `p2p` messages.
566                    //
567                    // To ensure that messages for subprotocols are distinct from messages meant
568                    // for the `p2p` capability, message IDs 0x00 - 0x0f are reserved for `p2p`
569                    // messages, so subprotocol messages must have an ID of 0x10 or higher.
570                    //
571                    // To ensure that messages for two different capabilities are distinct from
572                    // each other, all shared capabilities are first ordered lexicographically.
573                    // Message IDs are then reserved in this order, starting at 0x10, reserving a
574                    // message ID for each message the capability supports.
575                    //
576                    // For example, if the shared capabilities are `eth/67` (containing 10
577                    // messages), and "qrs/65" (containing 8 messages):
578                    //
579                    //  * The special case of `p2p`: `p2p` is reserved message IDs 0x00 - 0x0f.
580                    //  * `eth/67` is reserved message IDs 0x10 - 0x19.
581                    //  * `qrs/65` is reserved message IDs 0x1a - 0x21.
582                    //
583                    decompress_buf[0] = bytes[0] - MAX_RESERVED_MESSAGE_ID - 1;
584
585                    return Poll::Ready(Some(Ok(decompress_buf)))
586                }
587            }
588        }
589
590        Poll::Pending
591    }
592}
593
594impl<S> Sink<Bytes> for P2PStream<S>
595where
596    S: Sink<Bytes, Error = io::Error> + Unpin,
597{
598    type Error = P2PStreamError;
599
600    fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
601        let this = self.as_mut().get_mut();
602
603        // Poll the pinger to determine if we should send a ping; `send_ping` and
604        // `start_disconnect` set `needs_control_flush`.
605        match this.pinger.poll_ping(cx) {
606            Poll::Pending => {}
607            Poll::Ready(Ok(PingerEvent::Ping)) => {
608                this.send_ping();
609            }
610            Poll::Ready(Ok(PingerEvent::Timeout) | Err(_)) => {
611                this.start_disconnect(DisconnectReason::PingTimeout)?;
612            }
613        }
614
615        // Control messages (ping/pong/disconnect) must reach the wire even if the caller never
616        // sends a message, so they force a flush. Subprotocol messages are only drained into the
617        // underlying sink (unflushed) once the buffer is full; the caller is responsible for
618        // flushing the batch via `poll_flush`.
619        if self.needs_control_flush {
620            ready!(self.as_mut().poll_flush(cx))?;
621        } else if !self.has_outgoing_capacity() {
622            ready!(self.as_mut().poll_drain_outgoing(cx))?;
623        }
624
625        // both branches above fully drain the queue, and an empty queue always has capacity
626        debug_assert!(self.has_outgoing_capacity());
627        Poll::Ready(Ok(()))
628    }
629
630    fn start_send(self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
631        if item.len() > MAX_PAYLOAD_SIZE {
632            return Err(P2PStreamError::MessageTooBig {
633                message_size: item.len(),
634                max_size: MAX_PAYLOAD_SIZE,
635            })
636        }
637
638        if item.is_empty() {
639            // empty messages are not allowed
640            return Err(P2PStreamError::EmptyProtocolMessage)
641        }
642
643        // ensure we have free capacity
644        if !self.has_outgoing_capacity() {
645            return Err(P2PStreamError::SendBufferFull)
646        }
647
648        let this = self.project();
649
650        // all messages sent in this stream are subprotocol messages, so we need to switch the
651        // message id based on the offset
652        let compressed = compress_frame(
653            this.encoder,
654            this.compress_scratch,
655            item[0] + MAX_RESERVED_MESSAGE_ID + 1,
656            &item[1..],
657        )
658        .map_err(|err| {
659            debug!(
660                %err,
661                msg=%hex::encode(&item[1..]),
662                "error compressing p2p message"
663            );
664            err
665        })?;
666        this.outgoing_messages.push_back(compressed);
667
668        Ok(())
669    }
670
671    /// Returns `Poll::Ready(Ok(()))` when no buffered items remain.
672    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
673        ready!(self.as_mut().poll_drain_outgoing(cx))?;
674
675        let mut this = self.project();
676
677        if *this.needs_flush {
678            ready!(this.inner.as_mut().poll_flush(cx))?;
679            *this.needs_flush = false;
680        }
681        *this.needs_control_flush = false;
682
683        Poll::Ready(Ok(()))
684    }
685
686    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
687        ready!(self.as_mut().poll_flush(cx))?;
688        ready!(self.project().inner.poll_close(cx))?;
689
690        Poll::Ready(Ok(()))
691    }
692}
693
694/// This represents only the reserved `p2p` subprotocol messages.
695#[derive(Debug, Clone, PartialEq, Eq)]
696#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
697#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
698#[add_arbitrary_tests(rlp)]
699pub enum P2PMessage {
700    /// The first packet sent over the connection, and sent once by both sides.
701    Hello(HelloMessage),
702
703    /// Inform the peer that a disconnection is imminent; if received, a peer should disconnect
704    /// immediately.
705    Disconnect(DisconnectReason),
706
707    /// Requests an immediate reply of [`P2PMessage::Pong`] from the peer.
708    Ping,
709
710    /// Reply to the peer's [`P2PMessage::Ping`] packet.
711    Pong,
712}
713
714impl P2PMessage {
715    /// Gets the [`P2PMessageID`] for the given message.
716    pub const fn message_id(&self) -> P2PMessageID {
717        match self {
718            Self::Hello(_) => P2PMessageID::Hello,
719            Self::Disconnect(_) => P2PMessageID::Disconnect,
720            Self::Ping => P2PMessageID::Ping,
721            Self::Pong => P2PMessageID::Pong,
722        }
723    }
724}
725
726impl Encodable for P2PMessage {
727    /// The [`Encodable`] implementation for [`P2PMessage::Ping`] and [`P2PMessage::Pong`] encodes
728    /// the message as RLP, and prepends a snappy header to the RLP bytes for all variants except
729    /// the [`P2PMessage::Hello`] variant, because the hello message is never compressed in the
730    /// `p2p` subprotocol.
731    fn encode(&self, out: &mut dyn BufMut) {
732        (self.message_id() as u8).encode(out);
733        match self {
734            Self::Hello(msg) => msg.encode(out),
735            Self::Disconnect(msg) => msg.encode(out),
736            Self::Ping => {
737                // Ping payload is _always_ snappy encoded
738                out.put_slice(SNAPPY_EMPTY_LIST_PAYLOAD);
739            }
740            Self::Pong => {
741                // Pong payload is _always_ snappy encoded
742                out.put_slice(SNAPPY_EMPTY_LIST_PAYLOAD);
743            }
744        }
745    }
746
747    fn length(&self) -> usize {
748        let payload_len = match self {
749            Self::Hello(msg) => msg.length(),
750            Self::Disconnect(msg) => msg.length(),
751            // snappy encoded empty RLP list payload
752            Self::Ping | Self::Pong => SNAPPY_EMPTY_LIST_PAYLOAD.len(),
753        };
754        payload_len + 1 // (1 for length of p2p message id)
755    }
756}
757
758impl Decodable for P2PMessage {
759    /// The [`Decodable`] implementation for [`P2PMessage`] assumes that each of the message
760    /// variants are snappy compressed, except for the [`P2PMessage::Hello`] variant since the
761    /// hello message is never compressed in the `p2p` subprotocol.
762    ///
763    /// The [`Decodable`] implementation for [`P2PMessage::Ping`] and [`P2PMessage::Pong`] expects
764    /// a snappy encoded payload, see [`Encodable`] implementation.
765    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
766        /// Removes the snappy prefix from the Ping/Pong buffer
767        fn advance_snappy_ping_pong_payload(buf: &mut &[u8]) -> alloy_rlp::Result<()> {
768            if buf.len() < 3 {
769                return Err(RlpError::InputTooShort)
770            }
771            if buf[..3] != [0x01, 0x00, EMPTY_LIST_CODE] {
772                return Err(RlpError::Custom("expected snappy payload"))
773            }
774            buf.advance(3);
775            Ok(())
776        }
777
778        let message_id = u8::decode(&mut &buf[..])?;
779        let id = P2PMessageID::try_from(message_id)
780            .or(Err(RlpError::Custom("unknown p2p message id")))?;
781        buf.advance(1);
782        match id {
783            P2PMessageID::Hello => Ok(Self::Hello(HelloMessage::decode(buf)?)),
784            P2PMessageID::Disconnect => Ok(Self::Disconnect(DisconnectReason::decode(buf)?)),
785            P2PMessageID::Ping => {
786                advance_snappy_ping_pong_payload(buf)?;
787                Ok(Self::Ping)
788            }
789            P2PMessageID::Pong => {
790                advance_snappy_ping_pong_payload(buf)?;
791                Ok(Self::Pong)
792            }
793        }
794    }
795}
796
797/// Message IDs for `p2p` subprotocol messages.
798#[derive(Debug, Copy, Clone, Eq, PartialEq)]
799pub enum P2PMessageID {
800    /// Message ID for the [`P2PMessage::Hello`] message.
801    Hello = 0x00,
802
803    /// Message ID for the [`P2PMessage::Disconnect`] message.
804    Disconnect = 0x01,
805
806    /// Message ID for the [`P2PMessage::Ping`] message.
807    Ping = 0x02,
808
809    /// Message ID for the [`P2PMessage::Pong`] message.
810    Pong = 0x03,
811}
812
813impl From<P2PMessage> for P2PMessageID {
814    fn from(msg: P2PMessage) -> Self {
815        match msg {
816            P2PMessage::Hello(_) => Self::Hello,
817            P2PMessage::Disconnect(_) => Self::Disconnect,
818            P2PMessage::Ping => Self::Ping,
819            P2PMessage::Pong => Self::Pong,
820        }
821    }
822}
823
824impl TryFrom<u8> for P2PMessageID {
825    type Error = P2PStreamError;
826
827    fn try_from(id: u8) -> Result<Self, Self::Error> {
828        match id {
829            0x00 => Ok(Self::Hello),
830            0x01 => Ok(Self::Disconnect),
831            0x02 => Ok(Self::Ping),
832            0x03 => Ok(Self::Pong),
833            _ => Err(P2PStreamError::UnknownReservedMessageId(id)),
834        }
835    }
836}
837
838/// Snappy-compresses an id-prefixed `p2p` message payload into a frame carrying the given wire
839/// message id.
840///
841/// Frames whose worst-case compressed size fits within [`MAX_COMPRESS_SCRATCH_SIZE`] are
842/// compressed through the reusable `scratch` buffer and copied out at their exact size; larger
843/// frames use a one-off allocation, see [`MAX_COMPRESS_SCRATCH_SIZE`].
844fn compress_frame(
845    encoder: &mut snap::raw::Encoder,
846    scratch: &mut Vec<u8>,
847    wire_id: u8,
848    payload: &[u8],
849) -> Result<Bytes, snap::Error> {
850    let needed = 1 + snap::raw::max_compress_len(payload.len());
851
852    if needed > MAX_COMPRESS_SCRATCH_SIZE {
853        let mut compressed = vec![0u8; needed];
854        let compressed_size = encoder.compress(payload, &mut compressed[1..])?;
855        compressed[0] = wire_id;
856        compressed.truncate(compressed_size + 1);
857        return Ok(compressed.into())
858    }
859
860    if scratch.len() < needed {
861        scratch.resize(needed, 0);
862    }
863    let compressed_size = encoder.compress(payload, &mut scratch[1..])?;
864    scratch[0] = wire_id;
865    Ok(Bytes::copy_from_slice(&scratch[..compressed_size + 1]))
866}
867
868#[cfg(test)]
869mod tests {
870    use super::*;
871    use crate::{
872        capability::SharedCapability, test_utils::eth_hello, Capability, EthVersion,
873        ProtocolVersion,
874    };
875    use futures::task::noop_waker_ref;
876    use tokio::net::{TcpListener, TcpStream};
877    use tokio_util::codec::Decoder;
878
879    /// A sink that records started frames and counts flushes, to observe batching behavior.
880    #[derive(Default)]
881    struct FlushCountingTransport {
882        sent: Vec<Bytes>,
883        flushes: usize,
884    }
885
886    impl Stream for FlushCountingTransport {
887        type Item = io::Result<BytesMut>;
888
889        fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
890            Poll::Pending
891        }
892    }
893
894    impl Sink<Bytes> for FlushCountingTransport {
895        type Error = io::Error;
896
897        fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
898            Poll::Ready(Ok(()))
899        }
900
901        fn start_send(mut self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
902            self.sent.push(item);
903            Ok(())
904        }
905
906        fn poll_flush(
907            mut self: Pin<&mut Self>,
908            _: &mut Context<'_>,
909        ) -> Poll<Result<(), Self::Error>> {
910            self.flushes += 1;
911            Poll::Ready(Ok(()))
912        }
913
914        fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
915            Poll::Ready(Ok(()))
916        }
917    }
918
919    fn eth_shared_capabilities() -> SharedCapabilities {
920        SharedCapabilities::try_new(
921            vec![EthVersion::Eth68.into()],
922            vec![Capability::eth(EthVersion::Eth68)],
923        )
924        .unwrap()
925    }
926
927    #[tokio::test]
928    async fn poll_ready_drains_full_subprotocol_queue_without_flushing_inner() {
929        let mut stream =
930            P2PStream::new(FlushCountingTransport::default(), eth_shared_capabilities());
931        stream.set_outgoing_message_buffer_capacity(1);
932        Pin::new(&mut stream).start_send(Bytes::from_static(&[0x00, EMPTY_LIST_CODE])).unwrap();
933
934        let waker = noop_waker_ref();
935        let mut cx = Context::from_waker(waker);
936        assert!(Pin::new(&mut stream).poll_ready(&mut cx).is_ready());
937
938        // the full queue was drained into the inner sink to make room, but not flushed
939        assert_eq!(stream.inner().sent.len(), 1);
940        assert_eq!(stream.inner().flushes, 0);
941
942        // the caller-driven flush pushes the batch out with a single inner flush
943        assert!(Pin::new(&mut stream).poll_flush(&mut cx).is_ready());
944        assert_eq!(stream.inner().flushes, 1);
945
946        // flushing again is a no-op on the inner sink
947        assert!(Pin::new(&mut stream).poll_flush(&mut cx).is_ready());
948        assert_eq!(stream.inner().flushes, 1);
949    }
950
951    #[tokio::test]
952    async fn poll_ready_flushes_queued_control_messages() {
953        let mut stream =
954            P2PStream::new(FlushCountingTransport::default(), eth_shared_capabilities());
955        stream.send_ping();
956
957        let waker = noop_waker_ref();
958        let mut cx = Context::from_waker(waker);
959        assert!(Pin::new(&mut stream).poll_ready(&mut cx).is_ready());
960
961        // control messages must not wait for a caller-driven flush
962        assert_eq!(stream.inner().sent.len(), 1);
963        assert_eq!(stream.inner().flushes, 1);
964    }
965
966    #[tokio::test]
967    async fn test_can_disconnect() {
968        reth_tracing::init_test_tracing();
969        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
970        let local_addr = listener.local_addr().unwrap();
971
972        let expected_disconnect = DisconnectReason::UselessPeer;
973
974        let handle = tokio::spawn(async move {
975            // roughly based off of the design of tokio::net::TcpListener
976            let (incoming, _) = listener.accept().await.unwrap();
977            let stream = crate::PassthroughCodec::default().framed(incoming);
978
979            let (server_hello, _) = eth_hello();
980
981            let (mut p2p_stream, _) =
982                UnauthedP2PStream::new(stream).handshake(server_hello).await.unwrap();
983
984            p2p_stream.disconnect(expected_disconnect).await.unwrap();
985        });
986
987        let outgoing = TcpStream::connect(local_addr).await.unwrap();
988        let sink = crate::PassthroughCodec::default().framed(outgoing);
989
990        let (client_hello, _) = eth_hello();
991
992        let (mut p2p_stream, _) =
993            UnauthedP2PStream::new(sink).handshake(client_hello).await.unwrap();
994
995        let err = p2p_stream.next().await.unwrap().unwrap_err();
996        match err {
997            P2PStreamError::Disconnected(reason) => assert_eq!(reason, expected_disconnect),
998            e => panic!("unexpected err: {e}"),
999        }
1000
1001        handle.await.unwrap();
1002    }
1003
1004    #[tokio::test]
1005    async fn test_can_disconnect_weird_disconnect_encoding() {
1006        reth_tracing::init_test_tracing();
1007        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1008        let local_addr = listener.local_addr().unwrap();
1009
1010        let expected_disconnect = DisconnectReason::SubprotocolSpecific;
1011
1012        let handle = tokio::spawn(async move {
1013            // roughly based off of the design of tokio::net::TcpListener
1014            let (incoming, _) = listener.accept().await.unwrap();
1015            let stream = crate::PassthroughCodec::default().framed(incoming);
1016
1017            let (server_hello, _) = eth_hello();
1018
1019            let (mut p2p_stream, _) =
1020                UnauthedP2PStream::new(stream).handshake(server_hello).await.unwrap();
1021
1022            // Unrolled `disconnect` method, without compression
1023            p2p_stream.outgoing_messages.clear();
1024
1025            p2p_stream.outgoing_messages.push_back(Bytes::from(alloy_rlp::encode(
1026                P2PMessage::Disconnect(DisconnectReason::SubprotocolSpecific),
1027            )));
1028            p2p_stream.disconnecting = true;
1029            p2p_stream.close().await.unwrap();
1030        });
1031
1032        let outgoing = TcpStream::connect(local_addr).await.unwrap();
1033        let sink = crate::PassthroughCodec::default().framed(outgoing);
1034
1035        let (client_hello, _) = eth_hello();
1036
1037        let (mut p2p_stream, _) =
1038            UnauthedP2PStream::new(sink).handshake(client_hello).await.unwrap();
1039
1040        let err = p2p_stream.next().await.unwrap().unwrap_err();
1041        match err {
1042            P2PStreamError::Disconnected(reason) => assert_eq!(reason, expected_disconnect),
1043            e => panic!("unexpected err: {e}"),
1044        }
1045
1046        handle.await.unwrap();
1047    }
1048
1049    #[tokio::test]
1050    async fn test_handshake_passthrough() {
1051        // create a p2p stream and server, then confirm that the two are authed
1052        // create tcpstream
1053        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1054        let local_addr = listener.local_addr().unwrap();
1055
1056        let handle = tokio::spawn(async move {
1057            // roughly based off of the design of tokio::net::TcpListener
1058            let (incoming, _) = listener.accept().await.unwrap();
1059            let stream = crate::PassthroughCodec::default().framed(incoming);
1060
1061            let (server_hello, _) = eth_hello();
1062
1063            let unauthed_stream = UnauthedP2PStream::new(stream);
1064            let (p2p_stream, _) = unauthed_stream.handshake(server_hello).await.unwrap();
1065
1066            // ensure that the two share a single capability, eth67
1067            assert_eq!(
1068                *p2p_stream.shared_capabilities.iter_caps().next().unwrap(),
1069                SharedCapability::Eth {
1070                    version: EthVersion::Eth67,
1071                    offset: MAX_RESERVED_MESSAGE_ID + 1
1072                }
1073            );
1074        });
1075
1076        let outgoing = TcpStream::connect(local_addr).await.unwrap();
1077        let sink = crate::PassthroughCodec::default().framed(outgoing);
1078
1079        let (client_hello, _) = eth_hello();
1080
1081        let unauthed_stream = UnauthedP2PStream::new(sink);
1082        let (p2p_stream, _) = unauthed_stream.handshake(client_hello).await.unwrap();
1083
1084        // ensure that the two share a single capability, eth67
1085        assert_eq!(
1086            *p2p_stream.shared_capabilities.iter_caps().next().unwrap(),
1087            SharedCapability::Eth {
1088                version: EthVersion::Eth67,
1089                offset: MAX_RESERVED_MESSAGE_ID + 1
1090            }
1091        );
1092
1093        // make sure the server receives the message and asserts before ending the test
1094        handle.await.unwrap();
1095    }
1096
1097    #[tokio::test]
1098    async fn test_handshake_disconnect() {
1099        // create a p2p stream and server, then confirm that the two are authed
1100        // create tcpstream
1101        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1102        let local_addr = listener.local_addr().unwrap();
1103
1104        let handle = tokio::spawn(async move {
1105            // roughly based off of the design of tokio::net::TcpListener
1106            let (incoming, _) = listener.accept().await.unwrap();
1107            let stream = crate::PassthroughCodec::default().framed(incoming);
1108
1109            let (server_hello, _) = eth_hello();
1110
1111            let unauthed_stream = UnauthedP2PStream::new(stream);
1112            match unauthed_stream.handshake(server_hello.clone()).await {
1113                Ok((_, hello)) => {
1114                    panic!("expected handshake to fail, instead got a successful Hello: {hello:?}")
1115                }
1116                Err(P2PStreamError::MismatchedProtocolVersion(GotExpected { got, expected })) => {
1117                    assert_ne!(expected, got);
1118                    assert_eq!(expected, server_hello.protocol_version);
1119                }
1120                Err(other_err) => {
1121                    panic!("expected mismatched protocol version error, got {other_err:?}")
1122                }
1123            }
1124        });
1125
1126        let outgoing = TcpStream::connect(local_addr).await.unwrap();
1127        let sink = crate::PassthroughCodec::default().framed(outgoing);
1128
1129        let (mut client_hello, _) = eth_hello();
1130
1131        // modify the hello to include an incompatible p2p protocol version
1132        client_hello.protocol_version = ProtocolVersion::V4;
1133
1134        let unauthed_stream = UnauthedP2PStream::new(sink);
1135        match unauthed_stream.handshake(client_hello.clone()).await {
1136            Ok((_, hello)) => {
1137                panic!("expected handshake to fail, instead got a successful Hello: {hello:?}")
1138            }
1139            Err(P2PStreamError::MismatchedProtocolVersion(GotExpected { got, expected })) => {
1140                assert_ne!(expected, got);
1141                assert_eq!(expected, client_hello.protocol_version);
1142            }
1143            Err(other_err) => {
1144                panic!("expected mismatched protocol version error, got {other_err:?}")
1145            }
1146        }
1147
1148        // make sure the server receives the message and asserts before ending the test
1149        handle.await.unwrap();
1150    }
1151
1152    #[test]
1153    fn snappy_ping_pong_consts_match_rlp_encoding() {
1154        assert_eq!(alloy_rlp::encode(P2PMessage::Ping).as_slice(), SNAPPY_PING_MESSAGE);
1155        assert_eq!(alloy_rlp::encode(P2PMessage::Pong).as_slice(), SNAPPY_PONG_MESSAGE);
1156    }
1157
1158    #[test]
1159    fn snappy_decode_encode_ping() {
1160        let snappy_ping = b"\x02\x01\0\xc0";
1161        let ping = P2PMessage::decode(&mut &snappy_ping[..]).unwrap();
1162        assert!(matches!(ping, P2PMessage::Ping));
1163        assert_eq!(alloy_rlp::encode(ping), &snappy_ping[..]);
1164    }
1165
1166    #[test]
1167    fn snappy_decode_encode_pong() {
1168        let snappy_pong = b"\x03\x01\0\xc0";
1169        let pong = P2PMessage::decode(&mut &snappy_pong[..]).unwrap();
1170        assert!(matches!(pong, P2PMessage::Pong));
1171        assert_eq!(alloy_rlp::encode(pong), &snappy_pong[..]);
1172    }
1173}