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, KECCAK256_EMPTY, U256};
11use alloy_rlp::{BufMut, Decodable, Encodable, RlpDecodable, RlpEncodable};
12use alloy_trie::{TrieAccount, EMPTY_ROOT_HASH};
13use reth_codecs_derive::add_arbitrary_tests;
14
15/// Supported SNAP protocol versions.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[repr(u8)]
19pub enum SnapVersion {
20    /// BAL-based healing as proposed by EIP-8189.
21    #[default]
22    V2 = 2,
23}
24
25impl SnapVersion {
26    /// Returns the protocol message slot length for this version (not the count of valid ids; use
27    /// [`Self::supports_message_id`] to check validity).
28    pub const fn message_count(self) -> u8 {
29        match self {
30            Self::V2 => 10,
31        }
32    }
33
34    /// Returns `true` if `id` is a valid `snap/2` message id.
35    ///
36    /// snap/2 (EIP-8189) drops trie nodes (`0x06`/`0x07`) and adds BAL (`0x08`/`0x09`),
37    /// so validity is not a contiguous range.
38    pub const fn supports_message_id(self, id: u8) -> bool {
39        match self {
40            // snap/2: 0x00..=0x05 plus BAL (0x08/0x09). TrieNodes (0x06/0x07) removed.
41            Self::V2 => {
42                id <= SnapMessageId::ByteCodes as u8 ||
43                    id == SnapMessageId::GetBlockAccessLists as u8 ||
44                    id == SnapMessageId::BlockAccessLists as u8
45            }
46        }
47    }
48}
49
50/// Message IDs for the snap sync protocol
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum SnapMessageId {
53    /// Requests of an unknown number of accounts from a given account trie.
54    GetAccountRange = 0x00,
55    /// Response with the number of consecutive accounts and the Merkle proofs for the entire
56    /// range.
57    AccountRange = 0x01,
58    /// Requests for the storage slots of multiple accounts' storage tries.
59    GetStorageRanges = 0x02,
60    /// Response for the number of consecutive storage slots for the requested account.
61    StorageRanges = 0x03,
62    /// Request of the number of contract byte-codes by hash.
63    GetByteCodes = 0x04,
64    /// Response for the number of requested contract codes.
65    ByteCodes = 0x05,
66    /// Request BALs for a list of block hashes.
67    GetBlockAccessLists = 0x08,
68    /// Response containing BALs for the requested block hashes.
69    BlockAccessLists = 0x09,
70}
71
72impl SnapMessageId {
73    /// Returns the message id of the response paired with this request, or `None` if this id is
74    /// itself a response.
75    pub const fn response(self) -> Option<Self> {
76        match self {
77            Self::GetAccountRange => Some(Self::AccountRange),
78            Self::GetStorageRanges => Some(Self::StorageRanges),
79            Self::GetByteCodes => Some(Self::ByteCodes),
80            Self::GetBlockAccessLists => Some(Self::BlockAccessLists),
81            Self::AccountRange | Self::StorageRanges | Self::ByteCodes | Self::BlockAccessLists => {
82                None
83            }
84        }
85    }
86}
87
88/// Request for a range of accounts from the state trie.
89// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getaccountrange-0x00
90#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
91#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
92#[add_arbitrary_tests(rlp)]
93pub struct GetAccountRangeMessage {
94    /// Request ID to match up responses with
95    pub request_id: u64,
96    /// Root hash of the account trie to serve
97    pub root_hash: B256,
98    /// Account hash of the first to retrieve
99    pub starting_hash: B256,
100    /// Account hash after which to stop serving data
101    pub limit_hash: B256,
102    /// Soft limit at which to stop returning data
103    pub response_bytes: u64,
104}
105
106/// Account data in the response.
107#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
108#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
109#[add_arbitrary_tests(rlp)]
110pub struct AccountData {
111    /// Hash of the account address (trie path)
112    pub hash: B256,
113    /// Account body in slim format
114    pub body: Bytes,
115}
116
117impl AccountData {
118    /// Encodes `account` in snap/2's slim format.
119    pub fn from_trie_account(hash: B256, account: &TrieAccount) -> Self {
120        let body = alloy_rlp::encode(SlimAccountBodyRef {
121            nonce: account.nonce,
122            balance: account.balance,
123            storage_root: SlimAccountBodyRef::shorten(&account.storage_root, EMPTY_ROOT_HASH),
124            code_hash: SlimAccountBodyRef::shorten(&account.code_hash, KECCAK256_EMPTY),
125        });
126        Self { hash, body: body.into() }
127    }
128
129    /// Decodes the slim body into the account the trie leaf commits to.
130    ///
131    /// Range proofs are verified against the full encoding, so the omitted storage root and code
132    /// hash are restored to their defaults here.
133    pub fn trie_account(&self) -> alloy_rlp::Result<TrieAccount> {
134        let slim = alloy_rlp::decode_exact::<SlimAccountBody>(&self.body)?;
135
136        Ok(TrieAccount {
137            nonce: slim.nonce,
138            balance: slim.balance,
139            storage_root: SlimAccountBody::restore(&slim.storage_root, EMPTY_ROOT_HASH)?,
140            code_hash: SlimAccountBody::restore(&slim.code_hash, KECCAK256_EMPTY)?,
141        })
142    }
143}
144
145/// Response containing a number of consecutive accounts and the Merkle proofs for the entire range.
146// http://github.com/ethereum/devp2p/blob/master/caps/snap.md#accountrange-0x01
147#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
148#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
149#[add_arbitrary_tests(rlp)]
150pub struct AccountRangeMessage {
151    /// ID of the request this is a response for
152    pub request_id: u64,
153    /// List of consecutive accounts from the trie
154    pub accounts: Vec<AccountData>,
155    /// List of trie nodes proving the account range
156    pub proof: Vec<Bytes>,
157}
158
159/// Request for the storage slots of multiple accounts' storage tries.
160// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getstorageranges-0x02
161#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
162#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
163#[add_arbitrary_tests(rlp)]
164pub struct GetStorageRangesMessage {
165    /// Request ID to match up responses with
166    pub request_id: u64,
167    /// Root hash of the account trie to serve
168    pub root_hash: B256,
169    /// Account hashes of the storage tries to serve
170    pub account_hashes: Vec<B256>,
171    /// Storage slot hash of the first to retrieve; unbounded (served as `B256::ZERO`) when the
172    /// wire encoding is an empty byte string.
173    pub starting_hash: RangeBound,
174    /// Storage slot hash after which to stop serving; unbounded (served as
175    /// `B256::repeat_byte(0xff)`) when the wire encoding is an empty byte string.
176    pub limit_hash: RangeBound,
177    /// Soft limit at which to stop returning data
178    pub response_bytes: u64,
179}
180
181/// A `snap/2` storage-range bound (`origin`/`limit` on [`GetStorageRangesMessage`]).
182///
183/// Encoded as either an empty byte string (unbounded) or a 32-byte hash.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
185#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
186pub struct RangeBound(Option<B256>);
187
188impl RangeBound {
189    /// Returns the bound's hash, or `default` if it was encoded as an empty byte string.
190    pub const fn unwrap_or(self, default: B256) -> B256 {
191        match self.0 {
192            Some(hash) => hash,
193            None => default,
194        }
195    }
196}
197
198impl From<B256> for RangeBound {
199    fn from(hash: B256) -> Self {
200        Self(Some(hash))
201    }
202}
203
204impl Encodable for RangeBound {
205    fn encode(&self, out: &mut dyn BufMut) {
206        match self.0 {
207            Some(hash) => hash.encode(out),
208            None => Bytes::new().encode(out),
209        }
210    }
211
212    fn length(&self) -> usize {
213        match self.0 {
214            Some(hash) => hash.length(),
215            None => Bytes::new().length(),
216        }
217    }
218}
219
220impl Decodable for RangeBound {
221    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
222        let bytes = Bytes::decode(buf)?;
223        match bytes.len() {
224            0 => Ok(Self(None)),
225            32 => Ok(Self(Some(B256::from_slice(&bytes)))),
226            _ => Err(alloy_rlp::Error::UnexpectedLength),
227        }
228    }
229}
230
231/// Storage slot data in the response.
232#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
233#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
234#[add_arbitrary_tests(rlp)]
235pub struct StorageData {
236    /// Hash of the storage slot key (trie path)
237    pub hash: B256,
238    /// Data content of the slot
239    pub data: Bytes,
240}
241
242impl StorageData {
243    /// Encodes a slot value as the storage trie leaf commits to it.
244    pub fn from_value(hash: B256, value: U256) -> Self {
245        Self { hash, data: alloy_rlp::encode(value).into() }
246    }
247
248    /// Decodes the slot value.
249    pub fn value(&self) -> alloy_rlp::Result<U256> {
250        alloy_rlp::decode_exact(&self.data)
251    }
252}
253
254/// Response containing a number of consecutive storage slots for the requested account
255/// and optionally the merkle proofs for the last range (boundary proofs) if it only partially
256/// covers the storage trie.
257// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#storageranges-0x03
258#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
259#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
260#[add_arbitrary_tests(rlp)]
261pub struct StorageRangesMessage {
262    /// ID of the request this is a response for
263    pub request_id: u64,
264    /// List of list of consecutive slots from the trie (one list per account)
265    pub slots: Vec<Vec<StorageData>>,
266    /// List of trie nodes proving the slot range (if partial)
267    pub proof: Vec<Bytes>,
268}
269
270/// Request to get a number of requested contract codes.
271// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getbytecodes-0x04
272#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
273#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
274#[add_arbitrary_tests(rlp)]
275pub struct GetByteCodesMessage {
276    /// Request ID to match up responses with
277    pub request_id: u64,
278    /// Code hashes to retrieve the code for
279    pub hashes: Vec<B256>,
280    /// Soft limit at which to stop returning data (in bytes)
281    pub response_bytes: u64,
282}
283
284/// Response containing a number of requested contract codes.
285// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#bytecodes-0x05
286#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
287#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
288#[add_arbitrary_tests(rlp)]
289pub struct ByteCodesMessage {
290    /// ID of the request this is a response for
291    pub request_id: u64,
292    /// The requested bytecodes in order
293    pub codes: Vec<Bytes>,
294}
295
296/// Request BALs for the given block hashes.
297#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
298#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
299#[add_arbitrary_tests(rlp)]
300pub struct GetBlockAccessListsMessage {
301    /// Request ID to match up responses with.
302    pub request_id: u64,
303    /// Block hashes to retrieve BALs for.
304    pub block_hashes: Vec<B256>,
305    /// Soft limit at which to stop returning data (in bytes).
306    pub response_bytes: u64,
307}
308
309/// Response containing one BAL per requested block hash.
310#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
311#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
312#[add_arbitrary_tests(rlp)]
313pub struct BlockAccessListsMessage {
314    /// ID of the request this is a response for.
315    pub request_id: u64,
316    /// Raw BAL payloads in request order.
317    pub block_access_lists: BlockAccessLists,
318}
319
320/// Represents all types of messages in the snap sync protocol.
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub enum SnapProtocolMessage {
323    /// Request for an account range - see [`GetAccountRangeMessage`]
324    GetAccountRange(GetAccountRangeMessage),
325    /// Response with accounts and proofs - see [`AccountRangeMessage`]
326    AccountRange(AccountRangeMessage),
327    /// Request for storage slots - see [`GetStorageRangesMessage`]
328    GetStorageRanges(GetStorageRangesMessage),
329    /// Response with storage slots - see [`StorageRangesMessage`]
330    StorageRanges(StorageRangesMessage),
331    /// Request for contract bytecodes - see [`GetByteCodesMessage`]
332    GetByteCodes(GetByteCodesMessage),
333    /// Response with contract codes - see [`ByteCodesMessage`]
334    ByteCodes(ByteCodesMessage),
335    /// Request for block access lists - see [`GetBlockAccessListsMessage`]
336    GetBlockAccessLists(GetBlockAccessListsMessage),
337    /// Response with block access lists - see [`BlockAccessListsMessage`]
338    BlockAccessLists(BlockAccessListsMessage),
339}
340
341/// Error decoding an inbound `snap` protocol message from its framed bytes.
342#[derive(thiserror::Error, Debug)]
343pub enum SnapProtocolError {
344    /// The payload was empty and carried no message id.
345    #[error("empty snap message")]
346    Empty,
347    /// The message id is not valid for the negotiated snap version (e.g. the removed trie-node
348    /// messages `0x06`/`0x07` under snap/2).
349    #[error("message id {0:#x} is invalid for snap/{1:?}")]
350    UnsupportedMessageId(u8, SnapVersion),
351    /// Decoding the RLP message body failed.
352    #[error("RLP error: {0}")]
353    Rlp(#[from] alloy_rlp::Error),
354}
355
356impl SnapProtocolMessage {
357    /// Returns the protocol message ID for this message type.
358    ///
359    /// The message ID is used in the `RLPx` protocol to identify different types of messages.
360    pub const fn message_id(&self) -> SnapMessageId {
361        match self {
362            Self::GetAccountRange(_) => SnapMessageId::GetAccountRange,
363            Self::AccountRange(_) => SnapMessageId::AccountRange,
364            Self::GetStorageRanges(_) => SnapMessageId::GetStorageRanges,
365            Self::StorageRanges(_) => SnapMessageId::StorageRanges,
366            Self::GetByteCodes(_) => SnapMessageId::GetByteCodes,
367            Self::ByteCodes(_) => SnapMessageId::ByteCodes,
368            Self::GetBlockAccessLists(_) => SnapMessageId::GetBlockAccessLists,
369            Self::BlockAccessLists(_) => SnapMessageId::BlockAccessLists,
370        }
371    }
372
373    /// Returns the `request_id` used to correlate this message with its request/response pair.
374    pub const fn request_id(&self) -> u64 {
375        match self {
376            Self::GetAccountRange(m) => m.request_id,
377            Self::AccountRange(m) => m.request_id,
378            Self::GetStorageRanges(m) => m.request_id,
379            Self::StorageRanges(m) => m.request_id,
380            Self::GetByteCodes(m) => m.request_id,
381            Self::ByteCodes(m) => m.request_id,
382            Self::GetBlockAccessLists(m) => m.request_id,
383            Self::BlockAccessLists(m) => m.request_id,
384        }
385    }
386
387    /// Returns `true` if this is a response message (as opposed to a request).
388    pub const fn is_response(&self) -> bool {
389        matches!(
390            self,
391            Self::AccountRange(_) |
392                Self::StorageRanges(_) |
393                Self::ByteCodes(_) |
394                Self::BlockAccessLists(_)
395        )
396    }
397
398    /// Overwrites the `request_id`, e.g. so a session can assign a connection-unique id before
399    /// sending a request.
400    pub const fn set_request_id(&mut self, request_id: u64) {
401        match self {
402            Self::GetAccountRange(m) => m.request_id = request_id,
403            Self::AccountRange(m) => m.request_id = request_id,
404            Self::GetStorageRanges(m) => m.request_id = request_id,
405            Self::StorageRanges(m) => m.request_id = request_id,
406            Self::GetByteCodes(m) => m.request_id = request_id,
407            Self::ByteCodes(m) => m.request_id = request_id,
408            Self::GetBlockAccessLists(m) => m.request_id = request_id,
409            Self::BlockAccessLists(m) => m.request_id = request_id,
410        }
411    }
412
413    /// Encode the message to bytes
414    pub fn encode(&self) -> Bytes {
415        let mut buf = Vec::new();
416        // Add message ID as first byte
417        buf.push(self.message_id() as u8);
418
419        // Encode the message body based on its type
420        match self {
421            Self::GetAccountRange(msg) => msg.encode(&mut buf),
422            Self::AccountRange(msg) => msg.encode(&mut buf),
423            Self::GetStorageRanges(msg) => msg.encode(&mut buf),
424            Self::StorageRanges(msg) => msg.encode(&mut buf),
425            Self::GetByteCodes(msg) => msg.encode(&mut buf),
426            Self::ByteCodes(msg) => msg.encode(&mut buf),
427            Self::GetBlockAccessLists(msg) => msg.encode(&mut buf),
428            Self::BlockAccessLists(msg) => msg.encode(&mut buf),
429        }
430
431        Bytes::from(buf)
432    }
433
434    /// Decodes a SNAP protocol message from its message ID and RLP-encoded body.
435    pub fn decode(message_id: u8, buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
436        // Decoding protocol message variants based on message ID
437        macro_rules! decode_snap_message_variant {
438            ($message_id:expr, $buf:expr, $id:expr, $variant:ident, $msg_type:ty) => {
439                if $message_id == $id as u8 {
440                    return Ok(Self::$variant(<$msg_type>::decode($buf)?));
441                }
442            };
443        }
444
445        // Try to decode each message type based on the message ID
446        decode_snap_message_variant!(
447            message_id,
448            buf,
449            SnapMessageId::GetAccountRange,
450            GetAccountRange,
451            GetAccountRangeMessage
452        );
453        decode_snap_message_variant!(
454            message_id,
455            buf,
456            SnapMessageId::AccountRange,
457            AccountRange,
458            AccountRangeMessage
459        );
460        decode_snap_message_variant!(
461            message_id,
462            buf,
463            SnapMessageId::GetStorageRanges,
464            GetStorageRanges,
465            GetStorageRangesMessage
466        );
467        decode_snap_message_variant!(
468            message_id,
469            buf,
470            SnapMessageId::StorageRanges,
471            StorageRanges,
472            StorageRangesMessage
473        );
474        decode_snap_message_variant!(
475            message_id,
476            buf,
477            SnapMessageId::GetByteCodes,
478            GetByteCodes,
479            GetByteCodesMessage
480        );
481        decode_snap_message_variant!(
482            message_id,
483            buf,
484            SnapMessageId::ByteCodes,
485            ByteCodes,
486            ByteCodesMessage
487        );
488        decode_snap_message_variant!(
489            message_id,
490            buf,
491            SnapMessageId::GetBlockAccessLists,
492            GetBlockAccessLists,
493            GetBlockAccessListsMessage
494        );
495        decode_snap_message_variant!(
496            message_id,
497            buf,
498            SnapMessageId::BlockAccessLists,
499            BlockAccessLists,
500            BlockAccessListsMessage
501        );
502
503        Err(alloy_rlp::Error::Custom("Unknown message ID"))
504    }
505
506    /// Decodes a framed snap message (`[id, body..]`), validating the id against `version`.
507    ///
508    /// Empty payload, invalid id, and malformed body are reported as distinct
509    /// [`SnapProtocolError`] variants.
510    pub fn decode_versioned(version: SnapVersion, bytes: &[u8]) -> Result<Self, SnapProtocolError> {
511        let (&id, mut body) = bytes.split_first().ok_or(SnapProtocolError::Empty)?;
512        if !version.supports_message_id(id) {
513            return Err(SnapProtocolError::UnsupportedMessageId(id, version));
514        }
515        let msg = Self::decode(id, &mut body)?;
516        if !body.is_empty() {
517            return Err(SnapProtocolError::Rlp(alloy_rlp::Error::UnexpectedLength));
518        }
519        Ok(msg)
520    }
521}
522
523/// Like a trie account, with empty code and storage hashes omitted to reduce transfer size.
524#[derive(RlpDecodable)]
525struct SlimAccountBody {
526    /// The account's nonce.
527    nonce: u64,
528    /// The account's balance.
529    balance: U256,
530    /// Empty when the account has no storage.
531    storage_root: Bytes,
532    /// Empty when the account has no code.
533    code_hash: Bytes,
534}
535
536impl SlimAccountBody {
537    /// Restores a dropped field to `empty`, rejecting any length the encoding never produces.
538    fn restore(value: &[u8], empty: B256) -> alloy_rlp::Result<B256> {
539        match value {
540            [] => Ok(empty),
541            _ => B256::try_from(value).map_err(|_| alloy_rlp::Error::UnexpectedLength),
542        }
543    }
544}
545
546/// Borrowed encode twin of [`SlimAccountBody`].
547#[derive(RlpEncodable)]
548struct SlimAccountBodyRef<'a> {
549    /// The account's nonce.
550    nonce: u64,
551    /// The account's balance.
552    balance: U256,
553    /// Empty when the account has no storage.
554    storage_root: &'a [u8],
555    /// Empty when the account has no code.
556    code_hash: &'a [u8],
557}
558
559impl<'a> SlimAccountBodyRef<'a> {
560    /// Drops a field that holds its empty default, which is what makes the encoding slim.
561    fn shorten(value: &'a B256, empty: B256) -> &'a [u8] {
562        if *value == empty {
563            &[]
564        } else {
565            value.as_slice()
566        }
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use test_case::test_case;
574
575    // Helper function to create a B256 from a u64 for testing
576    fn b256_from_u64(value: u64) -> B256 {
577        B256::left_padding_from(&value.to_be_bytes())
578    }
579
580    // Helper function to test roundtrip encoding/decoding
581    fn test_roundtrip(original: SnapProtocolMessage) {
582        let encoded = original.encode();
583
584        // Verify the first byte matches the expected message ID
585        assert_eq!(encoded[0], original.message_id() as u8);
586
587        let mut buf = &encoded[1..];
588        let decoded = SnapProtocolMessage::decode(encoded[0], &mut buf).unwrap();
589
590        // Verify the match
591        assert_eq!(decoded, original);
592    }
593
594    // geth's GetStorageRangesPacket types Origin/Limit as raw byte strings and sends them
595    // empty for the common unbounded multi-account request, rather than as 32-byte
596    // zero/max-value hashes. A conforming decoder must accept this raw packet shape.
597    #[derive(alloy_rlp::RlpEncodable)]
598    struct GethStorageRequest {
599        request_id: u64,
600        root_hash: B256,
601        account_hashes: Vec<B256>,
602        origin: Bytes,
603        limit: Bytes,
604        response_bytes: u64,
605    }
606
607    #[test]
608    fn test_all_message_roundtrips() {
609        assert_eq!(SnapVersion::V2.message_count(), 10);
610
611        test_roundtrip(SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
612            request_id: 42,
613            root_hash: b256_from_u64(123),
614            starting_hash: b256_from_u64(456),
615            limit_hash: b256_from_u64(789),
616            response_bytes: 1024,
617        }));
618
619        test_roundtrip(SnapProtocolMessage::AccountRange(AccountRangeMessage {
620            request_id: 42,
621            accounts: vec![AccountData {
622                hash: b256_from_u64(123),
623                body: Bytes::from(vec![1, 2, 3]),
624            }],
625            proof: vec![Bytes::from(vec![4, 5, 6])],
626        }));
627
628        test_roundtrip(SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
629            request_id: 42,
630            root_hash: b256_from_u64(123),
631            account_hashes: vec![b256_from_u64(456)],
632            starting_hash: b256_from_u64(789).into(),
633            limit_hash: b256_from_u64(101112).into(),
634            response_bytes: 2048,
635        }));
636
637        // Geth's empty-byte-string encoding for an unbounded storage range.
638        test_roundtrip(SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
639            request_id: 43,
640            root_hash: b256_from_u64(123),
641            account_hashes: vec![b256_from_u64(456), b256_from_u64(789)],
642            starting_hash: RangeBound::default(),
643            limit_hash: RangeBound::default(),
644            response_bytes: 2048,
645        }));
646
647        test_roundtrip(SnapProtocolMessage::StorageRanges(StorageRangesMessage {
648            request_id: 42,
649            slots: vec![vec![StorageData {
650                hash: b256_from_u64(123),
651                data: Bytes::from(vec![1, 2, 3]),
652            }]],
653            proof: vec![Bytes::from(vec![4, 5, 6])],
654        }));
655
656        test_roundtrip(SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
657            request_id: 42,
658            hashes: vec![b256_from_u64(123)],
659            response_bytes: 1024,
660        }));
661
662        test_roundtrip(SnapProtocolMessage::ByteCodes(ByteCodesMessage {
663            request_id: 42,
664            codes: vec![Bytes::from(vec![1, 2, 3])],
665        }));
666
667        test_roundtrip(SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
668            request_id: 42,
669            block_hashes: vec![b256_from_u64(123), b256_from_u64(456)],
670            response_bytes: 4096,
671        }));
672
673        test_roundtrip(SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
674            request_id: 42,
675            block_access_lists: BlockAccessLists(vec![
676                Some(Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE])),
677                Some(Bytes::from_static(&[0xc1, alloy_rlp::EMPTY_LIST_CODE])),
678            ]),
679        }));
680    }
681
682    #[test]
683    fn test_unknown_message_id() {
684        // Create some random data
685        let data = Bytes::from(vec![1, 2, 3, 4]);
686        let mut buf = data.as_ref();
687
688        // Try to decode with an invalid message ID
689        let result = SnapProtocolMessage::decode(255, &mut buf);
690
691        assert!(result.is_err());
692        if let Err(e) = result {
693            assert_eq!(e.to_string(), "Unknown message ID");
694        }
695    }
696
697    #[test]
698    fn test_snap_v2_message_validity() {
699        let v2 = SnapVersion::V2;
700        // 0x00..=0x05 valid.
701        for id in 0x00..=0x05 {
702            assert!(v2.supports_message_id(id), "snap/2 should accept {id:#x}");
703        }
704        // Trie nodes (0x06/0x07) are removed in snap/2.
705        assert!(!v2.supports_message_id(0x06));
706        assert!(!v2.supports_message_id(0x07));
707        // BAL added in snap/2.
708        assert!(v2.supports_message_id(SnapMessageId::GetBlockAccessLists as u8));
709        assert!(v2.supports_message_id(SnapMessageId::BlockAccessLists as u8));
710        assert!(!v2.supports_message_id(0x0a));
711        assert!(!v2.supports_message_id(0xff));
712    }
713
714    #[test_case(
715        SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
716            request_id: 1, root_hash: B256::ZERO, starting_hash: B256::ZERO,
717            limit_hash: B256::ZERO, response_bytes: 0,
718        }), 1, false ; "get_account_range is a request"
719    )]
720    #[test_case(
721        SnapProtocolMessage::AccountRange(AccountRangeMessage {
722            request_id: 2, accounts: vec![], proof: vec![],
723        }), 2, true ; "account_range is a response"
724    )]
725    #[test_case(
726        SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
727            request_id: 3, root_hash: B256::ZERO, account_hashes: vec![],
728            starting_hash: B256::ZERO.into(), limit_hash: B256::ZERO.into(), response_bytes: 0,
729        }), 3, false ; "get_storage_ranges is a request"
730    )]
731    #[test_case(
732        SnapProtocolMessage::StorageRanges(StorageRangesMessage {
733            request_id: 4, slots: vec![], proof: vec![],
734        }), 4, true ; "storage_ranges is a response"
735    )]
736    #[test_case(
737        SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
738            request_id: 5, hashes: vec![], response_bytes: 0,
739        }), 5, false ; "get_byte_codes is a request"
740    )]
741    #[test_case(
742        SnapProtocolMessage::ByteCodes(ByteCodesMessage { request_id: 6, codes: vec![] }),
743        6, true ; "byte_codes is a response"
744    )]
745    #[test_case(
746        SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
747            request_id: 7, block_hashes: vec![], response_bytes: 0,
748        }), 7, false ; "get_block_access_lists is a request"
749    )]
750    #[test_case(
751        SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
752            request_id: 8, block_access_lists: BlockAccessLists(vec![]),
753        }), 8, true ; "block_access_lists is a response"
754    )]
755    fn request_id_and_is_response(msg: SnapProtocolMessage, expected_id: u64, is_response: bool) {
756        assert_eq!(msg.request_id(), expected_id);
757        assert_eq!(msg.is_response(), is_response);
758    }
759
760    #[test_case(
761        SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
762            request_id: 1, root_hash: B256::ZERO, starting_hash: B256::ZERO,
763            limit_hash: B256::ZERO, response_bytes: 0,
764        }) ; "get_account_range"
765    )]
766    #[test_case(
767        SnapProtocolMessage::AccountRange(AccountRangeMessage {
768            request_id: 1, accounts: vec![], proof: vec![],
769        }) ; "account_range"
770    )]
771    #[test_case(
772        SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
773            request_id: 1, root_hash: B256::ZERO, account_hashes: vec![],
774            starting_hash: B256::ZERO.into(), limit_hash: B256::ZERO.into(), response_bytes: 0,
775        }) ; "get_storage_ranges"
776    )]
777    #[test_case(
778        SnapProtocolMessage::StorageRanges(StorageRangesMessage {
779            request_id: 1, slots: vec![], proof: vec![],
780        }) ; "storage_ranges"
781    )]
782    #[test_case(
783        SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
784            request_id: 1, hashes: vec![], response_bytes: 0,
785        }) ; "get_byte_codes"
786    )]
787    #[test_case(
788        SnapProtocolMessage::ByteCodes(ByteCodesMessage { request_id: 1, codes: vec![] }) ;
789        "byte_codes"
790    )]
791    #[test_case(
792        SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
793            request_id: 1, block_hashes: vec![], response_bytes: 0,
794        }) ; "get_block_access_lists"
795    )]
796    #[test_case(
797        SnapProtocolMessage::BlockAccessLists(BlockAccessListsMessage {
798            request_id: 1, block_access_lists: BlockAccessLists(vec![]),
799        }) ; "block_access_lists"
800    )]
801    fn per_variant_request_id_and_round_trip(mut msg: SnapProtocolMessage) {
802        // set_request_id overwrites the id for every variant.
803        msg.set_request_id(42);
804        assert_eq!(msg.request_id(), 42);
805
806        // decode_versioned round-trips every valid snap/2 id.
807        let decoded =
808            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &msg.encode()).unwrap();
809        assert_eq!(decoded, msg);
810    }
811
812    #[test]
813    fn decode_versioned_rejects_empty() {
814        // An empty payload carries no message id and is distinct from an invalid id.
815        assert!(matches!(
816            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[]),
817            Err(SnapProtocolError::Empty)
818        ));
819    }
820
821    #[test]
822    fn decode_versioned_rejects_trie_node_ids_in_v2() {
823        // snap/2 (EIP-8189) removes trie nodes (`0x06`/`0x07`); decoding must reject them as an
824        // unsupported id rather than a malformed body.
825        for id in [0x06u8, 0x07] {
826            assert!(matches!(
827                SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[id]),
828                Err(SnapProtocolError::UnsupportedMessageId(got, SnapVersion::V2)) if got == id
829            ));
830        }
831    }
832
833    #[test]
834    fn decode_versioned_reports_malformed_body() {
835        // A valid id (GetBlockAccessLists, 0x08) with a non-decodable body is an RLP error, not an
836        // unsupported id.
837        assert!(matches!(
838            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &[0x08, 0xff]),
839            Err(SnapProtocolError::Rlp(_))
840        ));
841    }
842
843    #[test]
844    fn decode_versioned_rejects_trailing_bytes() {
845        // A valid framed message with junk appended after the RLP body must be rejected rather
846        // than silently decoded.
847        let original = SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
848            request_id: 7,
849            block_hashes: vec![b256_from_u64(1)],
850            response_bytes: 1024,
851        });
852        let mut framed = original.encode().to_vec();
853        framed.push(0xff);
854        assert!(matches!(
855            SnapProtocolMessage::decode_versioned(SnapVersion::V2, &framed),
856            Err(SnapProtocolError::Rlp(alloy_rlp::Error::UnexpectedLength))
857        ));
858    }
859
860    #[test]
861    fn get_storage_ranges_decodes_geths_empty_origin_and_limit() {
862        let body = alloy_rlp::encode(GethStorageRequest {
863            request_id: 21,
864            root_hash: B256::ZERO,
865            account_hashes: vec![B256::repeat_byte(1), B256::repeat_byte(2)],
866            origin: Bytes::new(),
867            limit: Bytes::new(),
868            response_bytes: 1024,
869        });
870        let mut framed = vec![SnapMessageId::GetStorageRanges as u8];
871        framed.extend_from_slice(&body);
872
873        let decoded = SnapProtocolMessage::decode_versioned(SnapVersion::V2, &framed).unwrap();
874        let SnapProtocolMessage::GetStorageRanges(msg) = decoded else {
875            panic!("expected a GetStorageRanges message");
876        };
877        assert_eq!(msg.starting_hash.unwrap_or(B256::ZERO), B256::ZERO);
878        assert_eq!(msg.limit_hash.unwrap_or(B256::repeat_byte(0xff)), B256::repeat_byte(0xff));
879    }
880
881    fn trie_account(storage_root: B256, code_hash: B256) -> TrieAccount {
882        TrieAccount { nonce: 7, balance: U256::from(42), storage_root, code_hash }
883    }
884
885    #[test]
886    fn slim_body_elides_empty_storage_and_code() {
887        let account = trie_account(EMPTY_ROOT_HASH, KECCAK256_EMPTY);
888        let encoded = AccountData::from_trie_account(B256::repeat_byte(1), &account);
889
890        let body = SlimAccountBody::decode(&mut encoded.body.as_ref()).unwrap();
891        assert!(body.storage_root.is_empty());
892        assert!(body.code_hash.is_empty());
893        assert_eq!(encoded.trie_account().unwrap(), account);
894    }
895
896    #[test]
897    fn slim_body_keeps_non_default_storage_and_code() {
898        let account = trie_account(B256::repeat_byte(2), B256::repeat_byte(3));
899        let encoded = AccountData::from_trie_account(B256::repeat_byte(1), &account);
900
901        let body = SlimAccountBody::decode(&mut encoded.body.as_ref()).unwrap();
902        assert_eq!(body.storage_root.len(), 32);
903        assert_eq!(body.code_hash.len(), 32);
904        assert_eq!(encoded.trie_account().unwrap(), account);
905    }
906
907    #[test]
908    fn slim_body_rejects_field_lengths_the_encoding_never_produces() {
909        // A 16-byte field is neither an elided default nor a hash, so accepting it would let a
910        // peer smuggle a value that hashes differently than the one it claims to serve.
911        let truncated = Bytes::from_static(&[0xaa; 16]);
912
913        assert!(SlimAccountBody::restore(&truncated, EMPTY_ROOT_HASH).is_err());
914    }
915
916    #[test]
917    fn slim_body_rejects_trailing_bytes() {
918        let account = trie_account(EMPTY_ROOT_HASH, KECCAK256_EMPTY);
919        let mut encoded = AccountData::from_trie_account(B256::repeat_byte(1), &account);
920        encoded.body = [encoded.body.as_ref(), &[0x00]].concat().into();
921
922        assert!(encoded.trie_account().is_err());
923    }
924
925    #[test]
926    fn storage_data_carries_the_trie_leaf_encoding() {
927        let value = U256::from(1234);
928        let slot = StorageData::from_value(B256::repeat_byte(4), value);
929
930        // Clients verify range proofs against the RLP-encoded trie leaf, so the wire bytes must be
931        // exactly that rather than a fixed-width word.
932        assert_eq!(slot.data.as_ref(), alloy_rlp::encode(value));
933        assert_eq!(slot.value().unwrap(), value);
934    }
935
936    #[test]
937    fn storage_data_rejects_trailing_bytes() {
938        let mut slot = StorageData::from_value(B256::repeat_byte(4), U256::from(1));
939        slot.data = [slot.data.as_ref(), &[0x00]].concat().into();
940
941        assert!(slot.value().is_err());
942    }
943}