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)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258pub enum BalError {
259 AccountNotFound {
266 address: Address,
268 },
269 InvalidAccountId {
277 account_id: AccountId,
279 },
280 SlotNotFound {
288 address: Address,
290 slot: StorageKey,
292 },
293}
294
295impl core::fmt::Display for BalError {
296 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
297 match self {
298 Self::AccountNotFound { address } => {
299 write!(f, "Account {address} not found in BAL")
300 }
301 Self::InvalidAccountId { account_id } => {
302 write!(f, "Invalid BAL account id {}", account_id.get())
303 }
304 Self::SlotNotFound { address, slot } => {
305 write!(f, "Slot {slot:#x} not found in BAL for account {address}")
306 }
307 }
308 }
309}
310
311impl core::error::Error for BalError {}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use alloy_eip7928::{
317 AccountChanges as AlloyAccountChanges, BalanceChange as AlloyBalanceChange,
318 CodeChange as AlloyCodeChange, NonceChange as AlloyNonceChange,
319 SlotChanges as AlloySlotChanges, StorageChange as AlloyStorageChange,
320 };
321 use bytecode::Bytecode;
322 use primitives::{Bytes, B256, U256};
323 use std::collections::BTreeMap;
324
325 fn code(byte: u8) -> (B256, Bytecode) {
326 let bytecode = Bytecode::new_raw(vec![byte].into());
327 (bytecode.hash_slow(), bytecode)
328 }
329
330 const fn idx(index: u64) -> BlockAccessIndex {
331 BlockAccessIndex::new(index)
332 }
333
334 #[test]
335 fn into_alloy_bal_canonicalizes_eip_7928_ordering() {
336 let low_address = Address::with_last_byte(1);
337 let high_address = Address::with_last_byte(2);
338
339 let unordered_account = AccountBal {
340 account_info: AccountInfoBal {
341 nonce: BalWrites {
342 writes: vec![(idx(9), 90), (idx(4), 40)],
343 },
344 balance: BalWrites {
345 writes: vec![(idx(5), U256::from(50)), (idx(2), U256::from(20))],
346 },
347 code: BalWrites {
348 writes: vec![(idx(7), code(7)), (idx(3), code(3))],
349 },
350 },
351 storage: StorageBal {
352 storage: BTreeMap::from([
353 (
354 U256::from(4),
355 BalWrites {
356 writes: vec![(idx(8), U256::from(80)), (idx(6), U256::from(60))],
357 },
358 ),
359 (U256::from(1), BalWrites { writes: vec![] }),
360 (
361 U256::from(2),
362 BalWrites {
363 writes: vec![(idx(3), U256::from(30)), (idx(1), U256::from(10))],
364 },
365 ),
366 (U256::from(3), BalWrites { writes: vec![] }),
367 ]),
368 },
369 };
370
371 let alloy_bal = Bal::from_iter([
372 (high_address, AccountBal::default()),
373 (low_address, unordered_account),
374 ])
375 .into_alloy_bal();
376
377 assert_eq!(
378 alloy_bal
379 .iter()
380 .map(|account| account.address)
381 .collect::<Vec<_>>(),
382 vec![low_address, high_address]
383 );
384
385 let account = &alloy_bal[0];
386 assert_eq!(account.storage_reads, vec![U256::from(1), U256::from(3)]);
387 assert_eq!(
388 account
389 .storage_changes
390 .iter()
391 .map(|slot| slot.slot)
392 .collect::<Vec<_>>(),
393 vec![U256::from(2), U256::from(4)]
394 );
395 assert_eq!(
396 account.storage_changes[0]
397 .changes
398 .iter()
399 .map(|change| change.block_access_index)
400 .collect::<Vec<_>>(),
401 vec![idx(1), idx(3)]
402 );
403 assert_eq!(
404 account.storage_changes[1]
405 .changes
406 .iter()
407 .map(|change| change.block_access_index)
408 .collect::<Vec<_>>(),
409 vec![idx(6), idx(8)]
410 );
411 assert_eq!(
412 account
413 .balance_changes
414 .iter()
415 .map(|change| change.block_access_index)
416 .collect::<Vec<_>>(),
417 vec![idx(2), idx(5)]
418 );
419 assert_eq!(
420 account
421 .nonce_changes
422 .iter()
423 .map(|change| change.block_access_index)
424 .collect::<Vec<_>>(),
425 vec![idx(4), idx(9)]
426 );
427 assert_eq!(
428 account
429 .code_changes
430 .iter()
431 .map(|change| change.block_access_index)
432 .collect::<Vec<_>>(),
433 vec![idx(3), idx(7)]
434 );
435 }
436
437 #[test]
438 fn try_from_alloy_decodes_block_access_list() {
439 let address = Address::with_last_byte(1);
440 let code_bytes = Bytes::from_static(&[0x60, 0x00]);
441 let alloy_bal = vec![AlloyAccountChanges {
442 address,
443 code_changes: vec![AlloyCodeChange::new(idx(1), code_bytes.clone())],
444 ..Default::default()
445 }];
446
447 let bal = Bal::try_from_alloy(alloy_bal).unwrap();
448 let account = bal.accounts.get(&address).unwrap();
449 let (_, bytecode) = &account.account_info.code.writes[0].1;
450
451 assert_eq!(bytecode.original_bytes(), code_bytes);
452 }
453
454 #[test]
455 fn clone_from_alloy_matches_owned_conversion() {
456 let address = Address::with_last_byte(1);
457 let code_bytes = Bytes::from_static(&[0x60, 0x00]);
458 let alloy_bal = vec![AlloyAccountChanges {
459 address,
460 storage_changes: vec![AlloySlotChanges::new(
461 U256::from(1),
462 vec![AlloyStorageChange::new(idx(1), U256::from(10))],
463 )],
464 storage_reads: vec![U256::from(2)],
465 balance_changes: vec![AlloyBalanceChange::new(idx(2), U256::from(20))],
466 nonce_changes: vec![AlloyNonceChange::new(idx(3), 30)],
467 code_changes: vec![AlloyCodeChange::new(idx(4), code_bytes.clone())],
468 }];
469
470 let borrowed = Bal::clone_from_alloy(&alloy_bal).unwrap();
471 let owned = Bal::try_from_alloy(alloy_bal.clone()).unwrap();
472
473 assert_eq!(borrowed, owned);
474 assert_eq!(alloy_bal[0].code_changes[0].new_code(), &code_bytes);
475 }
476
477 #[test]
478 fn try_from_alloy_errors_on_invalid_code_change() {
479 let alloy_bal = vec![AlloyAccountChanges {
480 address: Address::with_last_byte(1),
481 code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
482 ..Default::default()
483 }];
484
485 assert!(Bal::try_from_alloy(alloy_bal).is_err());
486 }
487
488 #[test]
489 fn clone_from_alloy_errors_on_invalid_code_change() {
490 let alloy_bal = vec![AlloyAccountChanges {
491 address: Address::with_last_byte(1),
492 code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
493 ..Default::default()
494 }];
495
496 assert!(Bal::clone_from_alloy(&alloy_bal).is_err());
497 }
498}