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