pub mod account;
pub mod alloy;
pub mod writes;
pub use account::{AccountBal, AccountInfoBal, StorageBal};
pub use alloy_eip7928::BlockAccessIndex;
pub use writes::BalWrites;
use crate::{Account, AccountId, AccountInfo};
use alloy_eip7928::BlockAccessList as AlloyBal;
use primitives::{Address, AddressIndexMap, StorageKey, StorageValue};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bal {
pub accounts: AddressIndexMap<AccountBal>,
}
impl FromIterator<(Address, AccountBal)> for Bal {
fn from_iter<I: IntoIterator<Item = (Address, AccountBal)>>(iter: I) -> Self {
Self {
accounts: iter.into_iter().collect(),
}
}
}
impl Bal {
pub fn new() -> Self {
Self {
accounts: AddressIndexMap::default(),
}
}
#[cfg(feature = "std")]
pub fn pretty_print(&self) {
println!("=== Block Access List (BAL) ===");
println!("Total accounts: {}", self.accounts.len());
println!();
if self.accounts.is_empty() {
println!("(empty)");
return;
}
let mut sorted_accounts: Vec<_> = self.accounts.iter().collect();
sorted_accounts.sort_unstable_by_key(|(address, _)| *address);
for (idx, (address, account)) in sorted_accounts.into_iter().enumerate() {
println!("Account #{idx} - Address: {address:?}");
println!(" Account Info:");
if account.account_info.nonce.is_empty() {
println!(" Nonce: (read-only, no writes)");
} else {
println!(" Nonce writes:");
for (bal_index, nonce) in &account.account_info.nonce.writes {
println!(" [{bal_index}] -> {nonce}");
}
}
if account.account_info.balance.is_empty() {
println!(" Balance: (read-only, no writes)");
} else {
println!(" Balance writes:");
for (bal_index, balance) in &account.account_info.balance.writes {
println!(" [{bal_index}] -> {balance}");
}
}
if account.account_info.code.is_empty() {
println!(" Code: (read-only, no writes)");
} else {
println!(" Code writes:");
for (bal_index, (code_hash, bytecode)) in &account.account_info.code.writes {
println!(
" [{}] -> hash: {:?}, size: {} bytes",
bal_index,
code_hash,
bytecode.len()
);
}
}
println!(" Storage:");
if account.storage.storage.is_empty() {
println!(" (no storage slots)");
} else {
println!(" Total slots: {}", account.storage.storage.len());
for (storage_key, storage_writes) in &account.storage.storage {
println!(" Slot: {storage_key:#x}");
if storage_writes.is_empty() {
println!(" (read-only, no writes)");
} else {
println!(" Writes:");
for (bal_index, value) in &storage_writes.writes {
println!(" [{bal_index}] -> {value:?}");
}
}
}
}
println!();
}
println!("=== End of BAL ===");
}
#[inline]
pub fn update_account(
&mut self,
bal_index: BlockAccessIndex,
address: Address,
account: &Account,
) {
let bal_account = self.accounts.entry(address).or_default();
bal_account.update(bal_index, account);
}
pub fn populate_account_info(
&self,
account_id: AccountId,
bal_index: BlockAccessIndex,
account: &mut AccountInfo,
) -> Result<bool, BalError> {
let Some((_, bal_account)) = self.accounts.get_index(account_id.get()) else {
return Err(BalError::InvalidAccountId { account_id });
};
account.account_id = Some(account_id);
Ok(bal_account.populate_account_info(bal_index, account))
}
#[inline]
pub fn populate_storage_slot_by_account_id(
&self,
account_id: AccountId,
bal_index: BlockAccessIndex,
key: StorageKey,
value: &mut StorageValue,
) -> Result<(), BalError> {
let Some((address, bal_account)) = self.accounts.get_index(account_id.get()) else {
return Err(BalError::InvalidAccountId { account_id });
};
if let Some(bal_value) = bal_account.storage.get(address, key, bal_index)? {
*value = bal_value;
};
Ok(())
}
#[inline]
pub fn populate_storage_slot(
&self,
account_address: Address,
bal_index: BlockAccessIndex,
key: StorageKey,
value: &mut StorageValue,
) -> Result<(), BalError> {
let Some(bal_account) = self.accounts.get(&account_address) else {
return Err(BalError::AccountNotFound {
address: account_address,
});
};
if let Some(bal_value) = bal_account.storage.get(&account_address, key, bal_index)? {
*value = bal_value;
};
Ok(())
}
pub fn account_storage(
&self,
account_id: AccountId,
key: StorageKey,
bal_index: BlockAccessIndex,
) -> Result<StorageValue, BalError> {
let Some((address, bal_account)) = self.accounts.get_index(account_id.get()) else {
return Err(BalError::InvalidAccountId { account_id });
};
let Some(storage_value) = bal_account.storage.get(address, key, bal_index)? else {
return Err(BalError::SlotNotFound {
address: *address,
slot: key,
});
};
Ok(storage_value)
}
pub fn into_alloy_bal(self) -> AlloyBal {
let mut alloy_bal = AlloyBal::from_iter(
self.accounts
.into_iter()
.map(|(address, account)| account.into_alloy_account(address)),
);
alloy_bal.sort_unstable_by_key(|a| a.address);
alloy_bal
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BalError {
AccountNotFound {
address: Address,
},
InvalidAccountId {
account_id: AccountId,
},
SlotNotFound {
address: Address,
slot: StorageKey,
},
}
impl core::fmt::Display for BalError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::AccountNotFound { address } => {
write!(f, "Account {address} not found in BAL")
}
Self::InvalidAccountId { account_id } => {
write!(f, "Invalid BAL account id {}", account_id.get())
}
Self::SlotNotFound { address, slot } => {
write!(f, "Slot {slot:#x} not found in BAL for account {address}")
}
}
}
}
impl core::error::Error for BalError {}
#[cfg(test)]
mod tests {
use super::*;
use alloy_eip7928::{
AccountChanges as AlloyAccountChanges, BalanceChange as AlloyBalanceChange,
CodeChange as AlloyCodeChange, NonceChange as AlloyNonceChange,
SlotChanges as AlloySlotChanges, StorageChange as AlloyStorageChange,
};
use bytecode::Bytecode;
use primitives::{Bytes, B256, U256};
use std::collections::BTreeMap;
fn code(byte: u8) -> (B256, Bytecode) {
let bytecode = Bytecode::new_raw(vec![byte].into());
(bytecode.hash_slow(), bytecode)
}
const fn idx(index: u64) -> BlockAccessIndex {
BlockAccessIndex::new(index)
}
#[test]
fn into_alloy_bal_canonicalizes_eip_7928_ordering() {
let low_address = Address::with_last_byte(1);
let high_address = Address::with_last_byte(2);
let unordered_account = AccountBal {
account_info: AccountInfoBal {
nonce: BalWrites {
writes: vec![(idx(9), 90), (idx(4), 40)],
},
balance: BalWrites {
writes: vec![(idx(5), U256::from(50)), (idx(2), U256::from(20))],
},
code: BalWrites {
writes: vec![(idx(7), code(7)), (idx(3), code(3))],
},
},
storage: StorageBal {
storage: BTreeMap::from([
(
U256::from(4),
BalWrites {
writes: vec![(idx(8), U256::from(80)), (idx(6), U256::from(60))],
},
),
(U256::from(1), BalWrites { writes: vec![] }),
(
U256::from(2),
BalWrites {
writes: vec![(idx(3), U256::from(30)), (idx(1), U256::from(10))],
},
),
(U256::from(3), BalWrites { writes: vec![] }),
]),
},
};
let alloy_bal = Bal::from_iter([
(high_address, AccountBal::default()),
(low_address, unordered_account),
])
.into_alloy_bal();
assert_eq!(
alloy_bal
.iter()
.map(|account| account.address)
.collect::<Vec<_>>(),
vec![low_address, high_address]
);
let account = &alloy_bal[0];
assert_eq!(account.storage_reads, vec![U256::from(1), U256::from(3)]);
assert_eq!(
account
.storage_changes
.iter()
.map(|slot| slot.slot)
.collect::<Vec<_>>(),
vec![U256::from(2), U256::from(4)]
);
assert_eq!(
account.storage_changes[0]
.changes
.iter()
.map(|change| change.block_access_index)
.collect::<Vec<_>>(),
vec![idx(1), idx(3)]
);
assert_eq!(
account.storage_changes[1]
.changes
.iter()
.map(|change| change.block_access_index)
.collect::<Vec<_>>(),
vec![idx(6), idx(8)]
);
assert_eq!(
account
.balance_changes
.iter()
.map(|change| change.block_access_index)
.collect::<Vec<_>>(),
vec![idx(2), idx(5)]
);
assert_eq!(
account
.nonce_changes
.iter()
.map(|change| change.block_access_index)
.collect::<Vec<_>>(),
vec![idx(4), idx(9)]
);
assert_eq!(
account
.code_changes
.iter()
.map(|change| change.block_access_index)
.collect::<Vec<_>>(),
vec![idx(3), idx(7)]
);
}
#[test]
fn try_from_alloy_decodes_block_access_list() {
let address = Address::with_last_byte(1);
let code_bytes = Bytes::from_static(&[0x60, 0x00]);
let alloy_bal = vec![AlloyAccountChanges {
address,
code_changes: vec![AlloyCodeChange::new(idx(1), code_bytes.clone())],
..Default::default()
}];
let bal = Bal::try_from_alloy(alloy_bal).unwrap();
let account = bal.accounts.get(&address).unwrap();
let (_, bytecode) = &account.account_info.code.writes[0].1;
assert_eq!(bytecode.original_bytes(), code_bytes);
}
#[test]
fn clone_from_alloy_matches_owned_conversion() {
let address = Address::with_last_byte(1);
let code_bytes = Bytes::from_static(&[0x60, 0x00]);
let alloy_bal = vec![AlloyAccountChanges {
address,
storage_changes: vec![AlloySlotChanges::new(
U256::from(1),
vec![AlloyStorageChange::new(idx(1), U256::from(10))],
)],
storage_reads: vec![U256::from(2)],
balance_changes: vec![AlloyBalanceChange::new(idx(2), U256::from(20))],
nonce_changes: vec![AlloyNonceChange::new(idx(3), 30)],
code_changes: vec![AlloyCodeChange::new(idx(4), code_bytes.clone())],
}];
let borrowed = Bal::clone_from_alloy(&alloy_bal).unwrap();
let owned = Bal::try_from_alloy(alloy_bal.clone()).unwrap();
assert_eq!(borrowed, owned);
assert_eq!(alloy_bal[0].code_changes[0].new_code(), &code_bytes);
}
#[test]
fn try_from_alloy_errors_on_invalid_code_change() {
let alloy_bal = vec![AlloyAccountChanges {
address: Address::with_last_byte(1),
code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
..Default::default()
}];
assert!(Bal::try_from_alloy(alloy_bal).is_err());
}
#[test]
fn clone_from_alloy_errors_on_invalid_code_change() {
let alloy_bal = vec![AlloyAccountChanges {
address: Address::with_last_byte(1),
code_changes: vec![AlloyCodeChange::new(idx(1), vec![0xef, 0x01, 0xde].into())],
..Default::default()
}];
assert!(Bal::clone_from_alloy(&alloy_bal).is_err());
}
}