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