1pub mod account;
18pub mod alloy;
19pub mod writes;
20
21pub use account::{AccountBal, AccountInfoBal, StorageBal};
22pub use alloy_eip7928::{BalAccountInfo, BlockAccessIndex};
23pub use writes::BalWrites;
24
25use crate::{Account, AccountId, AccountInfo};
26use alloy_eip7928::BlockAccessList as AlloyBal;
27use primitives::{Address, AddressIndexMap, StorageKey, StorageValue};
28
29#[derive(Debug, Default, Clone, PartialEq, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct Bal {
33 pub accounts: AddressIndexMap<AccountBal>,
35}
36
37impl FromIterator<(Address, AccountBal)> for Bal {
38 fn from_iter<I: IntoIterator<Item = (Address, AccountBal)>>(iter: I) -> Self {
39 Self {
40 accounts: iter.into_iter().collect(),
41 }
42 }
43}
44
45impl Bal {
46 pub fn new() -> Self {
48 Self {
49 accounts: AddressIndexMap::default(),
50 }
51 }
52
53 #[cfg(feature = "std")]
55 pub fn pretty_print(&self) {
56 println!("=== Block Access List (BAL) ===");
57 println!("Total accounts: {}", self.accounts.len());
58 println!();
59
60 if self.accounts.is_empty() {
61 println!("(empty)");
62 return;
63 }
64
65 let mut sorted_accounts: Vec<_> = self.accounts.iter().collect();
67 sorted_accounts.sort_unstable_by_key(|(address, _)| *address);
68
69 for (idx, (address, account)) in sorted_accounts.into_iter().enumerate() {
70 println!("Account #{idx} - Address: {address:?}");
71 println!(" Account Info:");
72
73 if account.account_info.nonce.is_empty() {
75 println!(" Nonce: (read-only, no writes)");
76 } else {
77 println!(" Nonce writes:");
78 for (bal_index, nonce) in &account.account_info.nonce.writes {
79 println!(" [{bal_index}] -> {nonce}");
80 }
81 }
82
83 if account.account_info.balance.is_empty() {
85 println!(" Balance: (read-only, no writes)");
86 } else {
87 println!(" Balance writes:");
88 for (bal_index, balance) in &account.account_info.balance.writes {
89 println!(" [{bal_index}] -> {balance}");
90 }
91 }
92
93 if account.account_info.code.is_empty() {
95 println!(" Code: (read-only, no writes)");
96 } else {
97 println!(" Code writes:");
98 for (bal_index, (code_hash, bytecode)) in &account.account_info.code.writes {
99 println!(
100 " [{}] -> hash: {:?}, size: {} bytes",
101 bal_index,
102 code_hash,
103 bytecode.len()
104 );
105 }
106 }
107
108 println!(" Storage:");
110 if account.storage.storage.is_empty() {
111 println!(" (no storage slots)");
112 } else {
113 println!(" Total slots: {}", account.storage.storage.len());
114 for (storage_key, storage_writes) in &account.storage.storage {
115 println!(" Slot: {storage_key:#x}");
116 if storage_writes.is_empty() {
117 println!(" (read-only, no writes)");
118 } else {
119 println!(" Writes:");
120 for (bal_index, value) in &storage_writes.writes {
121 println!(" [{bal_index}] -> {value:?}");
122 }
123 }
124 }
125 }
126
127 println!();
128 }
129 println!("=== End of BAL ===");
130 }
131
132 #[inline]
133 pub fn update_account(
135 &mut self,
136 bal_index: BlockAccessIndex,
137 address: Address,
138 account: &Account,
139 ) {
140 let bal_account = self.accounts.entry(address).or_default();
141 bal_account.update(bal_index, account);
142 }
143
144 pub fn populate_account_info(
146 &self,
147 account_id: AccountId,
148 bal_index: BlockAccessIndex,
149 account: &mut AccountInfo,
150 ) -> Result<bool, BalError> {
151 let Some((_, bal_account)) = self.accounts.get_index(account_id.get()) else {
152 return Err(BalError::InvalidAccountId { account_id });
153 };
154 account.account_id = Some(account_id);
155
156 Ok(bal_account.populate_account_info(bal_index, account))
157 }
158
159 #[inline]
163 pub fn populate_storage_slot_by_account_id(
164 &self,
165 account_id: AccountId,
166 bal_index: BlockAccessIndex,
167 key: StorageKey,
168 value: &mut StorageValue,
169 ) -> Result<(), BalError> {
170 let Some((address, bal_account)) = self.accounts.get_index(account_id.get()) else {
171 return Err(BalError::InvalidAccountId { account_id });
172 };
173
174 if let Some(bal_value) = bal_account.storage.get(address, key, bal_index)? {
175 *value = bal_value;
176 };
177
178 Ok(())
179 }
180
181 #[inline]
183 pub fn populate_storage_slot(
184 &self,
185 account_address: Address,
186 bal_index: BlockAccessIndex,
187 key: StorageKey,
188 value: &mut StorageValue,
189 ) -> Result<(), BalError> {
190 let Some(bal_account) = self.accounts.get(&account_address) else {
191 return Err(BalError::AccountNotFound {
192 address: account_address,
193 });
194 };
195
196 if let Some(bal_value) = bal_account.storage.get(&account_address, key, bal_index)? {
197 *value = bal_value;
198 };
199 Ok(())
200 }
201
202 pub fn account_storage(
204 &self,
205 account_id: AccountId,
206 key: StorageKey,
207 bal_index: BlockAccessIndex,
208 ) -> Result<StorageValue, BalError> {
209 let Some((address, bal_account)) = self.accounts.get_index(account_id.get()) else {
210 return Err(BalError::InvalidAccountId { account_id });
211 };
212
213 let Some(storage_value) = bal_account.storage.get(address, key, bal_index)? else {
214 return Err(BalError::SlotNotFound {
215 address: *address,
216 slot: key,
217 });
218 };
219
220 Ok(storage_value)
221 }
222
223 pub fn into_alloy_bal(self) -> AlloyBal {
232 let mut alloy_bal = AlloyBal::from_iter(
233 self.accounts
234 .into_iter()
235 .map(|(address, account)| account.into_alloy_account(address)),
236 );
237 alloy_bal.sort_unstable_by_key(|a| a.address);
238 alloy_bal
239 }
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
247pub enum BalAccountLookup {
248 Complete(AccountInfo),
253 Partial(BalAccountInfo),
258 NotCovered,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
279#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
280pub enum BalError {
281 AccountNotFound {
288 address: Address,
290 },
291 InvalidAccountId {
299 account_id: AccountId,
301 },
302 SlotNotFound {
310 address: Address,
312 slot: StorageKey,
314 },
315}
316
317impl core::fmt::Display for BalError {
318 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
319 match self {
320 Self::AccountNotFound { address } => {
321 write!(f, "Account {address} not found in BAL")
322 }
323 Self::InvalidAccountId { account_id } => {
324 write!(f, "Invalid BAL account id {}", account_id.get())
325 }
326 Self::SlotNotFound { address, slot } => {
327 write!(f, "Slot {slot:#x} not found in BAL for account {address}")
328 }
329 }
330 }
331}
332
333impl core::error::Error for BalError {}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use alloy_eip7928::{
339 AccountChanges as AlloyAccountChanges, BalanceChange as AlloyBalanceChange,
340 CodeChange as AlloyCodeChange, NonceChange as AlloyNonceChange,
341 SlotChanges as AlloySlotChanges, StorageChange as AlloyStorageChange,
342 };
343 use bytecode::Bytecode;
344 use primitives::{Bytes, B256, U256};
345 use std::collections::BTreeMap;
346
347 fn code(byte: u8) -> (B256, Bytecode) {
348 let bytecode = Bytecode::new_raw(vec![byte].into());
349 (bytecode.hash_slow(), bytecode)
350 }
351
352 #[test]
353 #[cfg(all(feature = "serde", feature = "account-ext"))]
354 fn extension_history_messagepack_roundtrip() {
355 let mut account = AccountInfoBal::default();
356 let legacy = (&account.nonce, &account.balance, &account.code);
357 let encoded = rmp_serde::to_vec(&account).unwrap();
358 assert_eq!(encoded, rmp_serde::to_vec(&legacy).unwrap());
359 assert_eq!(
360 rmp_serde::from_slice::<AccountInfoBal>(&encoded).unwrap(),
361 account
362 );
363
364 account
366 .extension
367 .force_update(idx(1), crate::AccountExtension::default());
368 let encoded = rmp_serde::to_vec(&account).unwrap();
369 assert_eq!(
370 rmp_serde::from_slice::<AccountInfoBal>(&encoded).unwrap(),
371 account
372 );
373 }
374
375 const fn idx(index: u64) -> BlockAccessIndex {
376 BlockAccessIndex::new(index)
377 }
378
379 #[test]
380 #[cfg(feature = "account-ext")]
381 fn account_extension_roundtrips_through_bal() {
382 let original = AccountInfo::default();
383 let present = original
384 .clone()
385 .with_extension(Bytes::from_static(b"extension"));
386 let mut bal = AccountInfoBal::default();
387 bal.update(idx(1), &original, &present);
388
389 let mut replayed = original;
390 assert!(bal.populate_account_info(idx(2), &mut replayed));
391 assert_eq!(replayed.extension, present.extension);
392 }
393
394 #[test]
395 fn into_alloy_bal_canonicalizes_eip_7928_ordering() {
396 let low_address = Address::with_last_byte(1);
397 let high_address = Address::with_last_byte(2);
398
399 let unordered_account = AccountBal {
400 account_info: AccountInfoBal {
401 nonce: BalWrites {
402 writes: vec![(idx(9), 90), (idx(4), 40)],
403 },
404 balance: BalWrites {
405 writes: vec![(idx(5), U256::from(50)), (idx(2), U256::from(20))],
406 },
407 code: BalWrites {
408 writes: vec![(idx(7), code(7)), (idx(3), code(3))],
409 },
410 #[cfg(feature = "account-ext")]
411 extension: BalWrites::default(),
412 },
413 storage: StorageBal {
414 storage: BTreeMap::from([
415 (
416 U256::from(4),
417 BalWrites {
418 writes: vec![(idx(8), U256::from(80)), (idx(6), U256::from(60))],
419 },
420 ),
421 (U256::from(1), BalWrites { writes: vec![] }),
422 (
423 U256::from(2),
424 BalWrites {
425 writes: vec![(idx(3), U256::from(30)), (idx(1), U256::from(10))],
426 },
427 ),
428 (U256::from(3), BalWrites { writes: vec![] }),
429 ]),
430 },
431 };
432
433 let alloy_bal = Bal::from_iter([
434 (high_address, AccountBal::default()),
435 (low_address, unordered_account),
436 ])
437 .into_alloy_bal();
438
439 assert_eq!(
440 alloy_bal
441 .iter()
442 .map(|account| account.address)
443 .collect::<Vec<_>>(),
444 vec![low_address, high_address]
445 );
446
447 let account = &alloy_bal[0];
448 assert_eq!(account.storage_reads, vec![U256::from(1), U256::from(3)]);
449 assert_eq!(
450 account
451 .storage_changes
452 .iter()
453 .map(|slot| slot.slot)
454 .collect::<Vec<_>>(),
455 vec![U256::from(2), U256::from(4)]
456 );
457 assert_eq!(
458 account.storage_changes[0]
459 .changes
460 .iter()
461 .map(|change| change.block_access_index)
462 .collect::<Vec<_>>(),
463 vec![idx(1), idx(3)]
464 );
465 assert_eq!(
466 account.storage_changes[1]
467 .changes
468 .iter()
469 .map(|change| change.block_access_index)
470 .collect::<Vec<_>>(),
471 vec![idx(6), idx(8)]
472 );
473 assert_eq!(
474 account
475 .balance_changes
476 .iter()
477 .map(|change| change.block_access_index)
478 .collect::<Vec<_>>(),
479 vec![idx(2), idx(5)]
480 );
481 assert_eq!(
482 account
483 .nonce_changes
484 .iter()
485 .map(|change| change.block_access_index)
486 .collect::<Vec<_>>(),
487 vec![idx(4), idx(9)]
488 );
489 assert_eq!(
490 account
491 .code_changes
492 .iter()
493 .map(|change| change.block_access_index)
494 .collect::<Vec<_>>(),
495 vec![idx(3), idx(7)]
496 );
497 }
498
499 #[test]
500 fn try_from_alloy_decodes_block_access_list() {
501 let address = Address::with_last_byte(1);
502 let code_bytes = Bytes::from_static(&[0x60, 0x00]);
503 let alloy_bal = vec![AlloyAccountChanges {
504 address,
505 code_changes: vec![AlloyCodeChange::new(idx(1), code_bytes.clone())],
506 ..Default::default()
507 }];
508
509 let bal = Bal::try_from_alloy(alloy_bal).unwrap();
510 let account = bal.accounts.get(&address).unwrap();
511 let (_, bytecode) = &account.account_info.code.writes[0].1;
512
513 assert_eq!(bytecode.original_bytes(), code_bytes);
514 }
515
516 #[test]
517 fn clone_from_alloy_matches_owned_conversion() {
518 let address = Address::with_last_byte(1);
519 let code_bytes = Bytes::from_static(&[0x60, 0x00]);
520 let alloy_bal = vec![AlloyAccountChanges {
521 address,
522 storage_changes: vec![AlloySlotChanges::new(
523 U256::from(1),
524 vec![AlloyStorageChange::new(idx(1), U256::from(10))],
525 )],
526 storage_reads: vec![U256::from(2)],
527 balance_changes: vec![AlloyBalanceChange::new(idx(2), U256::from(20))],
528 nonce_changes: vec![AlloyNonceChange::new(idx(3), 30)],
529 code_changes: vec![AlloyCodeChange::new(idx(4), code_bytes.clone())],
530 }];
531
532 let borrowed = Bal::clone_from_alloy(&alloy_bal).unwrap();
533 let owned = Bal::try_from_alloy(alloy_bal.clone()).unwrap();
534
535 assert_eq!(borrowed, owned);
536 assert_eq!(alloy_bal[0].code_changes[0].new_code(), &code_bytes);
537 }
538
539 #[test]
540 fn try_from_alloy_errors_on_invalid_code_change() {
541 let alloy_bal = vec![AlloyAccountChanges {
542 address: Address::with_last_byte(1),
543 code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
544 ..Default::default()
545 }];
546
547 assert!(Bal::try_from_alloy(alloy_bal).is_err());
548 }
549
550 #[test]
551 fn clone_from_alloy_errors_on_invalid_code_change() {
552 let alloy_bal = vec![AlloyAccountChanges {
553 address: Address::with_last_byte(1),
554 code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
555 ..Default::default()
556 }];
557
558 assert!(Bal::clone_from_alloy(&alloy_bal).is_err());
559 }
560
561 #[test]
562 fn account_info_lookup_uses_exclusive_index_and_includes_post_execution() {
563 let (code_hash, bytecode) = code(1);
564 #[cfg(feature = "account-ext")]
565 let extension = crate::AccountExtension::copy_from_slice(b"extension");
566 let account = AccountInfoBal {
567 nonce: BalWrites::new(vec![(idx(2), 5)]),
568 balance: BalWrites::new(vec![
569 (idx(0), U256::from(1)),
570 (idx(1), U256::from(7)),
571 (idx(3), U256::from(42)),
573 ]),
574 code: BalWrites::new(vec![(idx(3), (code_hash, bytecode.clone()))]),
575 #[cfg(feature = "account-ext")]
576 extension: BalWrites::new(vec![(idx(3), extension.clone())]),
577 };
578
579 for (index, balance, nonce) in [
580 (0, None, None),
581 (1, Some(U256::from(1)), None),
582 (2, Some(U256::from(7)), None),
583 (3, Some(U256::from(7)), Some(5)),
584 ] {
585 assert_eq!(
586 account.account_info_lookup(idx(index)),
587 BalAccountLookup::Partial(BalAccountInfo {
588 balance,
589 nonce,
590 code_hash: None
591 })
592 );
593 }
594
595 let BalAccountLookup::Complete(info) = account.account_info_lookup(idx(4)) else {
596 panic!("all fields must be known after post-execution");
597 };
598 assert_eq!(info.balance, U256::from(42));
599 assert_eq!(info.nonce, 5);
600 assert_eq!(info.code_hash, code_hash);
601 assert_eq!(info.code, Some(bytecode));
602 #[cfg(feature = "account-ext")]
603 assert_eq!(info.extension, extension);
604 }
605
606 #[test]
607 fn account_info_lookup_distinguishes_missing_fields_from_empty_values() {
608 let index = idx(1);
609 let bytecode = Bytecode::default();
610 for fields in 0..8 {
611 let mut account = AccountInfoBal::default();
612 let mut expected = BalAccountInfo::default();
613 if fields & 1 != 0 {
614 account.balance = BalWrites::new(vec![(index, U256::ZERO)]);
615 expected.balance = Some(U256::ZERO);
616 }
617 if fields & 2 != 0 {
618 account.nonce = BalWrites::new(vec![(index, 0)]);
619 expected.nonce = Some(0);
620 }
621 if fields & 4 != 0 {
622 account.code =
623 BalWrites::new(vec![(index, (bytecode.hash_slow(), bytecode.clone()))]);
624 expected.code_hash = Some(bytecode.hash_slow());
625 }
626 #[cfg(feature = "account-ext")]
627 {
628 account.extension = BalWrites::new(vec![(index, crate::AccountExtension::new())]);
629 }
630
631 let lookup = account.account_info_lookup(idx(2));
632 if expected.is_complete() {
633 let BalAccountLookup::Complete(info) = lookup else {
634 panic!("all recorded fields must produce a complete account");
635 };
636 assert!(info.is_empty());
637 assert_eq!(info.code, Some(bytecode.clone()));
638 } else {
639 assert_eq!(lookup, BalAccountLookup::Partial(expected));
640 }
641 }
642 }
643
644 #[test]
645 #[cfg(feature = "account-ext")]
646 fn account_info_lookup_requires_extension_write() {
647 let (code_hash, bytecode) = code(1);
648 let account = AccountInfoBal {
649 nonce: BalWrites::new(vec![(idx(1), 1)]),
650 balance: BalWrites::new(vec![(idx(1), U256::from(1))]),
651 code: BalWrites::new(vec![(idx(1), (code_hash, bytecode))]),
652 extension: BalWrites::default(),
653 };
654
655 assert_eq!(
657 account.account_info_lookup(idx(2)),
658 BalAccountLookup::Partial(BalAccountInfo {
659 balance: Some(U256::from(1)),
660 nonce: Some(1),
661 code_hash: Some(code_hash),
662 })
663 );
664 }
665}