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::{BufMut, 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; unbounded (served as `B256::ZERO`) when the
143    /// wire encoding is an empty byte string.
144    pub starting_hash: RangeBound,
145    /// Storage slot hash after which to stop serving; unbounded (served as
146    /// `B256::repeat_byte(0xff)`) when the wire encoding is an empty byte string.
147    pub limit_hash: RangeBound,
148    /// Soft limit at which to stop returning data
149    pub response_bytes: u64,
150}
151
152/// A `snap/2` storage-range bound (`origin`/`limit` on [`GetStorageRangesMessage`]).
153///
154/// Encoded as either an empty byte string (unbounded) or a 32-byte hash.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
156#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
157pub struct RangeBound(Option<B256>);
158
159impl RangeBound {
160    /// Returns the bound's hash, or `default` if it was encoded as an empty byte string.
161    pub const fn unwrap_or(self, default: B256) -> B256 {
162        match self.0 {
163            Some(hash) => hash,
164            None => default,
165        }
166    }
167}
168
169impl From<B256> for RangeBound {
170    fn from(hash: B256) -> Self {
171        Self(Some(hash))
172    }
173}
174
175impl Encodable for RangeBound {
176    fn encode(&self, out: &mut dyn BufMut) {
177        match self.0 {
178            Some(hash) => hash.encode(out),
179            None => Bytes::new().encode(out),
180        }
181    }
182
183    fn length(&self) -> usize {
184        match self.0 {
185            Some(hash) => hash.length(),
186            None => Bytes::new().length(),
187        }
188    }
189}
190
191impl Decodable for RangeBound {
192    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
193        let bytes = Bytes::decode(buf)?;
194        match bytes.len() {
195            0 => Ok(Self(None)),
196            32 => Ok(Self(Some(B256::from_slice(&bytes)))),
197            _ => Err(alloy_rlp::Error::UnexpectedLength),
198        }
199    }
200}
201
202/// Storage slot data in the response.
203#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
204#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
205#[add_arbitrary_tests(rlp)]
206pub struct StorageData {
207    /// Hash of the storage slot key (trie path)
208    pub hash: B256,
209    /// Data content of the slot
210    pub data: Bytes,
211}
212
213/// Response containing a number of consecutive storage slots for the requested account
214/// and optionally the merkle proofs for the last range (boundary proofs) if it only partially
215/// covers the storage trie.
216// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#storageranges-0x03
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 StorageRangesMessage {
221    /// ID of the request this is a response for
222    pub request_id: u64,
223    /// List of list of consecutive slots from the trie (one list per account)
224    pub slots: Vec<Vec<StorageData>>,
225    /// List of trie nodes proving the slot range (if partial)
226    pub proof: Vec<Bytes>,
227}
228
229/// Request to get a number of requested contract codes.
230// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getbytecodes-0x04
231#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
232#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
233#[add_arbitrary_tests(rlp)]
234pub struct GetByteCodesMessage {
235    /// Request ID to match up responses with
236    pub request_id: u64,
237    /// Code hashes to retrieve the code for
238    pub hashes: Vec<B256>,
239    /// Soft limit at which to stop returning data (in bytes)
240    pub response_bytes: u64,
241}
242
243/// Response containing a number of requested contract codes.
244// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#bytecodes-0x05
245#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
246#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
247#[add_arbitrary_tests(rlp)]
248pub struct ByteCodesMessage {
249    /// ID of the request this is a response for
250    pub request_id: u64,
251    /// The requested bytecodes in order
252    pub codes: Vec<Bytes>,
253}
254
255/// Request BALs for the given block hashes.
256#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
257#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
258#[add_arbitrary_tests(rlp)]
259pub struct GetBlockAccessListsMessage {
260    /// Request ID to match up responses with.
261    pub request_id: u64,
262    /// Block hashes to retrieve BALs for.
263    pub block_hashes: Vec<B256>,
264    /// Soft limit at which to stop returning data (in bytes).
265    pub response_bytes: u64,
266}
267
268/// Response containing one BAL per requested block hash.
269#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
270#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
271#[add_arbitrary_tests(rlp)]
272pub struct BlockAccessListsMessage {
273    /// ID of the request this is a response for.
274    pub request_id: u64,
275    /// Raw BAL payloads in request order.
276    pub block_access_lists: BlockAccessLists,
277}
278
279/// Represents all types of messages in the snap sync protocol.
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub enum SnapProtocolMessage {
282    /// Request for an account range - see [`GetAccountRangeMessage`]
283    GetAccountRange(GetAccountRangeMessage),
284    /// Response with accounts and proofs - see [`AccountRangeMessage`]
285    AccountRange(AccountRangeMessage),
286    /// Request for storage slots - see [`GetStorageRangesMessage`]
287    GetStorageRanges(GetStorageRangesMessage),
288    /// Response with storage slots - see [`StorageRangesMessage`]
289    StorageRanges(StorageRangesMessage),
290    /// Request for contract bytecodes - see [`GetByteCodesMessage`]
291    GetByteCodes(GetByteCodesMessage),
292    /// Response with contract codes - see [`ByteCodesMessage`]
293    ByteCodes(ByteCodesMessage),
294    /// Request for block access lists - see [`GetBlockAccessListsMessage`]
295    GetBlockAccessLists(GetBlockAccessListsMessage),
296    /// Response with block access lists - see [`BlockAccessListsMessage`]
297    BlockAccessLists(BlockAccessListsMessage),
298}
299
300/// Error decoding an inbound `snap` protocol message from its framed bytes.
301#[derive(thiserror::Error, Debug)]
302pub enum SnapProtocolError {
303    /// The payload was empty and carried no message id.
304    #[error("empty snap message")]
305    Empty,
306    /// The message id is not valid for the negotiated snap version (e.g. the removed trie-node
307    /// messages `0x06`/`0x07` under snap/2).
308    #[error("message id {0:#x} is invalid for snap/{1:?}")]
309    UnsupportedMessageId(u8, SnapVersion),
310    /// Decoding the RLP message body failed.
311    #[error("RLP error: {0}")]
312    Rlp(#[from] alloy_rlp::Error),
313}
314
315impl SnapProtocolMessage {
316    /// Returns the protocol message ID for this message type.
317    ///
318    /// The message ID is used in the `RLPx` protocol to identify different types of messages.
319    pub const fn message_id(&self) -> SnapMessageId {
320        match self {
321            Self::GetAccountRange(_) => SnapMessageId::GetAccountRange,
322            Self::AccountRange(_) => SnapMessageId::AccountRange,
323            Self::GetStorageRanges(_) => SnapMessageId::GetStorageRanges,
324            Self::StorageRanges(_) => SnapMessageId::StorageRanges,
325            Self::GetByteCodes(_) => SnapMessageId::GetByteCodes,
326            Self::ByteCodes(_) => SnapMessageId::ByteCodes,
327            Self::GetBlockAccessLists(_) => SnapMessageId::GetBlockAccessLists,
328            Self::BlockAccessLists(_) => SnapMessageId::BlockAccessLists,
329        }
330    }
331
332    /// Returns the `request_id` used to correlate this message with its request/response pair.
333    pub const fn request_id(&self) -> u64 {
334        match self {
335            Self::GetAccountRange(m) => m.request_id,
336            Self::AccountRange(m) => m.request_id,
337            Self::GetStorageRanges(m) => m.request_id,
338            Self::StorageRanges(m) => m.request_id,
339            Self::GetByteCodes(m) => m.request_id,
340            Self::ByteCodes(m) => m.request_id,
341            Self::GetBlockAccessLists(m) => m.request_id,
342            Self::BlockAccessLists(m) => m.request_id,
343        }
344    }
345
346    /// Returns `true` if this is a response message (as opposed to a request).
347    pub const fn is_response(&self) -> bool {
348        matches!(
349            self,
350            Self::AccountRange(_) |
351                Self::StorageRanges(_) |
352                Self::ByteCodes(_) |
353                Self::BlockAccessLists(_)
354        )
355    }
356
357    /// Overwrites the `request_id`, e.g. so a session can assign a connection-unique id before
358    /// sending a request.
359    pub const fn set_request_id(&mut self, request_id: u64) {
360        match self {
361            Self::GetAccountRange(m) => m.request_id = request_id,
362            Self::AccountRange(m) => m.request_id = request_id,
363            Self::GetStorageRanges(m) => m.request_id = request_id,
364            Self::StorageRanges(m) => m.request_id = request_id,
365            Self::GetByteCodes(m) => m.request_id = request_id,
366            Self::ByteCodes(m) => m.request_id = request_id,
367            Self::GetBlockAccessLists(m) => m.request_id = request_id,
368            Self::BlockAccessLists(m) => m.request_id = request_id,
369        }
370    }
371
372    /// Encode the message to bytes
373    pub fn encode(&self) -> Bytes {
374        let mut buf = Vec::new();
375        // Add message ID as first byte
376        buf.push(self.message_id() as u8);
377
378        // Encode the message body based on its type
379        match self {
380            Self::GetAccountRange(msg) => msg.encode(&mut buf),
381            Self::AccountRange(msg) => msg.encode(&mut buf),
382            Self::GetStorageRanges(msg) => msg.encode(&mut buf),
383            Self::StorageRanges(msg) => msg.encode(&mut buf),
384            Self::GetByteCodes(msg) => msg.encode(&mut buf),
385            Self::ByteCodes(msg) => msg.encode(&mut buf),
386            Self::GetBlockAccessLists(msg) => msg.encode(&mut buf),
387            Self::BlockAccessLists(msg) => msg.encode(&mut buf),
388        }
389
390        Bytes::from(buf)
391    }
392
393    /// Decodes a SNAP protocol message from its message ID and RLP-encoded body.
394    pub fn decode(message_id: u8, buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
395        // Decoding protocol message variants based on message ID
396        macro_rules! decode_snap_message_variant {
397            ($message_id:expr, $buf:expr, $id:expr, $variant:ident, $msg_type:ty) => {
398                if $message_id == $id as u8 {
399                    return Ok(Self::$variant(<$msg_type>::decode($buf)?));
400                }
401            };
402        }
403
404        // Try to decode each message type based on the message ID
405        decode_snap_message_variant!(
406            message_id,
407            buf,
408            SnapMessageId::GetAccountRange,
409            GetAccountRange,
410            GetAccountRangeMessage
411        );
412        decode_snap_message_variant!(
413            message_id,
414            buf,
415            SnapMessageId::AccountRange,
416            AccountRange,
417            AccountRangeMessage
418        );
419        decode_snap_message_variant!(
420            message_id,
421            buf,
422            SnapMessageId::GetStorageRanges,
423            GetStorageRanges,
424            GetStorageRangesMessage
425        );
426        decode_snap_message_variant!(
427            message_id,
428            buf,
429            SnapMessageId::StorageRanges,
430            StorageRanges,
431            StorageRangesMessage
432        );
433        decode_snap_message_variant!(
434            message_id,
435            buf,
436            SnapMessageId::GetByteCodes,
437            GetByteCodes,
438            GetByteCodesMessage
439        );
440        decode_snap_message_variant!(
441            message_id,
442            buf,
443            SnapMessageId::ByteCodes,
444            ByteCodes,
445            ByteCodesMessage
446        );
447        decode_snap_message_variant!(
448            message_id,
449            buf,
450            SnapMessageId::GetBlockAccessLists,
451            GetBlockAccessLists,
452            GetBlockAccessListsMessage
453        );
454        decode_snap_message_variant!(
455            message_id,
456            buf,
457            SnapMessageId::BlockAccessLists,
458            BlockAccessLists,
459            BlockAccessListsMessage
460        );
461
462        Err(alloy_rlp::Error::Custom("Unknown message ID"))
463    }
464
465    /// Decodes a framed snap message (`[id, body..]`), validating the id against `version`.
466    ///
467    /// Empty payload, invalid id, and malformed body are reported as distinct
468    /// [`SnapProtocolError`] variants.
469    pub fn decode_versioned(version: SnapVersion, bytes: &[u8]) -> Result<Self, SnapProtocolError> {
470        let (&id, mut body) = bytes.split_first().ok_or(SnapProtocolError::Empty)?;
471        if !version.supports_message_id(id) {
472            return Err(SnapProtocolError::UnsupportedMessageId(id, version));
473        }
474        let msg = Self::decode(id, &mut body)?;
475        if !body.is_empty() {
476            return Err(SnapProtocolError::Rlp(alloy_rlp::Error::UnexpectedLength));
477        }
478        Ok(msg)
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use test_case::test_case;
486
487    // Helper function to create a B256 from a u64 for testing
488    fn b256_from_u64(value: u64) -> B256 {
489        B256::left_padding_from(&value.to_be_bytes())
490    }
491
492    // Helper function to test roundtrip encoding/decoding
493    fn test_roundtrip(original: SnapProtocolMessage) {
494        let encoded = original.encode();
495
496        // Verify the first byte matches the expected message ID
497        assert_eq!(encoded[0], original.message_id() as u8);
498
499        let mut buf = &encoded[1..];
500        let decoded = SnapProtocolMessage::decode(encoded[0], &mut buf).unwrap();
501
502        // Verify the match
503        assert_eq!(decoded, original);
504    }
505
506    // geth's GetStorageRangesPacket types Origin/Limit as raw byte strings and sends them
507    // empty for the common unbounded multi-account request, rather than as 32-byte
508    // zero/max-value hashes. A conforming decoder must accept this raw packet shape.
509    #[derive(alloy_rlp::RlpEncodable)]
510    struct GethStorageRequest {
511        request_id: u64,
512        root_hash: B256,
513        account_hashes: Vec<B256>,
514        origin: Bytes,
515        limit: Bytes,
516        response_bytes: u64,
517    }
518
519    #[test]
520    fn test_all_message_roundtrips() {
521        assert_eq!(SnapVersion::V2.message_count(), 10);
522
523        test_roundtrip(SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
524            request_id: 42,
525            root_hash: b256_from_u64(123),
526            starting_hash: b256_from_u64(456),
527            limit_hash: b256_from_u64(789),
528            response_bytes: 1024,
529        }));
530
531        test_roundtrip(SnapProtocolMessage::AccountRange(AccountRangeMessage {
532            request_id: 42,
533            accounts: vec![AccountData {
534                hash: b256_from_u64(123),
535                body: Bytes::from(vec![1, 2, 3]),
536            }],
537            proof: vec![Bytes::from(vec![4, 5, 6])],
538        }));
539
540        test_roundtrip(SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
541            request_id: 42,
542            root_hash: b256_from_u64(123),
543            account_hashes: vec![b256_from_u64(456)],
544            starting_hash: b256_from_u64(789).into(),
545            limit_hash: b256_from_u64(101112).into(),
546            response_bytes: 2048,
547        }));
548
549        // Geth's empty-byte-string encoding for an unbounded storage range.
550        test_roundtrip(SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
551            request_id: 43,
552            root_hash: b256_from_u64(123),
553            account_hashes: vec![b256_from_u64(456), b256_from_u64(789)],
554            starting_hash: RangeBound::default(),
555            limit_hash: RangeBound::default(),
556            response_bytes: 2048,
557        }));
558
559        test_roundtrip(SnapProtocolMessage::StorageRanges(StorageRangesMessage {
560            request_id: 42,
561            slots: vec![vec![StorageData {
562                hash: b256_from_u64(123),
563                data: Bytes::from(vec![1, 2, 3]),
564            }]],
565            proof: vec![Bytes::from(vec![4, 5, 6])],
566        }));
567
568        test_roundtrip(SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
569            request_id: 42,
570            hashes: vec![b256_from_u64(123)],
571            response_bytes: 1024,
572        }));
573
574        test_roundtrip(SnapProtocolMessage::ByteCodes(ByteCodesMessage {
575            request_id: 42,
576            codes: vec![Bytes::from(vec![1, 2, 3])],
577        }));
578
579        test_roundtrip(SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
580            request_id: 42,
581            block_hashes: vec![b256_from_u64(123), b256_from_u64(456)],
582            response_bytes: 4096,
583        }));
584
585        test_roundtrip(SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
586            request_id: 42,
587            block_access_lists: BlockAccessLists(vec![
588                Some(Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE])),
589                Some(Bytes::from_static(&[0xc1, alloy_rlp::EMPTY_LIST_CODE])),
590            ]),
591        }));
592    }
593
594    #[test]
595    fn test_unknown_message_id() {
596        // Create some random data
597        let data = Bytes::from(vec![1, 2, 3, 4]);
598        let mut buf = data.as_ref();
599
600        // Try to decode with an invalid message ID
601        let result = SnapProtocolMessage::decode(255, &mut buf);
602
603        assert!(result.is_err());
604        if let Err(e) = result {
605            assert_eq!(e.to_string(), "Unknown message ID");
606        }
607    }
608
609    #[test]
610    fn test_snap_v2_message_validity() {
611        let v2 = SnapVersion::V2;
612        // 0x00..=0x05 valid.
613        for id in 0x00..=0x05 {
614            assert!(v2.supports_message_id(id), "snap/2 should accept {id:#x}");
615        }
616        // Trie nodes (0x06/0x07) are removed in snap/2.
617        assert!(!v2.supports_message_id(0x06));
618        assert!(!v2.supports_message_id(0x07));
619        // BAL added in snap/2.
620        assert!(v2.supports_message_id(SnapMessageId::GetBlockAccessLists as u8));
621        assert!(v2.supports_message_id(SnapMessageId::BlockAccessLists as u8));
622        assert!(!v2.supports_message_id(0x0a));
623        assert!(!v2.supports_message_id(0xff));
624    }
625
626    #[test_case(
627        SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
628            request_id: 1, root_hash: B256::ZERO, starting_hash: B256::ZERO,
629            limit_hash: B256::ZERO, response_bytes: 0,
630        }), 1, false ; "get_account_range is a request"
631    )]
632    #[test_case(
633        SnapProtocolMessage::AccountRange(AccountRangeMessage {
634            request_id: 2, accounts: vec![], proof: vec![],
635        }), 2, true ; "account_range is a response"
636    )]
637    #[test_case(
638        SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
639            request_id: 3, root_hash: B256::ZERO, account_hashes: vec![],
640            starting_hash: B256::ZERO.into(), limit_hash: B256::ZERO.into(), response_bytes: 0,
641        }), 3, false ; "get_storage_ranges is a request"
642    )]
643    #[test_case(
644        SnapProtocolMessage::StorageRanges(StorageRangesMessage {
645            request_id: 4, slots: vec![], proof: vec![],
646        }), 4, true ; "storage_ranges is a response"
647    )]
648    #[test_case(
649        SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
650            request_id: 5, hashes: vec![], response_bytes: 0,
651        }), 5, false ; "get_byte_codes is a request"
652    )]
653    #[test_case(
654        SnapProtocolMessage::ByteCodes(ByteCodesMessage { request_id: 6, codes: vec![] }),
655        6, true ; "byte_codes is a response"
656    )]
657    #[test_case(
658        SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
659            request_id: 7, block_hashes: vec![], response_bytes: 0,
660        }), 7, false ; "get_block_access_lists is a request"
661    )]
662    #[test_case(
663        SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
664            request_id: 8, block_access_lists: BlockAccessLists(vec![]),
665        }), 8, true ; "block_access_lists is a response"
666    )]
667    fn request_id_and_is_response(msg: SnapProtocolMessage, expected_id: u64, is_response: bool) {
668        assert_eq!(msg.request_id(), expected_id);
669        assert_eq!(msg.is_response(), is_response);
670    }
671
672    #[test_case(
673        SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
674            request_id: 1, root_hash: B256::ZERO, starting_hash: B256::ZERO,
675            limit_hash: B256::ZERO, response_bytes: 0,
676        }) ; "get_account_range"
677    )]
678    #[test_case(
679        SnapProtocolMessage::AccountRange(AccountRangeMessage {
680            request_id: 1, accounts: vec![], proof: vec![],
681        }) ; "account_range"
682    )]
683    #[test_case(
684        SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
685            request_id: 1, root_hash: B256::ZERO, account_hashes: vec![],
686            starting_hash: B256::ZERO.into(), limit_hash: B256::ZERO.into(), response_bytes: 0,
687        }) ; "get_storage_ranges"
688    )]
689    #[test_case(
690        SnapProtocolMessage::StorageRanges(StorageRangesMessage {
691            request_id: 1, slots: vec![], proof: vec![],
692        }) ; "storage_ranges"
693    )]
694    #[test_case(
695        SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
696            request_id: 1, hashes: vec![], response_bytes: 0,
697        }) ; "get_byte_codes"
698    )]
699    #[test_case(
700        SnapProtocolMessage::ByteCodes(ByteCodesMessage { request_id: 1, codes: vec![] }) ;
701        "byte_codes"
702    )]
703    #[test_case(
704        SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
705            request_id: 1, block_hashes: vec![], response_bytes: 0,
706        }) ; "get_block_access_lists"
707    )]
708    #[test_case(
709        SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
710            request_id: 1, block_access_lists: BlockAccessLists(vec![]),
711        }) ; "block_access_lists"
712    )]
713    fn per_variant_request_id_and_round_trip(mut msg: SnapProtocolMessage) {
714        // set_request_id overwrites the id for every variant.
715        msg.set_request_id(42);
716        assert_eq!(msg.request_id(), 42);
717
718        // decode_versioned round-trips every valid snap/2 id.
719        let decoded =
720            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &msg.encode()).unwrap();
721        assert_eq!(decoded, msg);
722    }
723
724    #[test]
725    fn decode_versioned_rejects_empty() {
726        // An empty payload carries no message id and is distinct from an invalid id.
727        assert!(matches!(
728            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[]),
729            Err(SnapProtocolError::Empty)
730        ));
731    }
732
733    #[test]
734    fn decode_versioned_rejects_trie_node_ids_in_v2() {
735        // snap/2 (EIP-8189) removes trie nodes (`0x06`/`0x07`); decoding must reject them as an
736        // unsupported id rather than a malformed body.
737        for id in [0x06u8, 0x07] {
738            assert!(matches!(
739                SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[id]),
740                Err(SnapProtocolError::UnsupportedMessageId(got, SnapVersion::V2)) if got == id
741            ));
742        }
743    }
744
745    #[test]
746    fn decode_versioned_reports_malformed_body() {
747        // A valid id (GetBlockAccessLists, 0x08) with a non-decodable body is an RLP error, not an
748        // unsupported id.
749        assert!(matches!(
750            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[0x08, 0xff]),
751            Err(SnapProtocolError::Rlp(_))
752        ));
753    }
754
755    #[test]
756    fn decode_versioned_rejects_trailing_bytes() {
757        // A valid framed message with junk appended after the RLP body must be rejected rather
758        // than silently decoded.
759        let original = SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
760            request_id: 7,
761            block_hashes: vec![b256_from_u64(1)],
762            response_bytes: 1024,
763        });
764        let mut framed = original.encode().to_vec();
765        framed.push(0xff);
766        assert!(matches!(
767            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &framed),
768            Err(SnapProtocolError::Rlp(alloy_rlp::Error::UnexpectedLength))
769        ));
770    }
771
772    #[test]
773    fn get_storage_ranges_decodes_geths_empty_origin_and_limit() {
774        let body = alloy_rlp::encode(GethStorageRequest {
775            request_id: 21,
776            root_hash: B256::ZERO,
777            account_hashes: vec![B256::repeat_byte(1), B256::repeat_byte(2)],
778            origin: Bytes::new(),
779            limit: Bytes::new(),
780            response_bytes: 1024,
781        });
782        let mut framed = vec![SnapMessageId::GetStorageRanges as u8];
783        framed.extend_from_slice(&body);
784
785        let decoded = SnapProtocolMessage::decode_versioned(SnapVersion::V2, &framed).unwrap();
786        let SnapProtocolMessage::GetStorageRanges(msg) = decoded else {
787            panic!("expected a GetStorageRanges message");
788        };
789        assert_eq!(msg.starting_hash.unwrap_or(B256::ZERO), B256::ZERO);
790        assert_eq!(msg.limit_hash.unwrap_or(B256::repeat_byte(0xff)), B256::repeat_byte(0xff));
791    }
792}