Skip to main content

reth_eth_wire_types/
block_access_lists.rs

1//! Implements the `GetBlockAccessLists` and `BlockAccessLists` message types.
2
3use alloc::vec::Vec;
4use alloy_primitives::{Bytes, B256};
5use alloy_rlp::{
6    BufMut, Decodable, Encodable, Header, RlpDecodableWrapper, RlpEncodableWrapper,
7    EMPTY_STRING_CODE,
8};
9use reth_codecs_derive::add_arbitrary_tests;
10
11/// A request for block access lists from the given block hashes.
12#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Default)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
15#[add_arbitrary_tests(rlp)]
16pub struct GetBlockAccessLists(
17    /// The block hashes to request block access lists for.
18    pub Vec<B256>,
19);
20
21/// Response for [`GetBlockAccessLists`] containing one BAL entry per requested block hash.
22///
23/// Present `Bytes` values store raw BAL RLP payloads and are encoded as nested RLP items, not as
24/// RLP byte strings.
25#[derive(Clone, Debug, PartialEq, Eq, Default)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27#[add_arbitrary_tests(rlp)]
28pub struct BlockAccessLists(
29    /// The requested block access lists as raw RLP blobs. Per EIP-8159, unavailable entries are
30    /// represented by `None` and encoded as the RLP empty string (`0x80`).
31    pub Vec<Option<Bytes>>,
32);
33
34impl Encodable for BlockAccessLists {
35    fn encode(&self, out: &mut dyn BufMut) {
36        let payload_length =
37            self.0.iter().map(|entry| entry.as_ref().map_or(1, |bytes| bytes.len())).sum();
38        Header { list: true, payload_length }.encode(out);
39        for entry in &self.0 {
40            match entry {
41                Some(bal) => out.put_slice(bal),
42                None => out.put_u8(EMPTY_STRING_CODE),
43            }
44        }
45    }
46
47    fn length(&self) -> usize {
48        let payload_length =
49            self.0.iter().map(|entry| entry.as_ref().map_or(1, |bytes| bytes.len())).sum();
50        Header { list: true, payload_length }.length_with_payload()
51    }
52}
53
54impl Decodable for BlockAccessLists {
55    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
56        let header = Header::decode(buf)?;
57        if !header.list {
58            return Err(alloy_rlp::Error::UnexpectedString)
59        }
60        if buf.len() < header.payload_length {
61            return Err(alloy_rlp::Error::InputTooShort)
62        }
63
64        let (mut payload, rest) = buf.split_at(header.payload_length);
65        *buf = rest;
66        let mut bals = Vec::new();
67
68        while !payload.is_empty() {
69            let item_start = payload;
70            let item_header = Header::decode(&mut payload)?;
71            let header_length = item_start.len() - payload.len();
72            let item_length = header_length + item_header.payload_length;
73            if item_length > item_start.len() {
74                return Err(alloy_rlp::Error::InputTooShort)
75            }
76            if item_header.list {
77                bals.push(Some(Bytes::copy_from_slice(&item_start[..item_length])));
78            } else if item_start[..item_length] == [EMPTY_STRING_CODE] {
79                bals.push(None);
80            } else {
81                return Err(alloy_rlp::Error::UnexpectedString)
82            }
83
84            payload = &payload[item_header.payload_length..];
85        }
86
87        Ok(Self(bals))
88    }
89}
90
91#[cfg(any(test, feature = "arbitrary"))]
92impl<'a> arbitrary::Arbitrary<'a> for BlockAccessLists {
93    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
94        let entries = Vec::<Option<Vec<alloy_eip7928::AccountChanges>>>::arbitrary(u)?
95            .into_iter()
96            .map(|entry| {
97                let entry = entry?;
98                let mut out = Vec::new();
99                alloy_rlp::encode_list(&entry, &mut out);
100                Some(Bytes::from(out))
101            })
102            .collect();
103        Ok(Self(entries))
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use alloy_eip7928::{
111        AccountChanges, BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges,
112        StorageChange,
113    };
114    use alloy_primitives::{Address, U256};
115    use alloy_rlp::{EMPTY_LIST_CODE, EMPTY_STRING_CODE};
116
117    fn elaborate_account_changes(seed: u8) -> Vec<AccountChanges> {
118        vec![
119            AccountChanges {
120                address: Address::from([seed; 20]),
121                storage_changes: vec![SlotChanges::new(
122                    U256::from_be_bytes([seed.wrapping_add(1); 32]),
123                    vec![
124                        StorageChange::new(
125                            BlockAccessIndex::new(1),
126                            U256::from_be_bytes([seed.wrapping_add(2); 32]),
127                        ),
128                        StorageChange::new(
129                            BlockAccessIndex::new(2),
130                            U256::from_be_bytes([seed.wrapping_add(3); 32]),
131                        ),
132                    ],
133                )],
134                storage_reads: vec![
135                    U256::from_be_bytes([seed.wrapping_add(4); 32]),
136                    U256::from_be_bytes([seed.wrapping_add(5); 32]),
137                ],
138                balance_changes: vec![
139                    BalanceChange::new(BlockAccessIndex::new(1), U256::from(1_000 + seed as u64)),
140                    BalanceChange::new(BlockAccessIndex::new(2), U256::from(2_000 + seed as u64)),
141                ],
142                nonce_changes: vec![
143                    NonceChange::new(BlockAccessIndex::new(1), seed as u64),
144                    NonceChange::new(BlockAccessIndex::new(2), seed as u64 + 1),
145                ],
146                code_changes: vec![CodeChange::new(
147                    BlockAccessIndex::new(1),
148                    Bytes::from(vec![0x60, seed, 0x61, seed.wrapping_add(1), 0x56]),
149                )],
150            },
151            AccountChanges {
152                address: Address::from([seed.wrapping_add(9); 20]),
153                storage_changes: Vec::new(),
154                storage_reads: vec![U256::from_be_bytes([seed.wrapping_add(10); 32])],
155                balance_changes: vec![BalanceChange::new(
156                    BlockAccessIndex::new(3),
157                    U256::from(3_000 + seed as u64),
158                )],
159                nonce_changes: vec![NonceChange::new(BlockAccessIndex::new(3), seed as u64 + 2)],
160                code_changes: vec![CodeChange::new(
161                    BlockAccessIndex::new(2),
162                    Bytes::from(vec![0x5f, 0x5f, 0xf3]),
163                )],
164            },
165        ]
166    }
167
168    fn elaborate_bal_entry(seed: u8) -> Bytes {
169        let account_changes = elaborate_account_changes(seed);
170        let mut out = Vec::new();
171        alloy_rlp::encode_list(&account_changes, &mut out);
172        Bytes::from(out)
173    }
174
175    #[test]
176    fn unavailable_bal_entry_encodes_as_empty_string() {
177        let encoded = alloy_rlp::encode(BlockAccessLists(vec![None]));
178        assert_eq!(encoded, vec![0xc1, EMPTY_STRING_CODE]);
179    }
180
181    #[test]
182    fn empty_bal_entry_encodes_as_empty_list() {
183        let encoded =
184            alloy_rlp::encode(BlockAccessLists(vec![Some(Bytes::from_static(&[EMPTY_LIST_CODE]))]));
185        assert_eq!(encoded, vec![0xc1, EMPTY_LIST_CODE]);
186    }
187
188    #[test]
189    fn block_access_lists_roundtrip_preserves_raw_bal_items() {
190        let original = BlockAccessLists(vec![
191            None,
192            Some(Bytes::from_static(&[EMPTY_LIST_CODE])),
193            Some(Bytes::from_static(&[0xc1, EMPTY_LIST_CODE])),
194            Some(Bytes::from_static(&[0xc2, EMPTY_LIST_CODE, EMPTY_LIST_CODE])),
195        ]);
196
197        let encoded = alloy_rlp::encode(&original);
198        let decoded = alloy_rlp::decode_exact::<BlockAccessLists>(&encoded).unwrap();
199
200        assert_eq!(decoded, original);
201    }
202
203    #[test]
204    fn empty_response_roundtrips() {
205        let original = BlockAccessLists(Vec::new());
206        let encoded = alloy_rlp::encode(&original);
207        let decoded = alloy_rlp::decode_exact::<BlockAccessLists>(&encoded).unwrap();
208
209        assert_eq!(decoded, original);
210    }
211
212    #[test]
213    fn rejects_non_list_bal_entries() {
214        let err = alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc1, 0x01]).unwrap_err();
215        assert!(matches!(err, alloy_rlp::Error::UnexpectedString));
216    }
217
218    #[test]
219    fn rejects_non_empty_string_bal_entries() {
220        let err = alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc2, 0x81, 0x80]).unwrap_err();
221        assert!(matches!(err, alloy_rlp::Error::UnexpectedString));
222    }
223
224    #[test]
225    fn rejects_truncated_response_payload() {
226        let err =
227            alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc2, EMPTY_LIST_CODE]).unwrap_err();
228        assert!(matches!(err, alloy_rlp::Error::InputTooShort));
229    }
230
231    #[test]
232    fn elaborate_bal_entry_roundtrips_into_account_changes() {
233        let expected = elaborate_account_changes(0x11);
234        let decoded =
235            alloy_rlp::decode_exact::<Vec<AccountChanges>>(&elaborate_bal_entry(0x11)).unwrap();
236
237        assert_eq!(decoded, expected);
238    }
239
240    #[test]
241    fn elaborate_block_access_lists_roundtrip_preserves_complex_bal_contents() {
242        let original = BlockAccessLists(vec![
243            Some(elaborate_bal_entry(0x11)),
244            None,
245            Some(Bytes::from_static(&[EMPTY_LIST_CODE])),
246            Some(elaborate_bal_entry(0x77)),
247        ]);
248
249        let encoded = alloy_rlp::encode(&original);
250        let decoded = alloy_rlp::decode_exact::<BlockAccessLists>(&encoded).unwrap();
251
252        assert_eq!(decoded, original);
253        assert_eq!(
254            alloy_rlp::decode_exact::<Vec<AccountChanges>>(decoded.0[0].as_ref().unwrap()).unwrap(),
255            elaborate_account_changes(0x11)
256        );
257        assert!(decoded.0[1].is_none());
258        assert_eq!(
259            alloy_rlp::decode_exact::<Vec<AccountChanges>>(decoded.0[2].as_ref().unwrap()).unwrap(),
260            vec![]
261        );
262        assert_eq!(
263            alloy_rlp::decode_exact::<Vec<AccountChanges>>(decoded.0[3].as_ref().unwrap()).unwrap(),
264            elaborate_account_changes(0x77)
265        );
266    }
267
268    #[test]
269    fn elaborate_block_access_lists_embed_raw_bal_payloads_without_reencoding() {
270        let first = elaborate_bal_entry(0x21);
271        let second = elaborate_bal_entry(0x42);
272        let encoded =
273            alloy_rlp::encode(BlockAccessLists(vec![Some(first.clone()), Some(second.clone())]));
274
275        let header = alloy_rlp::Header::decode(&mut &encoded[..]).unwrap();
276        let payload = &encoded[header.length()..];
277        let expected_payload = [first.as_ref(), second.as_ref()].concat();
278
279        assert!(header.list);
280        assert_eq!(payload, expected_payload.as_slice());
281    }
282}