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//! - [`BalAccountInfo`]: account fields a BAL changed, each `None` when not written
10//! - [`BalAccountLookup`]: complete, partial, or uncovered account information at a read position
11//! - **`Bal`**: Main BAL structure containing a map of accounts
12//! - **`BalWrites<T>`**: Array of (index, value) pairs representing sequential writes to a state item
13//! - **`AccountBal`**: Complete BAL structure for an account (balance, nonce, code, and storage)
14//! - **`AccountInfoBal`**: Account info BAL data (nonce, balance, code)
15//! - **`StorageBal`**: Storage-level BAL data for an account
16
17pub 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/// BAL structure.
30#[derive(Debug, Default, Clone, PartialEq, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct Bal {
33    /// Accounts bal.
34    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    /// Create a new BAL builder.
47    pub fn new() -> Self {
48        Self {
49            accounts: AddressIndexMap::default(),
50        }
51    }
52
53    /// Pretty print the entire BAL structure in a human-readable format.
54    #[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        // Sort accounts by address before printing
66        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            // Print nonce writes
74            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            // Print balance writes
84            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            // Print code writes
94            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            // Print storage writes
109            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    /// Extend BAL with account.
134    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    /// Populate account from BAL. Return true if account info got changed.
145    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    /// Populate storage slot from BAL.
160    ///
161    /// If slot is not found in BAL, it will return an error.
162    #[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    /// Populate storage slot from BAL by account address.
182    #[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    /// Get storage from BAL.
203    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    /// Consume `Bal` and create a canonical EIP-7928 [`AlloyBal`].
224    ///
225    /// The returned access list is ordered deterministically: accounts are
226    /// sorted lexicographically by address, and each account's nested reads and
227    /// changes are sorted by [`AccountBal::into_alloy_account`].
228    ///
229    /// This matches the EIP-7928 ordering requirements:
230    /// <https://eips.ethereum.org/EIPS/eip-7928#ordering-uniqueness-and-determinism>.
231    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/// Account information available from a BAL at a read position.
243///
244/// Returned by [`AccountInfoBal::account_info_lookup`] and `BalState::get_bal_account_info`
245/// without consulting the backing database.
246#[derive(Clone, Debug, PartialEq, Eq)]
247pub enum BalAccountLookup {
248    /// Every account field is known, including decoded bytecode.
249    ///
250    /// This describes field completeness, not account existence. An empty account is returned as
251    /// an [`AccountInfo`]; the caller decides whether state-clearing rules make it absent.
252    Complete(AccountInfo),
253    /// Only some account fields are known. Missing fields must be read from the backing account.
254    ///
255    /// An empty [`BalAccountInfo`] means no account fields are known at this position; it does
256    /// not mean the account itself is empty or absent. Code changes carry their hash only.
257    Partial(BalAccountInfo),
258    /// No BAL is attached, or the account is missing and database fallback is enabled.
259    NotCovered,
260}
261
262/// Error returned when a BAL (Block Access List, [EIP-7928]) lookup
263/// cannot find data the caller expected to be present.
264///
265/// A BAL is supposed to enumerate every account and storage slot a block
266/// will touch, so when execution queries the BAL for an entry that is
267/// missing, the BAL is either malformed or being consulted for state that
268/// it does not cover. Each variant identifies which kind of lookup failed
269/// and carries the key that was queried so callers can report it.
270///
271/// Produced by [`Bal`] read paths ([`Bal::populate_account_info`],
272/// [`Bal::populate_storage_slot`], [`Bal::populate_storage_slot_by_account_id`],
273/// [`Bal::account_storage`], [`StorageBal::get`], [`StorageBal::get_bal_writes`])
274/// and surfaced through `BalState` / `BalDatabase` in `revm-database-interface`,
275/// where it is wrapped into `EvmDatabaseError::Bal` before reaching the EVM.
276///
277/// [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
278#[derive(Debug, Clone, PartialEq, Eq)]
279#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
280pub enum BalError {
281    /// The address was not present in the BAL's accounts map.
282    ///
283    /// Returned by address-keyed lookups (e.g. `BalState::get_account_id`,
284    /// `BalState::storage`, `Bal::populate_storage_slot`) when the BAL is
285    /// attached but does not list this account. Means the BAL is
286    /// incomplete for the access being attempted.
287    AccountNotFound {
288        /// Address that was not found.
289        address: Address,
290    },
291    /// The supplied [`AccountId`] index is out of range for the BAL's
292    /// accounts map.
293    ///
294    /// `AccountId`s are positional indices into the BAL — they are only
295    /// valid for the same BAL they were obtained from. This variant
296    /// indicates a stale or mismatched id was used (e.g. an id from a
297    /// different BAL, or one created before the current BAL was built).
298    InvalidAccountId {
299        /// Account id that was supplied.
300        account_id: AccountId,
301    },
302    /// The account exists in the BAL but the requested storage slot is not
303    /// listed under it.
304    ///
305    /// Returned by storage lookups when the account is covered by the BAL
306    /// yet this particular slot was not declared. As with
307    /// [`BalError::AccountNotFound`], this indicates the BAL is incomplete
308    /// for the access being attempted.
309    SlotNotFound {
310        /// Address of the account whose slot was missing.
311        address: Address,
312        /// Storage slot that was not found.
313        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        // Clearing an extension is a write, not an empty history.
365        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                // Post-execution for a two-transaction block.
572                (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        // The backing account may carry an extension the BAL never wrote.
656        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}