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    #[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        // Clearing an extension is a write, not an empty history.
343        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}