1use 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#[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 pub Vec<B256>,
19);
20
21#[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 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}