Skip to main content

reth_eth_wire_types/
snap.rs

1//! Implements Ethereum SNAP message types.
2//! Snap protocol runs on top of `RLPx`
3//! facilitating the exchange of Ethereum state snapshots between peers
4//! Reference: [Ethereum Snapshot Protocol](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#protocol-messages)
5//!
6//! This module implements the snap/2 (EIP-8189) message definitions.
7
8use crate::BlockAccessLists;
9use alloc::vec::Vec;
10use alloy_primitives::{Bytes, B256};
11use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
12use reth_codecs_derive::add_arbitrary_tests;
13
14/// Supported SNAP protocol versions.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[repr(u8)]
18pub enum SnapVersion {
19    /// BAL-based healing as proposed by EIP-8189.
20    #[default]
21    V2 = 2,
22}
23
24impl SnapVersion {
25    /// Returns the protocol message slot length for this version (not the count of valid ids; use
26    /// [`Self::supports_message_id`] to check validity).
27    pub const fn message_count(self) -> u8 {
28        match self {
29            Self::V2 => 10,
30        }
31    }
32
33    /// Returns `true` if `id` is a valid `snap/2` message id.
34    ///
35    /// snap/2 (EIP-8189) drops trie nodes (`0x06`/`0x07`) and adds BAL (`0x08`/`0x09`),
36    /// so validity is not a contiguous range.
37    pub const fn supports_message_id(self, id: u8) -> bool {
38        match self {
39            // snap/2: 0x00..=0x05 plus BAL (0x08/0x09). TrieNodes (0x06/0x07) removed.
40            Self::V2 => {
41                id <= SnapMessageId::ByteCodes as u8 ||
42                    id == SnapMessageId::GetBlockAccessLists as u8 ||
43                    id == SnapMessageId::BlockAccessLists as u8
44            }
45        }
46    }
47}
48
49/// Message IDs for the snap sync protocol
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum SnapMessageId {
52    /// Requests of an unknown number of accounts from a given account trie.
53    GetAccountRange = 0x00,
54    /// Response with the number of consecutive accounts and the Merkle proofs for the entire
55    /// range.
56    AccountRange = 0x01,
57    /// Requests for the storage slots of multiple accounts' storage tries.
58    GetStorageRanges = 0x02,
59    /// Response for the number of consecutive storage slots for the requested account.
60    StorageRanges = 0x03,
61    /// Request of the number of contract byte-codes by hash.
62    GetByteCodes = 0x04,
63    /// Response for the number of requested contract codes.
64    ByteCodes = 0x05,
65    /// Request BALs for a list of block hashes.
66    GetBlockAccessLists = 0x08,
67    /// Response containing BALs for the requested block hashes.
68    BlockAccessLists = 0x09,
69}
70
71impl SnapMessageId {
72    /// Returns the message id of the response paired with this request, or `None` if this id is
73    /// itself a response.
74    pub const fn response(self) -> Option<Self> {
75        match self {
76            Self::GetAccountRange => Some(Self::AccountRange),
77            Self::GetStorageRanges => Some(Self::StorageRanges),
78            Self::GetByteCodes => Some(Self::ByteCodes),
79            Self::GetBlockAccessLists => Some(Self::BlockAccessLists),
80            Self::AccountRange | Self::StorageRanges | Self::ByteCodes | Self::BlockAccessLists => {
81                None
82            }
83        }
84    }
85}
86
87/// Request for a range of accounts from the state trie.
88// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getaccountrange-0x00
89#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
90#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
91#[add_arbitrary_tests(rlp)]
92pub struct GetAccountRangeMessage {
93    /// Request ID to match up responses with
94    pub request_id: u64,
95    /// Root hash of the account trie to serve
96    pub root_hash: B256,
97    /// Account hash of the first to retrieve
98    pub starting_hash: B256,
99    /// Account hash after which to stop serving data
100    pub limit_hash: B256,
101    /// Soft limit at which to stop returning data
102    pub response_bytes: u64,
103}
104
105/// Account data in the response.
106#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
107#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
108#[add_arbitrary_tests(rlp)]
109pub struct AccountData {
110    /// Hash of the account address (trie path)
111    pub hash: B256,
112    /// Account body in slim format
113    pub body: Bytes,
114}
115
116/// Response containing a number of consecutive accounts and the Merkle proofs for the entire range.
117// http://github.com/ethereum/devp2p/blob/master/caps/snap.md#accountrange-0x01
118#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
119#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
120#[add_arbitrary_tests(rlp)]
121pub struct AccountRangeMessage {
122    /// ID of the request this is a response for
123    pub request_id: u64,
124    /// List of consecutive accounts from the trie
125    pub accounts: Vec<AccountData>,
126    /// List of trie nodes proving the account range
127    pub proof: Vec<Bytes>,
128}
129
130/// Request for the storage slots of multiple accounts' storage tries.
131// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getstorageranges-0x02
132#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
133#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
134#[add_arbitrary_tests(rlp)]
135pub struct GetStorageRangesMessage {
136    /// Request ID to match up responses with
137    pub request_id: u64,
138    /// Root hash of the account trie to serve
139    pub root_hash: B256,
140    /// Account hashes of the storage tries to serve
141    pub account_hashes: Vec<B256>,
142    /// Storage slot hash of the first to retrieve
143    pub starting_hash: B256,
144    /// Storage slot hash after which to stop serving
145    pub limit_hash: B256,
146    /// Soft limit at which to stop returning data
147    pub response_bytes: u64,
148}
149
150/// Storage slot data in the response.
151#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
152#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
153#[add_arbitrary_tests(rlp)]
154pub struct StorageData {
155    /// Hash of the storage slot key (trie path)
156    pub hash: B256,
157    /// Data content of the slot
158    pub data: Bytes,
159}
160
161/// Response containing a number of consecutive storage slots for the requested account
162/// and optionally the merkle proofs for the last range (boundary proofs) if it only partially
163/// covers the storage trie.
164// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#storageranges-0x03
165#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
166#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
167#[add_arbitrary_tests(rlp)]
168pub struct StorageRangesMessage {
169    /// ID of the request this is a response for
170    pub request_id: u64,
171    /// List of list of consecutive slots from the trie (one list per account)
172    pub slots: Vec<Vec<StorageData>>,
173    /// List of trie nodes proving the slot range (if partial)
174    pub proof: Vec<Bytes>,
175}
176
177/// Request to get a number of requested contract codes.
178// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getbytecodes-0x04
179#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
180#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
181#[add_arbitrary_tests(rlp)]
182pub struct GetByteCodesMessage {
183    /// Request ID to match up responses with
184    pub request_id: u64,
185    /// Code hashes to retrieve the code for
186    pub hashes: Vec<B256>,
187    /// Soft limit at which to stop returning data (in bytes)
188    pub response_bytes: u64,
189}
190
191/// Response containing a number of requested contract codes.
192// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#bytecodes-0x05
193#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
194#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
195#[add_arbitrary_tests(rlp)]
196pub struct ByteCodesMessage {
197    /// ID of the request this is a response for
198    pub request_id: u64,
199    /// The requested bytecodes in order
200    pub codes: Vec<Bytes>,
201}
202
203/// Request BALs for the given block hashes.
204#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
205#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
206#[add_arbitrary_tests(rlp)]
207pub struct GetBlockAccessListsMessage {
208    /// Request ID to match up responses with.
209    pub request_id: u64,
210    /// Block hashes to retrieve BALs for.
211    pub block_hashes: Vec<B256>,
212    /// Soft limit at which to stop returning data (in bytes).
213    pub response_bytes: u64,
214}
215
216/// Response containing one BAL per requested block hash.
217#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
218#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
219#[add_arbitrary_tests(rlp)]
220pub struct BlockAccessListsMessage {
221    /// ID of the request this is a response for.
222    pub request_id: u64,
223    /// Raw BAL payloads in request order.
224    pub block_access_lists: BlockAccessLists,
225}
226
227/// Represents all types of messages in the snap sync protocol.
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub enum SnapProtocolMessage {
230    /// Request for an account range - see [`GetAccountRangeMessage`]
231    GetAccountRange(GetAccountRangeMessage),
232    /// Response with accounts and proofs - see [`AccountRangeMessage`]
233    AccountRange(AccountRangeMessage),
234    /// Request for storage slots - see [`GetStorageRangesMessage`]
235    GetStorageRanges(GetStorageRangesMessage),
236    /// Response with storage slots - see [`StorageRangesMessage`]
237    StorageRanges(StorageRangesMessage),
238    /// Request for contract bytecodes - see [`GetByteCodesMessage`]
239    GetByteCodes(GetByteCodesMessage),
240    /// Response with contract codes - see [`ByteCodesMessage`]
241    ByteCodes(ByteCodesMessage),
242    /// Request for block access lists - see [`GetBlockAccessListsMessage`]
243    GetBlockAccessLists(GetBlockAccessListsMessage),
244    /// Response with block access lists - see [`BlockAccessListsMessage`]
245    BlockAccessLists(BlockAccessListsMessage),
246}
247
248/// Error decoding an inbound `snap` protocol message from its framed bytes.
249#[derive(thiserror::Error, Debug)]
250pub enum SnapProtocolError {
251    /// The payload was empty and carried no message id.
252    #[error("empty snap message")]
253    Empty,
254    /// The message id is not valid for the negotiated snap version (e.g. the removed trie-node
255    /// messages `0x06`/`0x07` under snap/2).
256    #[error("message id {0:#x} is invalid for snap/{1:?}")]
257    UnsupportedMessageId(u8, SnapVersion),
258    /// Decoding the RLP message body failed.
259    #[error("RLP error: {0}")]
260    Rlp(#[from] alloy_rlp::Error),
261}
262
263impl SnapProtocolMessage {
264    /// Returns the protocol message ID for this message type.
265    ///
266    /// The message ID is used in the `RLPx` protocol to identify different types of messages.
267    pub const fn message_id(&self) -> SnapMessageId {
268        match self {
269            Self::GetAccountRange(_) => SnapMessageId::GetAccountRange,
270            Self::AccountRange(_) => SnapMessageId::AccountRange,
271            Self::GetStorageRanges(_) => SnapMessageId::GetStorageRanges,
272            Self::StorageRanges(_) => SnapMessageId::StorageRanges,
273            Self::GetByteCodes(_) => SnapMessageId::GetByteCodes,
274            Self::ByteCodes(_) => SnapMessageId::ByteCodes,
275            Self::GetBlockAccessLists(_) => SnapMessageId::GetBlockAccessLists,
276            Self::BlockAccessLists(_) => SnapMessageId::BlockAccessLists,
277        }
278    }
279
280    /// Returns the `request_id` used to correlate this message with its request/response pair.
281    pub const fn request_id(&self) -> u64 {
282        match self {
283            Self::GetAccountRange(m) => m.request_id,
284            Self::AccountRange(m) => m.request_id,
285            Self::GetStorageRanges(m) => m.request_id,
286            Self::StorageRanges(m) => m.request_id,
287            Self::GetByteCodes(m) => m.request_id,
288            Self::ByteCodes(m) => m.request_id,
289            Self::GetBlockAccessLists(m) => m.request_id,
290            Self::BlockAccessLists(m) => m.request_id,
291        }
292    }
293
294    /// Returns `true` if this is a response message (as opposed to a request).
295    pub const fn is_response(&self) -> bool {
296        matches!(
297            self,
298            Self::AccountRange(_) |
299                Self::StorageRanges(_) |
300                Self::ByteCodes(_) |
301                Self::BlockAccessLists(_)
302        )
303    }
304
305    /// Overwrites the `request_id`, e.g. so a session can assign a connection-unique id before
306    /// sending a request.
307    pub const fn set_request_id(&mut self, request_id: u64) {
308        match self {
309            Self::GetAccountRange(m) => m.request_id = request_id,
310            Self::AccountRange(m) => m.request_id = request_id,
311            Self::GetStorageRanges(m) => m.request_id = request_id,
312            Self::StorageRanges(m) => m.request_id = request_id,
313            Self::GetByteCodes(m) => m.request_id = request_id,
314            Self::ByteCodes(m) => m.request_id = request_id,
315            Self::GetBlockAccessLists(m) => m.request_id = request_id,
316            Self::BlockAccessLists(m) => m.request_id = request_id,
317        }
318    }
319
320    /// Encode the message to bytes
321    pub fn encode(&self) -> Bytes {
322        let mut buf = Vec::new();
323        // Add message ID as first byte
324        buf.push(self.message_id() as u8);
325
326        // Encode the message body based on its type
327        match self {
328            Self::GetAccountRange(msg) => msg.encode(&mut buf),
329            Self::AccountRange(msg) => msg.encode(&mut buf),
330            Self::GetStorageRanges(msg) => msg.encode(&mut buf),
331            Self::StorageRanges(msg) => msg.encode(&mut buf),
332            Self::GetByteCodes(msg) => msg.encode(&mut buf),
333            Self::ByteCodes(msg) => msg.encode(&mut buf),
334            Self::GetBlockAccessLists(msg) => msg.encode(&mut buf),
335            Self::BlockAccessLists(msg) => msg.encode(&mut buf),
336        }
337
338        Bytes::from(buf)
339    }
340
341    /// Decodes a SNAP protocol message from its message ID and RLP-encoded body.
342    pub fn decode(message_id: u8, buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
343        // Decoding protocol message variants based on message ID
344        macro_rules! decode_snap_message_variant {
345            ($message_id:expr, $buf:expr, $id:expr, $variant:ident, $msg_type:ty) => {
346                if $message_id == $id as u8 {
347                    return Ok(Self::$variant(<$msg_type>::decode($buf)?));
348                }
349            };
350        }
351
352        // Try to decode each message type based on the message ID
353        decode_snap_message_variant!(
354            message_id,
355            buf,
356            SnapMessageId::GetAccountRange,
357            GetAccountRange,
358            GetAccountRangeMessage
359        );
360        decode_snap_message_variant!(
361            message_id,
362            buf,
363            SnapMessageId::AccountRange,
364            AccountRange,
365            AccountRangeMessage
366        );
367        decode_snap_message_variant!(
368            message_id,
369            buf,
370            SnapMessageId::GetStorageRanges,
371            GetStorageRanges,
372            GetStorageRangesMessage
373        );
374        decode_snap_message_variant!(
375            message_id,
376            buf,
377            SnapMessageId::StorageRanges,
378            StorageRanges,
379            StorageRangesMessage
380        );
381        decode_snap_message_variant!(
382            message_id,
383            buf,
384            SnapMessageId::GetByteCodes,
385            GetByteCodes,
386            GetByteCodesMessage
387        );
388        decode_snap_message_variant!(
389            message_id,
390            buf,
391            SnapMessageId::ByteCodes,
392            ByteCodes,
393            ByteCodesMessage
394        );
395        decode_snap_message_variant!(
396            message_id,
397            buf,
398            SnapMessageId::GetBlockAccessLists,
399            GetBlockAccessLists,
400            GetBlockAccessListsMessage
401        );
402        decode_snap_message_variant!(
403            message_id,
404            buf,
405            SnapMessageId::BlockAccessLists,
406            BlockAccessLists,
407            BlockAccessListsMessage
408        );
409
410        Err(alloy_rlp::Error::Custom("Unknown message ID"))
411    }
412
413    /// Decodes a framed snap message (`[id, body..]`), validating the id against `version`.
414    ///
415    /// Empty payload, invalid id, and malformed body are reported as distinct
416    /// [`SnapProtocolError`] variants.
417    pub fn decode_versioned(version: SnapVersion, bytes: &[u8]) -> Result<Self, SnapProtocolError> {
418        let (&id, mut body) = bytes.split_first().ok_or(SnapProtocolError::Empty)?;
419        if !version.supports_message_id(id) {
420            return Err(SnapProtocolError::UnsupportedMessageId(id, version));
421        }
422        let msg = Self::decode(id, &mut body)?;
423        if !body.is_empty() {
424            return Err(SnapProtocolError::Rlp(alloy_rlp::Error::UnexpectedLength));
425        }
426        Ok(msg)
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use test_case::test_case;
434
435    // Helper function to create a B256 from a u64 for testing
436    fn b256_from_u64(value: u64) -> B256 {
437        B256::left_padding_from(&value.to_be_bytes())
438    }
439
440    // Helper function to test roundtrip encoding/decoding
441    fn test_roundtrip(original: SnapProtocolMessage) {
442        let encoded = original.encode();
443
444        // Verify the first byte matches the expected message ID
445        assert_eq!(encoded[0], original.message_id() as u8);
446
447        let mut buf = &encoded[1..];
448        let decoded = SnapProtocolMessage::decode(encoded[0], &mut buf).unwrap();
449
450        // Verify the match
451        assert_eq!(decoded, original);
452    }
453
454    #[test]
455    fn test_all_message_roundtrips() {
456        assert_eq!(SnapVersion::V2.message_count(), 10);
457
458        test_roundtrip(SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
459            request_id: 42,
460            root_hash: b256_from_u64(123),
461            starting_hash: b256_from_u64(456),
462            limit_hash: b256_from_u64(789),
463            response_bytes: 1024,
464        }));
465
466        test_roundtrip(SnapProtocolMessage::AccountRange(AccountRangeMessage {
467            request_id: 42,
468            accounts: vec![AccountData {
469                hash: b256_from_u64(123),
470                body: Bytes::from(vec![1, 2, 3]),
471            }],
472            proof: vec![Bytes::from(vec![4, 5, 6])],
473        }));
474
475        test_roundtrip(SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
476            request_id: 42,
477            root_hash: b256_from_u64(123),
478            account_hashes: vec![b256_from_u64(456)],
479            starting_hash: b256_from_u64(789),
480            limit_hash: b256_from_u64(101112),
481            response_bytes: 2048,
482        }));
483
484        test_roundtrip(SnapProtocolMessage::StorageRanges(StorageRangesMessage {
485            request_id: 42,
486            slots: vec![vec![StorageData {
487                hash: b256_from_u64(123),
488                data: Bytes::from(vec![1, 2, 3]),
489            }]],
490            proof: vec![Bytes::from(vec![4, 5, 6])],
491        }));
492
493        test_roundtrip(SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
494            request_id: 42,
495            hashes: vec![b256_from_u64(123)],
496            response_bytes: 1024,
497        }));
498
499        test_roundtrip(SnapProtocolMessage::ByteCodes(ByteCodesMessage {
500            request_id: 42,
501            codes: vec![Bytes::from(vec![1, 2, 3])],
502        }));
503
504        test_roundtrip(SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
505            request_id: 42,
506            block_hashes: vec![b256_from_u64(123), b256_from_u64(456)],
507            response_bytes: 4096,
508        }));
509
510        test_roundtrip(SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
511            request_id: 42,
512            block_access_lists: BlockAccessLists(vec![
513                Some(Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE])),
514                Some(Bytes::from_static(&[0xc1, alloy_rlp::EMPTY_LIST_CODE])),
515            ]),
516        }));
517    }
518
519    #[test]
520    fn test_unknown_message_id() {
521        // Create some random data
522        let data = Bytes::from(vec![1, 2, 3, 4]);
523        let mut buf = data.as_ref();
524
525        // Try to decode with an invalid message ID
526        let result = SnapProtocolMessage::decode(255, &mut buf);
527
528        assert!(result.is_err());
529        if let Err(e) = result {
530            assert_eq!(e.to_string(), "Unknown message ID");
531        }
532    }
533
534    #[test]
535    fn test_snap_v2_message_validity() {
536        let v2 = SnapVersion::V2;
537        // 0x00..=0x05 valid.
538        for id in 0x00..=0x05 {
539            assert!(v2.supports_message_id(id), "snap/2 should accept {id:#x}");
540        }
541        // Trie nodes (0x06/0x07) are removed in snap/2.
542        assert!(!v2.supports_message_id(0x06));
543        assert!(!v2.supports_message_id(0x07));
544        // BAL added in snap/2.
545        assert!(v2.supports_message_id(SnapMessageId::GetBlockAccessLists as u8));
546        assert!(v2.supports_message_id(SnapMessageId::BlockAccessLists as u8));
547        assert!(!v2.supports_message_id(0x0a));
548        assert!(!v2.supports_message_id(0xff));
549    }
550
551    #[test_case(
552        SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
553            request_id: 1, root_hash: B256::ZERO, starting_hash: B256::ZERO,
554            limit_hash: B256::ZERO, response_bytes: 0,
555        }), 1, false ; "get_account_range is a request"
556    )]
557    #[test_case(
558        SnapProtocolMessage::AccountRange(AccountRangeMessage {
559            request_id: 2, accounts: vec![], proof: vec![],
560        }), 2, true ; "account_range is a response"
561    )]
562    #[test_case(
563        SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
564            request_id: 3, root_hash: B256::ZERO, account_hashes: vec![],
565            starting_hash: B256::ZERO, limit_hash: B256::ZERO, response_bytes: 0,
566        }), 3, false ; "get_storage_ranges is a request"
567    )]
568    #[test_case(
569        SnapProtocolMessage::StorageRanges(StorageRangesMessage {
570            request_id: 4, slots: vec![], proof: vec![],
571        }), 4, true ; "storage_ranges is a response"
572    )]
573    #[test_case(
574        SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
575            request_id: 5, hashes: vec![], response_bytes: 0,
576        }), 5, false ; "get_byte_codes is a request"
577    )]
578    #[test_case(
579        SnapProtocolMessage::ByteCodes(ByteCodesMessage { request_id: 6, codes: vec![] }),
580        6, true ; "byte_codes is a response"
581    )]
582    #[test_case(
583        SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
584            request_id: 7, block_hashes: vec![], response_bytes: 0,
585        }), 7, false ; "get_block_access_lists is a request"
586    )]
587    #[test_case(
588        SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
589            request_id: 8, block_access_lists: BlockAccessLists(vec![]),
590        }), 8, true ; "block_access_lists is a response"
591    )]
592    fn request_id_and_is_response(msg: SnapProtocolMessage, expected_id: u64, is_response: bool) {
593        assert_eq!(msg.request_id(), expected_id);
594        assert_eq!(msg.is_response(), is_response);
595    }
596
597    #[test_case(
598        SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
599            request_id: 1, root_hash: B256::ZERO, starting_hash: B256::ZERO,
600            limit_hash: B256::ZERO, response_bytes: 0,
601        }) ; "get_account_range"
602    )]
603    #[test_case(
604        SnapProtocolMessage::AccountRange(AccountRangeMessage {
605            request_id: 1, accounts: vec![], proof: vec![],
606        }) ; "account_range"
607    )]
608    #[test_case(
609        SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
610            request_id: 1, root_hash: B256::ZERO, account_hashes: vec![],
611            starting_hash: B256::ZERO, limit_hash: B256::ZERO, response_bytes: 0,
612        }) ; "get_storage_ranges"
613    )]
614    #[test_case(
615        SnapProtocolMessage::StorageRanges(StorageRangesMessage {
616            request_id: 1, slots: vec![], proof: vec![],
617        }) ; "storage_ranges"
618    )]
619    #[test_case(
620        SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
621            request_id: 1, hashes: vec![], response_bytes: 0,
622        }) ; "get_byte_codes"
623    )]
624    #[test_case(
625        SnapProtocolMessage::ByteCodes(ByteCodesMessage { request_id: 1, codes: vec![] }) ;
626        "byte_codes"
627    )]
628    #[test_case(
629        SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
630            request_id: 1, block_hashes: vec![], response_bytes: 0,
631        }) ; "get_block_access_lists"
632    )]
633    #[test_case(
634        SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
635            request_id: 1, block_access_lists: BlockAccessLists(vec![]),
636        }) ; "block_access_lists"
637    )]
638    fn per_variant_request_id_and_round_trip(mut msg: SnapProtocolMessage) {
639        // set_request_id overwrites the id for every variant.
640        msg.set_request_id(42);
641        assert_eq!(msg.request_id(), 42);
642
643        // decode_versioned round-trips every valid snap/2 id.
644        let decoded =
645            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &msg.encode()).unwrap();
646        assert_eq!(decoded, msg);
647    }
648
649    #[test]
650    fn decode_versioned_rejects_empty() {
651        // An empty payload carries no message id and is distinct from an invalid id.
652        assert!(matches!(
653            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[]),
654            Err(SnapProtocolError::Empty)
655        ));
656    }
657
658    #[test]
659    fn decode_versioned_rejects_trie_node_ids_in_v2() {
660        // snap/2 (EIP-8189) removes trie nodes (`0x06`/`0x07`); decoding must reject them as an
661        // unsupported id rather than a malformed body.
662        for id in [0x06u8, 0x07] {
663            assert!(matches!(
664                SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[id]),
665                Err(SnapProtocolError::UnsupportedMessageId(got, SnapVersion::V2)) if got == id
666            ));
667        }
668    }
669
670    #[test]
671    fn decode_versioned_reports_malformed_body() {
672        // A valid id (GetBlockAccessLists, 0x08) with a non-decodable body is an RLP error, not an
673        // unsupported id.
674        assert!(matches!(
675            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[0x08, 0xff]),
676            Err(SnapProtocolError::Rlp(_))
677        ));
678    }
679
680    #[test]
681    fn decode_versioned_rejects_trailing_bytes() {
682        // A valid framed message with junk appended after the RLP body must be rejected rather
683        // than silently decoded.
684        let original = SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
685            request_id: 7,
686            block_hashes: vec![b256_from_u64(1)],
687            response_bytes: 1024,
688        });
689        let mut framed = original.encode().to_vec();
690        framed.push(0xff);
691        assert!(matches!(
692            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &framed),
693            Err(SnapProtocolError::Rlp(alloy_rlp::Error::UnexpectedLength))
694        ));
695    }
696}