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