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
33const MAX_PAYLOAD_SIZE: usize = 16 * 1024 * 1024;
36
37pub const MAX_RESERVED_MESSAGE_ID: u8 = 0x0f;
40
41const MAX_P2P_MESSAGE_ID: u8 = P2PMessageID::Pong as u8;
43
44const SNAPPY_EMPTY_LIST_PAYLOAD: &[u8] = &[0x01, 0x00, EMPTY_LIST_CODE];
46
47const SNAPPY_PING_MESSAGE: &[u8] = &[0x02, 0x01, 0x00, EMPTY_LIST_CODE];
49
50const SNAPPY_PONG_MESSAGE: &[u8] = &[0x03, 0x01, 0x00, EMPTY_LIST_CODE];
52
53pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
56
57const PING_TIMEOUT: Duration = Duration::from_secs(15);
60
61const PING_INTERVAL: Duration = Duration::from_secs(60);
64
65const PING_TOKEN_BUCKET_CAPACITY: u8 = 5;
67
68const MAX_P2P_CAPACITY: usize = 2;
75
76const MAX_COMPRESS_SCRATCH_SIZE: usize = 256 * 1024;
83
84#[pin_project]
87#[derive(Debug)]
88pub struct UnauthedP2PStream<S> {
89 #[pin]
90 inner: S,
91}
92
93impl<S> UnauthedP2PStream<S> {
94 pub const fn new(inner: S) -> Self {
96 Self { inner }
97 }
98
99 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 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 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 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 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 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 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 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 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 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#[pin_project]
261#[derive(Debug)]
262pub struct P2PStream<S> {
263 #[pin]
264 inner: S,
265
266 encoder: snap::raw::Encoder,
268
269 compress_scratch: Vec<u8>,
275
276 decoder: snap::raw::Decoder,
278
279 pinger: Pinger,
281
282 ping_token_bucket: PingTokenBucket,
284
285 shared_capabilities: SharedCapabilities,
287
288 inbound_protocol_limits: Vec<InboundProtocolLimit>,
290
291 outgoing_messages: VecDeque<Bytes>,
293
294 outgoing_message_buffer_capacity: usize,
297
298 disconnecting: bool,
301
302 needs_flush: bool,
304
305 needs_control_flush: bool,
308}
309
310impl<S> P2PStream<S> {
311 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 pub const fn inner(&self) -> &S {
334 &self.inner
335 }
336
337 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 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 fn has_outgoing_capacity(&self) -> bool {
391 self.outgoing_messages.len() < self.outgoing_message_buffer_capacity
392 }
393
394 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 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#[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
454pub trait DisconnectP2P {
457 fn start_disconnect(&mut self, reason: DisconnectReason) -> Result<(), P2PStreamError>;
459
460 fn is_disconnecting(&self) -> bool;
462}
463
464impl<S> DisconnectP2P for P2PStream<S> {
465 fn start_disconnect(&mut self, reason: DisconnectReason) -> Result<(), P2PStreamError> {
474 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 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 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 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
539impl<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 return Poll::Ready(None)
553 }
554
555 let mut ping_batch_time = None;
556
557 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 return Poll::Ready(Some(Err(P2PStreamError::EmptyProtocolMessage)))
569 }
570
571 let id = bytes[0];
576 if id == P2PMessageID::Disconnect as u8 {
577 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 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 cx.waker().wake_by_ref();
610 } else {
611 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 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 let mut decompress_buf = BytesMut::zeroed(frame_len);
647
648 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 return Poll::Ready(Some(Err(P2PStreamError::HandshakeError(
664 P2PHandshakeError::HelloNotInHandshake,
665 ))))
666 }
667 _ if id == P2PMessageID::Disconnect as u8 => {
668 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 return Poll::Ready(Some(Err(P2PStreamError::UnknownReservedMessageId(id))))
683 }
684 _ => {
685 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
719fn 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 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 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 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 return Err(P2PStreamError::EmptyProtocolMessage)
785 }
786
787 if !self.has_outgoing_capacity() {
789 return Err(P2PStreamError::SendBufferFull)
790 }
791
792 let this = self.project();
793
794 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 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#[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 Hello(HelloMessage),
846
847 Disconnect(DisconnectReason),
850
851 Ping,
853
854 Pong,
856}
857
858impl P2PMessage {
859 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 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 out.put_slice(SNAPPY_EMPTY_LIST_PAYLOAD);
883 }
884 Self::Pong => {
885 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 Self::Ping | Self::Pong => SNAPPY_EMPTY_LIST_PAYLOAD.len(),
897 };
898 payload_len + 1 }
900}
901
902impl Decodable for P2PMessage {
903 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
910 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
943pub enum P2PMessageID {
944 Hello = 0x00,
946
947 Disconnect = 0x01,
949
950 Ping = 0x02,
952
953 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
982fn 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 #[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 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 assert_eq!(stream.inner().sent.len(), 1);
1318 assert_eq!(stream.inner().flushes, 0);
1319
1320 assert!(Pin::new(&mut stream).poll_flush(&mut cx).is_ready());
1322 assert_eq!(stream.inner().flushes, 1);
1323
1324 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 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 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 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 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 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 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 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 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 handle.await.unwrap();
1473 }
1474
1475 #[tokio::test]
1476 async fn test_handshake_disconnect() {
1477 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 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 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 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}