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 #[test]
331 #[cfg(all(feature = "serde", feature = "account-ext"))]
332 fn extension_history_messagepack_roundtrip() {
333 let mut account = AccountInfoBal::default();
334 let legacy = (&account.nonce, &account.balance, &account.code);
335 let encoded = rmp_serde::to_vec(&account).unwrap();
336 assert_eq!(encoded, rmp_serde::to_vec(&legacy).unwrap());
337 assert_eq!(
338 rmp_serde::from_slice::<AccountInfoBal>(&encoded).unwrap(),
339 account
340 );
341
342 account
344 .extension
345 .force_update(idx(1), crate::AccountExtension::default());
346 let encoded = rmp_serde::to_vec(&account).unwrap();
347 assert_eq!(
348 rmp_serde::from_slice::<AccountInfoBal>(&encoded).unwrap(),
349 account
350 );
351 }
352
353 const fn idx(index: u64) -> BlockAccessIndex {
354 BlockAccessIndex::new(index)
355 }
356
357 #[test]
358 #[cfg(feature = "account-ext")]
359 fn account_extension_roundtrips_through_bal() {
360 let original = AccountInfo::default();
361 let present = original
362 .clone()
363 .with_extension(Bytes::from_static(b"extension"));
364 let mut bal = AccountInfoBal::default();
365 bal.update(idx(1), &original, &present);
366
367 let mut replayed = original;
368 assert!(bal.populate_account_info(idx(2), &mut replayed));
369 assert_eq!(replayed.extension, present.extension);
370 }
371
372 #[test]
373 fn into_alloy_bal_canonicalizes_eip_7928_ordering() {
374 let low_address = Address::with_last_byte(1);
375 let high_address = Address::with_last_byte(2);
376
377 let unordered_account = AccountBal {
378 account_info: AccountInfoBal {
379 nonce: BalWrites {
380 writes: vec![(idx(9), 90), (idx(4), 40)],
381 },
382 balance: BalWrites {
383 writes: vec![(idx(5), U256::from(50)), (idx(2), U256::from(20))],
384 },
385 code: BalWrites {
386 writes: vec![(idx(7), code(7)), (idx(3), code(3))],
387 },
388 #[cfg(feature = "account-ext")]
389 extension: BalWrites::default(),
390 },
391 storage: StorageBal {
392 storage: BTreeMap::from([
393 (
394 U256::from(4),
395 BalWrites {
396 writes: vec![(idx(8), U256::from(80)), (idx(6), U256::from(60))],
397 },
398 ),
399 (U256::from(1), BalWrites { writes: vec![] }),
400 (
401 U256::from(2),
402 BalWrites {
403 writes: vec![(idx(3), U256::from(30)), (idx(1), U256::from(10))],
404 },
405 ),
406 (U256::from(3), BalWrites { writes: vec![] }),
407 ]),
408 },
409 };
410
411 let alloy_bal = Bal::from_iter([
412 (high_address, AccountBal::default()),
413 (low_address, unordered_account),
414 ])
415 .into_alloy_bal();
416
417 assert_eq!(
418 alloy_bal
419 .iter()
420 .map(|account| account.address)
421 .collect::<Vec<_>>(),
422 vec![low_address, high_address]
423 );
424
425 let account = &alloy_bal[0];
426 assert_eq!(account.storage_reads, vec![U256::from(1), U256::from(3)]);
427 assert_eq!(
428 account
429 .storage_changes
430 .iter()
431 .map(|slot| slot.slot)
432 .collect::<Vec<_>>(),
433 vec![U256::from(2), U256::from(4)]
434 );
435 assert_eq!(
436 account.storage_changes[0]
437 .changes
438 .iter()
439 .map(|change| change.block_access_index)
440 .collect::<Vec<_>>(),
441 vec![idx(1), idx(3)]
442 );
443 assert_eq!(
444 account.storage_changes[1]
445 .changes
446 .iter()
447 .map(|change| change.block_access_index)
448 .collect::<Vec<_>>(),
449 vec![idx(6), idx(8)]
450 );
451 assert_eq!(
452 account
453 .balance_changes
454 .iter()
455 .map(|change| change.block_access_index)
456 .collect::<Vec<_>>(),
457 vec![idx(2), idx(5)]
458 );
459 assert_eq!(
460 account
461 .nonce_changes
462 .iter()
463 .map(|change| change.block_access_index)
464 .collect::<Vec<_>>(),
465 vec![idx(4), idx(9)]
466 );
467 assert_eq!(
468 account
469 .code_changes
470 .iter()
471 .map(|change| change.block_access_index)
472 .collect::<Vec<_>>(),
473 vec![idx(3), idx(7)]
474 );
475 }
476
477 #[test]
478 fn try_from_alloy_decodes_block_access_list() {
479 let address = Address::with_last_byte(1);
480 let code_bytes = Bytes::from_static(&[0x60, 0x00]);
481 let alloy_bal = vec![AlloyAccountChanges {
482 address,
483 code_changes: vec![AlloyCodeChange::new(idx(1), code_bytes.clone())],
484 ..Default::default()
485 }];
486
487 let bal = Bal::try_from_alloy(alloy_bal).unwrap();
488 let account = bal.accounts.get(&address).unwrap();
489 let (_, bytecode) = &account.account_info.code.writes[0].1;
490
491 assert_eq!(bytecode.original_bytes(), code_bytes);
492 }
493
494 #[test]
495 fn clone_from_alloy_matches_owned_conversion() {
496 let address = Address::with_last_byte(1);
497 let code_bytes = Bytes::from_static(&[0x60, 0x00]);
498 let alloy_bal = vec![AlloyAccountChanges {
499 address,
500 storage_changes: vec![AlloySlotChanges::new(
501 U256::from(1),
502 vec![AlloyStorageChange::new(idx(1), U256::from(10))],
503 )],
504 storage_reads: vec![U256::from(2)],
505 balance_changes: vec![AlloyBalanceChange::new(idx(2), U256::from(20))],
506 nonce_changes: vec![AlloyNonceChange::new(idx(3), 30)],
507 code_changes: vec![AlloyCodeChange::new(idx(4), code_bytes.clone())],
508 }];
509
510 let borrowed = Bal::clone_from_alloy(&alloy_bal).unwrap();
511 let owned = Bal::try_from_alloy(alloy_bal.clone()).unwrap();
512
513 assert_eq!(borrowed, owned);
514 assert_eq!(alloy_bal[0].code_changes[0].new_code(), &code_bytes);
515 }
516
517 #[test]
518 fn try_from_alloy_errors_on_invalid_code_change() {
519 let alloy_bal = vec![AlloyAccountChanges {
520 address: Address::with_last_byte(1),
521 code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
522 ..Default::default()
523 }];
524
525 assert!(Bal::try_from_alloy(alloy_bal).is_err());
526 }
527
528 #[test]
529 fn clone_from_alloy_errors_on_invalid_code_change() {
530 let alloy_bal = vec![AlloyAccountChanges {
531 address: Address::with_last_byte(1),
532 code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
533 ..Default::default()
534 }];
535
536 assert!(Bal::clone_from_alloy(&alloy_bal).is_err());
537 }
538}