1use crate::{
9 capability::SharedCapabilities,
10 errors::{EthStreamError, P2PStreamError},
11 handshake::EthRlpxHandshake,
12 message::EthBroadcastMessage,
13 Capability, EthMessage, EthNetworkPrimitives, EthStreamInner, EthVersion, NetworkPrimitives,
14 P2PStream, UnifiedStatus, HANDSHAKE_TIMEOUT,
15};
16use alloy_primitives::bytes::{Bytes, BytesMut};
17use futures::{Sink, SinkExt, Stream, StreamExt};
18use reth_eth_wire_types::{
19 snap::{SnapProtocolMessage, SnapVersion},
20 RawCapabilityMessage,
21};
22use reth_ethereum_forks::ForkFilter;
23use std::{
24 io,
25 pin::Pin,
26 sync::Arc,
27 task::{ready, Context, Poll},
28};
29
30#[derive(Debug)]
37pub struct EthSnapStream<St, N: NetworkPrimitives = EthNetworkPrimitives> {
38 conn: P2PStream<St>,
40 eth: EthStreamInner<N>,
42 snap_offset: u8,
46}
47
48impl<St, N> EthSnapStream<St, N>
49where
50 St: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin + Send + Sync,
51 N: NetworkPrimitives,
52{
53 pub async fn handshake(
60 mut conn: P2PStream<St>,
61 status: UnifiedStatus,
62 fork_filter: ForkFilter,
63 handshake: Arc<dyn EthRlpxHandshake>,
64 eth_max_message_size: usize,
65 ) -> Result<(Self, UnifiedStatus), EthStreamError> {
66 let eth_version = conn.shared_capabilities().eth_version()?;
67 let snap_offset = eth_snap_layout(conn.shared_capabilities())?;
68
69 let their_status =
70 handshake.handshake(&mut conn, status, fork_filter, HANDSHAKE_TIMEOUT).await?;
71
72 let eth = EthStreamInner::with_max_message_size(eth_version, eth_max_message_size);
73 Ok((Self { conn, eth, snap_offset }, their_status))
74 }
75}
76
77impl<St, N: NetworkPrimitives> EthSnapStream<St, N> {
78 #[inline]
80 pub const fn version(&self) -> EthVersion {
81 self.eth.version()
82 }
83
84 #[inline]
86 pub const fn set_reject_block_announcements(&mut self, reject: bool) {
87 self.eth.set_reject_block_announcements(reject);
88 }
89
90 #[inline]
92 pub const fn inner(&self) -> &P2PStream<St> {
93 &self.conn
94 }
95
96 #[inline]
98 pub const fn inner_mut(&mut self) -> &mut P2PStream<St> {
99 &mut self.conn
100 }
101
102 #[inline]
104 pub fn into_inner(self) -> P2PStream<St> {
105 self.conn
106 }
107}
108
109impl<St, N> EthSnapStream<St, N>
110where
111 St: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin,
112 N: NetworkPrimitives,
113{
114 pub fn start_send_broadcast(
116 &mut self,
117 item: EthBroadcastMessage<N>,
118 ) -> Result<(), EthStreamError> {
119 self.conn.start_send_unpin(item.encoded()).map_err(Into::into)
120 }
121
122 pub fn start_send_raw(&mut self, msg: RawCapabilityMessage) -> Result<(), EthStreamError> {
124 self.conn.start_send_unpin(msg.encoded()).map_err(Into::into)
125 }
126}
127
128#[derive(Debug)]
130pub enum EthSnapMessage<N: NetworkPrimitives = EthNetworkPrimitives> {
131 Eth(EthMessage<N>),
133 Snap(SnapProtocolMessage),
135}
136
137impl<St, N> Stream for EthSnapStream<St, N>
138where
139 St: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin,
140 N: NetworkPrimitives,
141{
142 type Item = Result<EthSnapMessage<N>, EthStreamError>;
143
144 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
145 let this = self.get_mut();
146 let mut bytes = match ready!(this.conn.poll_next_unpin(cx)) {
147 Some(Ok(bytes)) => bytes,
148 Some(Err(err)) => return Poll::Ready(Some(Err(err.into()))),
149 None => return Poll::Ready(None),
150 };
151
152 let Some(&id) = bytes.first() else {
153 return Poll::Ready(Some(Err(P2PStreamError::EmptyProtocolMessage.into())))
154 };
155
156 let Some(snap_id) = id.checked_sub(this.snap_offset) else {
160 return Poll::Ready(Some(this.eth.decode_message(bytes).map(EthSnapMessage::Eth)))
161 };
162 bytes[0] = snap_id;
163 Poll::Ready(Some(
164 SnapProtocolMessage::decode_versioned(SnapVersion::V2, &bytes)
165 .map(EthSnapMessage::Snap)
166 .map_err(Into::into),
167 ))
168 }
169}
170
171impl<St, N> Sink<EthSnapMessage<N>> for EthSnapStream<St, N>
172where
173 St: Stream<Item = io::Result<BytesMut>> + Sink<Bytes, Error = io::Error> + Unpin,
174 N: NetworkPrimitives,
175{
176 type Error = EthStreamError;
177
178 fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
179 self.get_mut().conn.poll_ready_unpin(cx).map_err(Into::into)
180 }
181
182 fn start_send(self: Pin<&mut Self>, item: EthSnapMessage<N>) -> Result<(), Self::Error> {
183 let this = self.get_mut();
184 let bytes = match item {
185 EthSnapMessage::Eth(msg) => this.eth.encode_message(msg)?,
186 EthSnapMessage::Snap(msg) => {
187 let mut buf =
190 msg.encode().0.try_into_mut().unwrap_or_else(|b| BytesMut::from(b.as_ref()));
191 mask_snap(&mut buf, this.snap_offset)?;
192 buf.freeze()
193 }
194 };
195 this.conn.start_send_unpin(bytes).map_err(Into::into)
196 }
197
198 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
199 self.get_mut().conn.poll_flush_unpin(cx).map_err(Into::into)
200 }
201
202 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
203 self.get_mut().conn.poll_close_unpin(cx).map_err(Into::into)
204 }
205}
206
207fn eth_snap_layout(caps: &SharedCapabilities) -> Result<u8, EthStreamError> {
215 if !caps.is_exact_eth_snap_v2() {
216 return Err(P2PStreamError::CapabilityNotShared.into())
217 }
218 let snap = caps
219 .ensure_matching_capability(&Capability::snap_2())
220 .map_err(|_| EthStreamError::from(P2PStreamError::CapabilityNotShared))?;
221 let snap_offset = snap.relative_message_id_offset();
222
223 let eth = caps.eth()?;
224 if eth.relative_message_id_offset() != 0 || eth.num_messages() != snap_offset {
225 return Err(P2PStreamError::CapabilityNotShared.into())
226 }
227 Ok(snap_offset)
228}
229
230#[inline]
232fn mask_snap(bytes: &mut BytesMut, snap_offset: u8) -> Result<(), io::Error> {
233 let id = bytes.first().ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
234 bytes[0] =
235 id.checked_add(snap_offset).ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
236 Ok(())
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::{
243 handshake::EthHandshake,
244 message::MAX_MESSAGE_SIZE,
245 protocol::Protocol,
246 test_utils::{connect_passthrough, eth_handshake, eth_hello},
247 UnauthedP2PStream,
248 };
249 use reth_eth_wire_types::{
250 snap::{BlockAccessListsMessage, GetBlockAccessListsMessage},
251 EthVersion,
252 };
253 use tokio::net::TcpListener;
254 use tokio_util::codec::Decoder;
255
256 fn shared_caps(local: Vec<Protocol>, peer: Vec<Capability>) -> SharedCapabilities {
258 SharedCapabilities::try_new(local, peer).unwrap()
259 }
260
261 fn eth_snap_hello() -> crate::HelloMessageWithProtocols {
263 let mut hello = eth_hello().0;
264 hello.try_add_protocol(Protocol::snap_2()).unwrap();
265 hello
266 }
267
268 #[test]
269 fn eth_snap_layout_accepts_eth_then_snap() {
270 let caps = shared_caps(
271 vec![EthVersion::Eth68.into(), Protocol::snap_2()],
272 vec![EthVersion::Eth68.into(), Capability::snap_2()],
273 );
274 let offset = eth_snap_layout(&caps).unwrap();
275 assert_eq!(offset, caps.eth().unwrap().num_messages());
276 }
277
278 #[test]
279 fn eth_snap_layout_rejects_missing_snap() {
280 let caps = shared_caps(vec![EthVersion::Eth68.into()], vec![EthVersion::Eth68.into()]);
281 assert!(eth_snap_layout(&caps).is_err());
282 }
283
284 #[test]
285 fn eth_snap_layout_rejects_capability_before_eth() {
286 let cap = Capability::new_static("aaa", 1);
288 let caps = shared_caps(
289 vec![Protocol::new(cap.clone(), 5), EthVersion::Eth68.into(), Protocol::snap_2()],
290 vec![cap, EthVersion::Eth68.into(), Capability::snap_2()],
291 );
292 assert!(eth_snap_layout(&caps).is_err());
293 }
294
295 #[test]
296 fn eth_snap_layout_rejects_capability_between_eth_and_snap() {
297 let cap = Capability::new_static("les", 1);
300 let caps = shared_caps(
301 vec![EthVersion::Eth68.into(), Protocol::new(cap.clone(), 5), Protocol::snap_2()],
302 vec![EthVersion::Eth68.into(), cap, Capability::snap_2()],
303 );
304 assert!(eth_snap_layout(&caps).is_err());
305 }
306
307 #[test]
308 fn eth_snap_layout_rejects_capability_after_snap() {
309 let cap = Capability::new_static("zzz", 1);
313 let caps = shared_caps(
314 vec![EthVersion::Eth68.into(), Protocol::snap_2(), Protocol::new(cap.clone(), 5)],
315 vec![EthVersion::Eth68.into(), Capability::snap_2(), cap],
316 );
317 assert!(eth_snap_layout(&caps).is_err());
318 }
319
320 #[test]
321 fn mask_snap_rebases_into_combined_space() {
322 let mut bytes = BytesMut::from(&[8u8, 0xcc][..]);
323 mask_snap(&mut bytes, 17).unwrap();
324 assert_eq!(bytes[0], 17 + 8);
325 }
326
327 #[tokio::test(flavor = "multi_thread")]
330 async fn snap_request_response_round_trips_over_the_wire() {
331 reth_tracing::init_test_tracing();
332 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
333 let local_addr = listener.local_addr().unwrap();
334 let (status, fork_filter) = eth_handshake();
335 let server_status = status;
336 let server_fork_filter = fork_filter.clone();
337
338 let server = tokio::spawn(async move {
340 let (incoming, _) = listener.accept().await.unwrap();
341 let stream = crate::PassthroughCodec::default().framed(incoming);
342 let server_hello = eth_snap_hello();
343 let (conn, _) = UnauthedP2PStream::new(stream).handshake(server_hello).await.unwrap();
344
345 let (mut stream, _) = EthSnapStream::<_, EthNetworkPrimitives>::handshake(
346 conn,
347 server_status,
348 server_fork_filter,
349 Arc::new(EthHandshake::default()),
350 MAX_MESSAGE_SIZE,
351 )
352 .await
353 .unwrap();
354
355 while let Some(Ok(msg)) = stream.next().await {
356 if let EthSnapMessage::Snap(SnapProtocolMessage::GetBlockAccessLists(req)) = msg {
357 let response = SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
358 request_id: req.request_id,
359 block_access_lists: reth_eth_wire_types::BlockAccessLists(vec![None]),
360 });
361 stream.send(EthSnapMessage::Snap(response)).await.unwrap();
362 }
363 }
364 });
365
366 let conn = connect_passthrough(local_addr, eth_snap_hello()).await;
368 let (mut stream, _) = EthSnapStream::<_, EthNetworkPrimitives>::handshake(
369 conn,
370 status,
371 fork_filter,
372 Arc::new(EthHandshake::default()),
373 MAX_MESSAGE_SIZE,
374 )
375 .await
376 .unwrap();
377
378 stream
379 .send(EthSnapMessage::Snap(SnapProtocolMessage::GetBlockAccessLists(
380 GetBlockAccessListsMessage {
381 request_id: 7,
382 block_hashes: Vec::new(),
383 response_bytes: u64::MAX,
384 },
385 )))
386 .await
387 .unwrap();
388
389 let response = loop {
390 if let EthSnapMessage::Snap(SnapProtocolMessage::BlockAccessLists(resp)) =
391 stream.next().await.unwrap().unwrap()
392 {
393 break resp;
394 }
395 };
396 assert_eq!(response.request_id, 7);
397
398 server.abort();
399 }
400
401 #[tokio::test(flavor = "multi_thread")]
404 async fn snap_two_rejects_removed_trie_node_messages_over_the_wire() {
405 reth_tracing::init_test_tracing();
406
407 for removed_snap_id in [0x06, 0x07] {
408 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
409 let local_addr = listener.local_addr().unwrap();
410 let (status, fork_filter) = eth_handshake();
411 let server_status = status;
412 let server_fork_filter = fork_filter.clone();
413
414 let server = tokio::spawn(async move {
415 let (incoming, _) = listener.accept().await.unwrap();
416 let stream = crate::PassthroughCodec::default().framed(incoming);
417 let (conn, _) =
418 UnauthedP2PStream::new(stream).handshake(eth_snap_hello()).await.unwrap();
419 let (mut stream, _) = EthSnapStream::<_, EthNetworkPrimitives>::handshake(
420 conn,
421 server_status,
422 server_fork_filter,
423 Arc::new(EthHandshake::default()),
424 MAX_MESSAGE_SIZE,
425 )
426 .await
427 .unwrap();
428
429 stream.next().await.expect("peer should send a frame").unwrap_err()
430 });
431
432 let conn = connect_passthrough(local_addr, eth_snap_hello()).await;
433 let (mut stream, _) = EthSnapStream::<_, EthNetworkPrimitives>::handshake(
434 conn,
435 status,
436 fork_filter,
437 Arc::new(EthHandshake::default()),
438 MAX_MESSAGE_SIZE,
439 )
440 .await
441 .unwrap();
442
443 let combined_id = stream.snap_offset as usize + removed_snap_id;
444 stream
445 .start_send_raw(RawCapabilityMessage::new(
446 combined_id,
447 Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]),
448 ))
449 .unwrap();
450 stream.flush().await.unwrap();
451
452 let error = server.await.unwrap();
453 assert!(
454 error.to_string().contains("invalid for snap"),
455 "unexpected error for removed snap id {removed_snap_id:#x}: {error}"
456 );
457 }
458 }
459}