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/// Error returned when a BAL (Block Access List, [EIP-7928]) lookup
241/// cannot find data the caller expected to be present.
242///
243/// A BAL is supposed to enumerate every account and storage slot a block
244/// will touch, so when execution queries the BAL for an entry that is
245/// missing, the BAL is either malformed or being consulted for state that
246/// it does not cover. Each variant identifies which kind of lookup failed
247/// and carries the key that was queried so callers can report it.
248///
249/// Produced by [`Bal`] read paths ([`Bal::populate_account_info`],
250/// [`Bal::populate_storage_slot`], [`Bal::populate_storage_slot_by_account_id`],
251/// [`Bal::account_storage`], [`StorageBal::get`], [`StorageBal::get_bal_writes`])
252/// and surfaced through `BalState` / `BalDatabase` in `revm-database-interface`,
253/// where it is wrapped into `EvmDatabaseError::Bal` before reaching the EVM.
254///
255/// [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
256#[derive(Debug, Clone, PartialEq, Eq)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258pub enum BalError {
259    /// The address was not present in the BAL's accounts map.
260    ///
261    /// Returned by address-keyed lookups (e.g. `BalState::get_account_id`,
262    /// `BalState::storage`, `Bal::populate_storage_slot`) when the BAL is
263    /// attached but does not list this account. Means the BAL is
264    /// incomplete for the access being attempted.
265    AccountNotFound {
266        /// Address that was not found.
267        address: Address,
268    },
269    /// The supplied [`AccountId`] index is out of range for the BAL's
270    /// accounts map.
271    ///
272    /// `AccountId`s are positional indices into the BAL — they are only
273    /// valid for the same BAL they were obtained from. This variant
274    /// indicates a stale or mismatched id was used (e.g. an id from a
275    /// different BAL, or one created before the current BAL was built).
276    InvalidAccountId {
277        /// Account id that was supplied.
278        account_id: AccountId,
279    },
280    /// The account exists in the BAL but the requested storage slot is not
281    /// listed under it.
282    ///
283    /// Returned by storage lookups when the account is covered by the BAL
284    /// yet this particular slot was not declared. As with
285    /// [`BalError::AccountNotFound`], this indicates the BAL is incomplete
286    /// for the access being attempted.
287    SlotNotFound {
288        /// Address of the account whose slot was missing.
289        address: Address,
290        /// Storage slot that was not found.
291        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}