From 57d77add7151f63cd2d7e3d49f969f79098a5d64 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 17 Apr 2026 14:34:11 +0200 Subject: [PATCH] feat(net): add snap/2 wire helpers and messages --- crates/net/eth-wire-types/src/capability.rs | 17 ++- crates/net/eth-wire-types/src/snap.rs | 113 ++++++++++++++++- crates/net/eth-wire/src/eth_snap_stream.rs | 130 ++++++++++++++++++-- crates/net/eth-wire/src/protocol.rs | 21 +++- 4 files changed, 268 insertions(+), 13 deletions(-) diff --git a/crates/net/eth-wire-types/src/capability.rs b/crates/net/eth-wire-types/src/capability.rs index 804345bd375..69c64038e74 100644 --- a/crates/net/eth-wire-types/src/capability.rs +++ b/crates/net/eth-wire-types/src/capability.rs @@ -1,6 +1,6 @@ //! All capability related types -use crate::{EthMessageID, EthVersion}; +use crate::{EthMessageID, EthVersion, SnapVersion}; use alloc::{borrow::Cow, string::String, vec::Vec}; use alloy_primitives::bytes::Bytes; use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable}; @@ -85,6 +85,11 @@ impl Capability { Self::new_static("eth", version as usize) } + /// Returns the corresponding snap capability for the given version. + pub const fn snap(version: SnapVersion) -> Self { + Self::new_static("snap", version as usize) + } + /// Returns the [`EthVersion::Eth66`] capability. pub const fn eth_66() -> Self { Self::eth(EthVersion::Eth66) @@ -115,6 +120,16 @@ impl Capability { Self::eth(EthVersion::Eth71) } + /// Returns the `snap/1` capability. + pub const fn snap_1() -> Self { + Self::snap(SnapVersion::V1) + } + + /// Returns the `snap/2` capability. + pub const fn snap_2() -> Self { + Self::snap(SnapVersion::V2) + } + /// Whether this is eth v66 protocol. #[inline] pub fn is_eth_v66(&self) -> bool { diff --git a/crates/net/eth-wire-types/src/snap.rs b/crates/net/eth-wire-types/src/snap.rs index e20786b48bd..5db75a08bbb 100644 --- a/crates/net/eth-wire-types/src/snap.rs +++ b/crates/net/eth-wire-types/src/snap.rs @@ -3,13 +3,41 @@ //! facilitating the exchange of Ethereum state snapshots between peers //! Reference: [Ethereum Snapshot Protocol](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#protocol-messages) //! -//! Current version: snap/1 +//! This module currently includes snap/1 plus preparatory snap/2 message definitions. +use crate::BlockAccessLists; use alloc::vec::Vec; use alloy_primitives::{Bytes, B256}; use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable}; use reth_codecs_derive::add_arbitrary_tests; +/// Supported SNAP protocol versions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[repr(u8)] +pub enum SnapVersion { + /// The original snapshot protocol. + #[default] + V1 = 1, + /// BAL-based healing as proposed by EIP-8189. + V2 = 2, +} + +impl SnapVersion { + /// Returns the number of messages supported by this version. + pub const fn message_count(self) -> u8 { + match self { + Self::V1 => 8, + Self::V2 => 10, + } + } + + /// Returns the highest supported message id for this version. + pub const fn max_message_id(self) -> u8 { + self.message_count() - 1 + } +} + /// Message IDs for the snap sync protocol #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SnapMessageId { @@ -27,9 +55,21 @@ pub enum SnapMessageId { /// Response for the number of requested contract codes. ByteCodes = 0x05, /// Request of the number of state (either account or storage) Merkle trie nodes by path. + /// + /// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`. GetTrieNodes = 0x06, /// Response for the number of requested state trie nodes. + /// + /// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`. TrieNodes = 0x07, + /// Request BALs for a list of block hashes. + /// + /// Only valid for `snap/2`. + GetBlockAccessLists = 0x08, + /// Response containing BALs for the requested block hashes. + /// + /// Only valid for `snap/2`. + BlockAccessLists = 0x09, } /// Request for a range of accounts from the state trie. @@ -187,6 +227,30 @@ pub struct TrieNodesMessage { pub nodes: Vec, } +/// Request BALs for the given block hashes. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] +#[add_arbitrary_tests(rlp)] +pub struct GetBlockAccessListsMessage { + /// Request ID to match up responses with. + pub request_id: u64, + /// Block hashes to retrieve BALs for. + pub block_hashes: Vec, + /// Soft limit at which to stop returning data (in bytes). + pub response_bytes: u64, +} + +/// Response containing one BAL per requested block hash. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] +#[add_arbitrary_tests(rlp)] +pub struct BlockAccessListsMessage { + /// ID of the request this is a response for. + pub request_id: u64, + /// Raw BAL payloads in request order. + pub block_access_lists: BlockAccessLists, +} + /// Represents all types of messages in the snap sync protocol. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SnapProtocolMessage { @@ -203,9 +267,21 @@ pub enum SnapProtocolMessage { /// Response with contract codes - see [`ByteCodesMessage`] ByteCodes(ByteCodesMessage), /// Request for trie nodes - see [`GetTrieNodesMessage`] + /// + /// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`. GetTrieNodes(GetTrieNodesMessage), /// Response with trie nodes - see [`TrieNodesMessage`] + /// + /// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`. TrieNodes(TrieNodesMessage), + /// Request for block access lists - see [`GetBlockAccessListsMessage`] + /// + /// Only valid for `snap/2`. + GetBlockAccessLists(GetBlockAccessListsMessage), + /// Response with block access lists - see [`BlockAccessListsMessage`] + /// + /// Only valid for `snap/2`. + BlockAccessLists(BlockAccessListsMessage), } impl SnapProtocolMessage { @@ -222,6 +298,8 @@ impl SnapProtocolMessage { Self::ByteCodes(_) => SnapMessageId::ByteCodes, Self::GetTrieNodes(_) => SnapMessageId::GetTrieNodes, Self::TrieNodes(_) => SnapMessageId::TrieNodes, + Self::GetBlockAccessLists(_) => SnapMessageId::GetBlockAccessLists, + Self::BlockAccessLists(_) => SnapMessageId::BlockAccessLists, } } @@ -241,6 +319,8 @@ impl SnapProtocolMessage { Self::ByteCodes(msg) => msg.encode(&mut buf), Self::GetTrieNodes(msg) => msg.encode(&mut buf), Self::TrieNodes(msg) => msg.encode(&mut buf), + Self::GetBlockAccessLists(msg) => msg.encode(&mut buf), + Self::BlockAccessLists(msg) => msg.encode(&mut buf), } Bytes::from(buf) @@ -314,6 +394,20 @@ impl SnapProtocolMessage { TrieNodes, TrieNodesMessage ); + decode_snap_message_variant!( + message_id, + buf, + SnapMessageId::GetBlockAccessLists, + GetBlockAccessLists, + GetBlockAccessListsMessage + ); + decode_snap_message_variant!( + message_id, + buf, + SnapMessageId::BlockAccessLists, + BlockAccessLists, + BlockAccessListsMessage + ); Err(alloy_rlp::Error::Custom("Unknown message ID")) } @@ -344,6 +438,9 @@ mod tests { #[test] fn test_all_message_roundtrips() { + assert_eq!(SnapVersion::V1.message_count(), 8); + assert_eq!(SnapVersion::V2.message_count(), 10); + test_roundtrip(SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage { request_id: 42, root_hash: b256_from_u64(123), @@ -404,6 +501,20 @@ mod tests { request_id: 42, nodes: vec![Bytes::from(vec![1, 2, 3])], })); + + test_roundtrip(SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage { + request_id: 42, + block_hashes: vec![b256_from_u64(123), b256_from_u64(456)], + response_bytes: 4096, + })); + + test_roundtrip(SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage { + request_id: 42, + block_access_lists: BlockAccessLists(vec![ + Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]), + Bytes::from_static(&[0xc1, alloy_rlp::EMPTY_LIST_CODE]), + ]), + })); } #[test] diff --git a/crates/net/eth-wire/src/eth_snap_stream.rs b/crates/net/eth-wire/src/eth_snap_stream.rs index ddcbf4601e7..79fae90eff2 100644 --- a/crates/net/eth-wire/src/eth_snap_stream.rs +++ b/crates/net/eth-wire/src/eth_snap_stream.rs @@ -7,7 +7,7 @@ use super::message::MAX_MESSAGE_SIZE; use crate::{ message::{EthBroadcastMessage, ProtocolBroadcastMessage}, EthMessage, EthMessageID, EthNetworkPrimitives, EthVersion, NetworkPrimitives, ProtocolMessage, - RawCapabilityMessage, SnapMessageId, SnapProtocolMessage, + RawCapabilityMessage, SnapProtocolMessage, SnapVersion, }; use alloy_rlp::{Bytes, BytesMut, Encodable}; use core::fmt::Debug; @@ -74,6 +74,18 @@ where Self { eth_snap: EthSnapStreamInner::new(eth_version), inner: stream } } + /// Create a new eth and snap protocol stream with an explicit snap version. + pub const fn new_with_snap_version( + stream: S, + eth_version: EthVersion, + snap_version: SnapVersion, + ) -> Self { + Self { + eth_snap: EthSnapStreamInner::new_with_snap_version(eth_version, snap_version), + inner: stream, + } + } + /// Create a new eth and snap protocol stream with a custom max message size. pub const fn with_max_message_size( stream: S, @@ -86,12 +98,35 @@ where } } + /// Create a new eth and snap protocol stream with a custom max message size and snap version. + pub const fn with_max_message_size_and_snap_version( + stream: S, + eth_version: EthVersion, + snap_version: SnapVersion, + max_message_size: usize, + ) -> Self { + Self { + eth_snap: EthSnapStreamInner::with_max_message_size_and_snap_version( + eth_version, + snap_version, + max_message_size, + ), + inner: stream, + } + } + /// Returns the eth version #[inline] pub const fn eth_version(&self) -> EthVersion { self.eth_snap.eth_version() } + /// Returns the snap version. + #[inline] + pub const fn snap_version(&self) -> SnapVersion { + self.eth_snap.snap_version() + } + /// Returns the underlying stream #[inline] pub const fn inner(&self) -> &S { @@ -193,13 +228,13 @@ where } } -/// Stream handling combined eth and snap protocol logic -/// Snap version is not critical to specify yet, -/// Only one version, snap/1, does exist. +/// Stream handling combined eth and snap protocol logic. #[derive(Debug, Clone)] struct EthSnapStreamInner { /// Eth protocol version eth_version: EthVersion, + /// Snap protocol version + snap_version: SnapVersion, /// Maximum allowed ETH/Snap message size. max_message_size: usize, /// Type marker @@ -212,12 +247,26 @@ where { /// Create a new eth and snap protocol stream const fn new(eth_version: EthVersion) -> Self { - Self::with_max_message_size(eth_version, MAX_MESSAGE_SIZE) + Self::new_with_snap_version(eth_version, SnapVersion::V1) + } + + /// Create a new eth and snap protocol stream with an explicit snap version. + const fn new_with_snap_version(eth_version: EthVersion, snap_version: SnapVersion) -> Self { + Self::with_max_message_size_and_snap_version(eth_version, snap_version, MAX_MESSAGE_SIZE) } /// Create a new eth and snap protocol stream with a custom max message size. const fn with_max_message_size(eth_version: EthVersion, max_message_size: usize) -> Self { - Self { eth_version, max_message_size, _pd: PhantomData } + Self::with_max_message_size_and_snap_version(eth_version, SnapVersion::V1, max_message_size) + } + + /// Create a new eth and snap protocol stream with a custom max message size and snap version. + const fn with_max_message_size_and_snap_version( + eth_version: EthVersion, + snap_version: SnapVersion, + max_message_size: usize, + ) -> Self { + Self { eth_version, snap_version, max_message_size, _pd: PhantomData } } #[inline] @@ -225,6 +274,11 @@ where self.eth_version } + #[inline] + const fn snap_version(&self) -> SnapVersion { + self.snap_version + } + /// Decode a message from the stream fn decode_message(&self, bytes: BytesMut) -> Result, EthSnapStreamError> { if bytes.len() > self.max_message_size { @@ -256,8 +310,9 @@ where } } } else if message_id > EthMessageID::max(self.eth_version) && - message_id <= - EthMessageID::message_count(self.eth_version) + SnapMessageId::TrieNodes as u8 + message_id < + EthMessageID::message_count(self.eth_version) + + self.snap_version.message_count() { // Checks for multiplexed snap message IDs : // - message_id > EthMessageID::max() : ensures it's not an eth message @@ -313,8 +368,8 @@ mod tests { use alloy_primitives::B256; use alloy_rlp::Encodable; use reth_eth_wire_types::{ - message::RequestPair, GetAccountRangeMessage, GetBlockAccessLists, GetBlockHeaders, - HeadersDirection, + message::RequestPair, BlockAccessLists, BlockAccessListsMessage, GetAccountRangeMessage, + GetBlockAccessLists, GetBlockAccessListsMessage, GetBlockHeaders, HeadersDirection, }; // Helper to create eth message and its bytes @@ -352,6 +407,22 @@ mod tests { (snap_msg, BytesMut::from(&encoded[..])) } + fn create_snap2_message() -> (SnapProtocolMessage, BytesMut) { + let snap_msg = SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage { + request_id: 1, + block_hashes: vec![B256::default()], + response_bytes: 1000, + }); + + let inner = EthSnapStreamInner::::new_with_snap_version( + EthVersion::Eth67, + SnapVersion::V2, + ); + let encoded = inner.encode_snap_message(snap_msg.clone()); + + (snap_msg, BytesMut::from(&encoded[..])) + } + #[test] fn test_eth_message_roundtrip() { let inner = EthSnapStreamInner::::new(EthVersion::Eth67); @@ -412,6 +483,25 @@ mod tests { } } + #[test] + fn test_snap2_protocol() { + let inner = EthSnapStreamInner::::new_with_snap_version( + EthVersion::Eth67, + SnapVersion::V2, + ); + let (snap_msg, snap_bytes) = create_snap2_message(); + + let encoded_bytes = inner.encode_snap_message(snap_msg.clone()); + assert!(!encoded_bytes.is_empty()); + + let decoded_result = inner.decode_message(snap_bytes.clone()); + assert!(matches!(decoded_result, Ok(EthSnapMessage::Snap(_)))); + + if let Ok(EthSnapMessage::Snap(decoded_msg)) = inner.decode_message(snap_bytes) { + assert_eq!(decoded_msg, snap_msg); + } + } + #[test] fn test_message_id_boundaries() { let inner = EthSnapStreamInner::::new(EthVersion::Eth67); @@ -475,4 +565,24 @@ mod tests { }; assert_eq!(decoded_eth, eth_msg); } + + #[test] + fn test_snap1_rejects_snap2_message_ids() { + let inner = EthSnapStreamInner::::new(EthVersion::Eth67); + let snap2_msg = SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage { + request_id: 1, + block_access_lists: BlockAccessLists(vec![alloy_primitives::Bytes::from_static(&[ + alloy_rlp::EMPTY_LIST_CODE, + ])]), + }); + + let encoded = EthSnapStreamInner::::new_with_snap_version( + EthVersion::Eth67, + SnapVersion::V2, + ) + .encode_snap_message(snap2_msg); + + let decoded = inner.decode_message(BytesMut::from(&encoded[..])); + assert!(matches!(decoded, Err(EthSnapStreamError::UnknownMessageId(_)))); + } } diff --git a/crates/net/eth-wire/src/protocol.rs b/crates/net/eth-wire/src/protocol.rs index 488f402aa51..4f886c89505 100644 --- a/crates/net/eth-wire/src/protocol.rs +++ b/crates/net/eth-wire/src/protocol.rs @@ -1,6 +1,6 @@ //! A Protocol defines a P2P subprotocol in an `RLPx` connection -use crate::{Capability, EthMessageID, EthVersion}; +use crate::{Capability, EthMessageID, EthVersion, SnapVersion}; /// Type that represents a [Capability] and the number of messages it uses. /// @@ -30,6 +30,13 @@ impl Protocol { Self::new(cap, messages) } + /// Returns the corresponding snap capability for the given version. + pub const fn snap(version: SnapVersion) -> Self { + let cap = Capability::snap(version); + let messages = version.message_count(); + Self::new(cap, messages) + } + /// Returns the [`EthVersion::Eth66`] capability. pub const fn eth_66() -> Self { Self::eth(EthVersion::Eth66) @@ -45,6 +52,16 @@ impl Protocol { Self::eth(EthVersion::Eth68) } + /// Returns the `snap/1` capability. + pub const fn snap_1() -> Self { + Self::snap(SnapVersion::V1) + } + + /// Returns the `snap/2` capability. + pub const fn snap_2() -> Self { + Self::snap(SnapVersion::V2) + } + /// Consumes the type and returns a tuple of the [Capability] and number of messages. #[inline] pub(crate) fn split(self) -> (Capability, u8) { @@ -86,5 +103,7 @@ mod tests { assert_eq!(Protocol::eth(EthVersion::Eth69).messages(), 18); assert_eq!(Protocol::eth(EthVersion::Eth70).messages(), 18); assert_eq!(Protocol::eth(EthVersion::Eth71).messages(), 20); + assert_eq!(Protocol::snap(SnapVersion::V1).messages(), 8); + assert_eq!(Protocol::snap(SnapVersion::V2).messages(), 10); } }