Skip to main content

revm_state/
bal.rs

1//! Block Access List (BAL) data structures for efficient state access in blockchain execution.
2//!
3//! This module provides types for managing Block Access Lists, which optimize state access
4//! by pre-computing and organizing data that will be accessed during block execution.
5//!
6//! ## Key Types
7//!
8//! - [`BlockAccessIndex`]: block access index
9//! - **`Bal`**: Main BAL structure containing a map of accounts
10//! - **`BalWrites<T>`**: Array of (index, value) pairs representing sequential writes to a state item
11//! - **`AccountBal`**: Complete BAL structure for an account (balance, nonce, code, and storage)
12//! - **`AccountInfoBal`**: Account info BAL data (nonce, balance, code)
13//! - **`StorageBal`**: Storage-level BAL data for an account
14
15pub 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/// BAL structure.
28#[derive(Debug, Default, Clone, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct Bal {
31    /// Accounts bal.
32    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    /// Create a new BAL builder.
45    pub fn new() -> Self {
46        Self {
47            accounts: AddressIndexMap::default(),
48        }
49    }
50
51    /// Pretty print the entire BAL structure in a human-readable format.
52    #[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        // Sort accounts by address before printing
64        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            // Print nonce writes
72            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            // Print balance writes
82            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            // Print code writes
92            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            // Print storage writes
107            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    /// Extend BAL with account.
132    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    /// Populate account from BAL. Return true if account info got changed.
143    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    /// Populate storage slot from BAL.
158    ///
159    /// If slot is not found in BAL, it will return an error.
160    #[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    /// Populate storage slot from BAL by account address.
180    #[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    /// Get storage from BAL.
201    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    /// Consume `Bal` and create a canonical EIP-7928 [`AlloyBal`].
222    ///
223    /// The returned access list is ordered deterministically: accounts are
224    /// sorted lexicographically by address, and each account's nested reads and
225    /// changes are sorted by [`AccountBal::into_alloy_account`].
226    ///
227    /// This matches the EIP-7928 ordering requirements:
228    /// <https://eips.ethereum.org/EIPS/eip-7928#ordering-uniqueness-and-determinism>.
229    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/// BAL error.
241#[derive(Debug, Clone, PartialEq, Eq)]
242#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
243pub enum BalError {
244    /// Account not found in BAL by address.
245    AccountNotFound {
246        /// Address that was not found.
247        address: Address,
248    },
249    /// Account id does not point at a valid entry in the BAL accounts map.
250    ///
251    /// Signals that a stale or mismatched id was supplied — the id is expected to come
252    /// from a prior BAL lookup against the same BAL.
253    InvalidAccountId {
254        /// Account id that was supplied.
255        account_id: AccountId,
256    },
257    /// Slot not found in BAL for a given account.
258    SlotNotFound {
259        /// Address of the account whose slot was missing.
260        address: Address,
261        /// Storage slot that was not found.
262        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}