1pub mod account;
16pub mod alloy;
17pub mod writes;
18
19pub use account::{AccountBal, AccountInfoBal, StorageBal};
20pub use alloy_eip7928::BlockAccessIndex;
21pub use writes::BalWrites;
22
23use crate::{Account, AccountId, AccountInfo};
24use alloy_eip7928::BlockAccessList as AlloyBal;
25use primitives::{Address, AddressIndexMap, StorageKey, StorageValue};
26
27#[derive(Debug, Default, Clone, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct Bal {
31 pub accounts: AddressIndexMap<AccountBal>,
33}
34
35impl FromIterator<(Address, AccountBal)> for Bal {
36 fn from_iter<I: IntoIterator<Item = (Address, AccountBal)>>(iter: I) -> Self {
37 Self {
38 accounts: iter.into_iter().collect(),
39 }
40 }
41}
42
43impl Bal {
44 pub fn new() -> Self {
46 Self {
47 accounts: AddressIndexMap::default(),
48 }
49 }
50
51 #[cfg(feature = "std")]
53 pub fn pretty_print(&self) {
54 println!("=== Block Access List (BAL) ===");
55 println!("Total accounts: {}", self.accounts.len());
56 println!();
57
58 if self.accounts.is_empty() {
59 println!("(empty)");
60 return;
61 }
62
63 let mut sorted_accounts: Vec<_> = self.accounts.iter().collect();
65 sorted_accounts.sort_unstable_by_key(|(address, _)| *address);
66
67 for (idx, (address, account)) in sorted_accounts.into_iter().enumerate() {
68 println!("Account #{idx} - Address: {address:?}");
69 println!(" Account Info:");
70
71 if account.account_info.nonce.is_empty() {
73 println!(" Nonce: (read-only, no writes)");
74 } else {
75 println!(" Nonce writes:");
76 for (bal_index, nonce) in &account.account_info.nonce.writes {
77 println!(" [{bal_index}] -> {nonce}");
78 }
79 }
80
81 if account.account_info.balance.is_empty() {
83 println!(" Balance: (read-only, no writes)");
84 } else {
85 println!(" Balance writes:");
86 for (bal_index, balance) in &account.account_info.balance.writes {
87 println!(" [{bal_index}] -> {balance}");
88 }
89 }
90
91 if account.account_info.code.is_empty() {
93 println!(" Code: (read-only, no writes)");
94 } else {
95 println!(" Code writes:");
96 for (bal_index, (code_hash, bytecode)) in &account.account_info.code.writes {
97 println!(
98 " [{}] -> hash: {:?}, size: {} bytes",
99 bal_index,
100 code_hash,
101 bytecode.len()
102 );
103 }
104 }
105
106 println!(" Storage:");
108 if account.storage.storage.is_empty() {
109 println!(" (no storage slots)");
110 } else {
111 println!(" Total slots: {}", account.storage.storage.len());
112 for (storage_key, storage_writes) in &account.storage.storage {
113 println!(" Slot: {storage_key:#x}");
114 if storage_writes.is_empty() {
115 println!(" (read-only, no writes)");
116 } else {
117 println!(" Writes:");
118 for (bal_index, value) in &storage_writes.writes {
119 println!(" [{bal_index}] -> {value:?}");
120 }
121 }
122 }
123 }
124
125 println!();
126 }
127 println!("=== End of BAL ===");
128 }
129
130 #[inline]
131 pub fn update_account(
133 &mut self,
134 bal_index: BlockAccessIndex,
135 address: Address,
136 account: &Account,
137 ) {
138 let bal_account = self.accounts.entry(address).or_default();
139 bal_account.update(bal_index, account);
140 }
141
142 pub fn populate_account_info(
144 &self,
145 account_id: AccountId,
146 bal_index: BlockAccessIndex,
147 account: &mut AccountInfo,
148 ) -> Result<bool, BalError> {
149 let Some((_, bal_account)) = self.accounts.get_index(account_id.get()) else {
150 return Err(BalError::InvalidAccountId { account_id });
151 };
152 account.account_id = Some(account_id);
153
154 Ok(bal_account.populate_account_info(bal_index, account))
155 }
156
157 #[inline]
161 pub fn populate_storage_slot_by_account_id(
162 &self,
163 account_id: AccountId,
164 bal_index: BlockAccessIndex,
165 key: StorageKey,
166 value: &mut StorageValue,
167 ) -> Result<(), BalError> {
168 let Some((address, bal_account)) = self.accounts.get_index(account_id.get()) else {
169 return Err(BalError::InvalidAccountId { account_id });
170 };
171
172 if let Some(bal_value) = bal_account.storage.get(address, key, bal_index)? {
173 *value = bal_value;
174 };
175
176 Ok(())
177 }
178
179 #[inline]
181 pub fn populate_storage_slot(
182 &self,
183 account_address: Address,
184 bal_index: BlockAccessIndex,
185 key: StorageKey,
186 value: &mut StorageValue,
187 ) -> Result<(), BalError> {
188 let Some(bal_account) = self.accounts.get(&account_address) else {
189 return Err(BalError::AccountNotFound {
190 address: account_address,
191 });
192 };
193
194 if let Some(bal_value) = bal_account.storage.get(&account_address, key, bal_index)? {
195 *value = bal_value;
196 };
197 Ok(())
198 }
199
200 pub fn account_storage(
202 &self,
203 account_id: AccountId,
204 key: StorageKey,
205 bal_index: BlockAccessIndex,
206 ) -> Result<StorageValue, BalError> {
207 let Some((address, bal_account)) = self.accounts.get_index(account_id.get()) else {
208 return Err(BalError::InvalidAccountId { account_id });
209 };
210
211 let Some(storage_value) = bal_account.storage.get(address, key, bal_index)? else {
212 return Err(BalError::SlotNotFound {
213 address: *address,
214 slot: key,
215 });
216 };
217
218 Ok(storage_value)
219 }
220
221 pub fn into_alloy_bal(self) -> AlloyBal {
230 let mut alloy_bal = AlloyBal::from_iter(
231 self.accounts
232 .into_iter()
233 .map(|(address, account)| account.into_alloy_account(address)),
234 );
235 alloy_bal.sort_unstable_by_key(|a| a.address);
236 alloy_bal
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
242#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
243pub enum BalError {
244 AccountNotFound {
246 address: Address,
248 },
249 InvalidAccountId {
254 account_id: AccountId,
256 },
257 SlotNotFound {
259 address: Address,
261 slot: StorageKey,
263 },
264}
265
266impl core::fmt::Display for BalError {
267 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
268 match self {
269 Self::AccountNotFound { address } => {
270 write!(f, "Account {address} not found in BAL")
271 }
272 Self::InvalidAccountId { account_id } => {
273 write!(f, "Invalid BAL account id {}", account_id.get())
274 }
275 Self::SlotNotFound { address, slot } => {
276 write!(f, "Slot {slot:#x} not found in BAL for account {address}")
277 }
278 }
279 }
280}
281
282impl core::error::Error for BalError {}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use alloy_eip7928::{
288 AccountChanges as AlloyAccountChanges, BalanceChange as AlloyBalanceChange,
289 CodeChange as AlloyCodeChange, NonceChange as AlloyNonceChange,
290 SlotChanges as AlloySlotChanges, StorageChange as AlloyStorageChange,
291 };
292 use bytecode::Bytecode;
293 use primitives::{Bytes, B256, U256};
294 use std::collections::BTreeMap;
295
296 fn code(byte: u8) -> (B256, Bytecode) {
297 let bytecode = Bytecode::new_raw(vec![byte].into());
298 (bytecode.hash_slow(), bytecode)
299 }
300
301 const fn idx(index: u64) -> BlockAccessIndex {
302 BlockAccessIndex::new(index)
303 }
304
305 #[test]
306 fn into_alloy_bal_canonicalizes_eip_7928_ordering() {
307 let low_address = Address::with_last_byte(1);
308 let high_address = Address::with_last_byte(2);
309
310 let unordered_account = AccountBal {
311 account_info: AccountInfoBal {
312 nonce: BalWrites {
313 writes: vec![(idx(9), 90), (idx(4), 40)],
314 },
315 balance: BalWrites {
316 writes: vec![(idx(5), U256::from(50)), (idx(2), U256::from(20))],
317 },
318 code: BalWrites {
319 writes: vec![(idx(7), code(7)), (idx(3), code(3))],
320 },
321 },
322 storage: StorageBal {
323 storage: BTreeMap::from([
324 (
325 U256::from(4),
326 BalWrites {
327 writes: vec![(idx(8), U256::from(80)), (idx(6), U256::from(60))],
328 },
329 ),
330 (U256::from(1), BalWrites { writes: vec![] }),
331 (
332 U256::from(2),
333 BalWrites {
334 writes: vec![(idx(3), U256::from(30)), (idx(1), U256::from(10))],
335 },
336 ),
337 (U256::from(3), BalWrites { writes: vec![] }),
338 ]),
339 },
340 };
341
342 let alloy_bal = Bal::from_iter([
343 (high_address, AccountBal::default()),
344 (low_address, unordered_account),
345 ])
346 .into_alloy_bal();
347
348 assert_eq!(
349 alloy_bal
350 .iter()
351 .map(|account| account.address)
352 .collect::<Vec<_>>(),
353 vec![low_address, high_address]
354 );
355
356 let account = &alloy_bal[0];
357 assert_eq!(account.storage_reads, vec![U256::from(1), U256::from(3)]);
358 assert_eq!(
359 account
360 .storage_changes
361 .iter()
362 .map(|slot| slot.slot)
363 .collect::<Vec<_>>(),
364 vec![U256::from(2), U256::from(4)]
365 );
366 assert_eq!(
367 account.storage_changes[0]
368 .changes
369 .iter()
370 .map(|change| change.block_access_index)
371 .collect::<Vec<_>>(),
372 vec![idx(1), idx(3)]
373 );
374 assert_eq!(
375 account.storage_changes[1]
376 .changes
377 .iter()
378 .map(|change| change.block_access_index)
379 .collect::<Vec<_>>(),
380 vec![idx(6), idx(8)]
381 );
382 assert_eq!(
383 account
384 .balance_changes
385 .iter()
386 .map(|change| change.block_access_index)
387 .collect::<Vec<_>>(),
388 vec![idx(2), idx(5)]
389 );
390 assert_eq!(
391 account
392 .nonce_changes
393 .iter()
394 .map(|change| change.block_access_index)
395 .collect::<Vec<_>>(),
396 vec![idx(4), idx(9)]
397 );
398 assert_eq!(
399 account
400 .code_changes
401 .iter()
402 .map(|change| change.block_access_index)
403 .collect::<Vec<_>>(),
404 vec![idx(3), idx(7)]
405 );
406 }
407
408 #[test]
409 fn try_from_alloy_decodes_block_access_list() {
410 let address = Address::with_last_byte(1);
411 let code_bytes = Bytes::from_static(&[0x60, 0x00]);
412 let alloy_bal = vec![AlloyAccountChanges {
413 address,
414 code_changes: vec![AlloyCodeChange::new(idx(1), code_bytes.clone())],
415 ..Default::default()
416 }];
417
418 let bal = Bal::try_from_alloy(alloy_bal).unwrap();
419 let account = bal.accounts.get(&address).unwrap();
420 let (_, bytecode) = &account.account_info.code.writes[0].1;
421
422 assert_eq!(bytecode.original_bytes(), code_bytes);
423 }
424
425 #[test]
426 fn clone_from_alloy_matches_owned_conversion() {
427 let address = Address::with_last_byte(1);
428 let code_bytes = Bytes::from_static(&[0x60, 0x00]);
429 let alloy_bal = vec![AlloyAccountChanges {
430 address,
431 storage_changes: vec![AlloySlotChanges::new(
432 U256::from(1),
433 vec![AlloyStorageChange::new(idx(1), U256::from(10))],
434 )],
435 storage_reads: vec![U256::from(2)],
436 balance_changes: vec![AlloyBalanceChange::new(idx(2), U256::from(20))],
437 nonce_changes: vec![AlloyNonceChange::new(idx(3), 30)],
438 code_changes: vec![AlloyCodeChange::new(idx(4), code_bytes.clone())],
439 }];
440
441 let borrowed = Bal::clone_from_alloy(&alloy_bal).unwrap();
442 let owned = Bal::try_from_alloy(alloy_bal.clone()).unwrap();
443
444 assert_eq!(borrowed, owned);
445 assert_eq!(alloy_bal[0].code_changes[0].new_code(), &code_bytes);
446 }
447
448 #[test]
449 fn try_from_alloy_errors_on_invalid_code_change() {
450 let alloy_bal = vec![AlloyAccountChanges {
451 address: Address::with_last_byte(1),
452 code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
453 ..Default::default()
454 }];
455
456 assert!(Bal::try_from_alloy(alloy_bal).is_err());
457 }
458
459 #[test]
460 fn clone_from_alloy_errors_on_invalid_code_change() {
461 let alloy_bal = vec![AlloyAccountChanges {
462 address: Address::with_last_byte(1),
463 code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
464 ..Default::default()
465 }];
466
467 assert!(Bal::clone_from_alloy(&alloy_bal).is_err());
468 }
469}