Skip to main content

bal_codec/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! EIP-7928 Block-Level Access List codec.
4//!
5//! This crate is the *only* place the 7928 wire format is known. Everything
6//! above it (archive, layout, cli) consumes the typed structures and must not
7//! touch RLP.
8//!
9//! Wire schema (from the EIP, Review status, subject to change):
10//!
11//! ```text
12//! StorageChange   = [BlockAccessIndex(u32), StorageValue(u256)]
13//! BalanceChange   = [BlockAccessIndex(u32), Balance(u256)]
14//! NonceChange     = [BlockAccessIndex(u32), Nonce(u64)]
15//! CodeChange      = [BlockAccessIndex(u32), Bytecode(bytes)]
16//! SlotChanges     = [StorageKey(u256), [StorageChange...]]
17//! AccountChanges  = [Address, [SlotChanges...], [StorageKey...],
18//!                    [BalanceChange...], [NonceChange...], [CodeChange...]]
19//! BlockAccessList = [AccountChanges...]
20//! block_access_list_hash = keccak256(rlp(BlockAccessList))
21//! ```
22//!
23//! Ordering is part of validity: accounts by address, slots by key, changes by
24//! index, all strictly ascending. A BAL that violates ordering or uniqueness
25//! is rejected by [`BlockAccessList::decode`]; it is never softly accepted.
26//!
27//! With the `json` feature, [`BlockAccessList::from_rpc_json`] accepts the
28//! execution-apis JSON form served by `eth_getBlockAccessList`.
29
30#[cfg(feature = "json")]
31mod json;
32mod schema;
33mod validate;
34
35pub use schema::{
36    AccountChanges, BalanceChange, BlockAccessIndex, BlockAccessList, CodeChange, NonceChange,
37    SlotChanges, StorageChange,
38};
39
40use alloy_primitives::{keccak256, Address, B256, U256};
41use alloy_rlp::{Decodable, Encodable};
42
43/// Hash of the empty BAL: `keccak256(rlp([]))` = `keccak256(0xc0)`.
44pub const EMPTY_BAL_HASH: B256 = B256::new([
45    0x1d, 0xcc, 0x4d, 0xe8, 0xde, 0xc7, 0x5d, 0x7a, 0xab, 0x85, 0xb5, 0x67, 0xb6, 0xcc, 0xd4, 0x1a,
46    0xd3, 0x12, 0x45, 0x1b, 0x94, 0x8a, 0x74, 0x13, 0xf0, 0xa1, 0x42, 0xfd, 0x40, 0xd4, 0x93, 0x47,
47]);
48
49/// Decoding, validation and verification failures.
50#[derive(Debug, thiserror::Error)]
51pub enum CodecError {
52    /// Malformed RLP.
53    #[error("rlp: {0}")]
54    Rlp(#[from] alloy_rlp::Error),
55    /// Bytes left over after the outer list.
56    #[error("trailing bytes after BAL: {0} bytes")]
57    Trailing(usize),
58    /// A list is not strictly ascending.
59    #[error("ordering violated: {0}")]
60    Ordering(&'static str),
61    /// A key appears twice in a list.
62    #[error("duplicate {what} at position {pos}")]
63    Duplicate {
64        /// Which list.
65        what: &'static str,
66        /// Index of the repeated element.
67        pos: usize,
68    },
69    /// The EIP forbids a key in both `storage_changes` and `storage_reads`.
70    #[error("storage key appears in both storage_changes and storage_reads for {0}")]
71    KeyInChangesAndReads(Address),
72    /// A slot entry with no changes.
73    #[error("empty change list for slot {slot} of {address}")]
74    EmptySlotChanges {
75        /// Account.
76        address: Address,
77        /// Slot key.
78        slot: B256,
79    },
80    /// `keccak(rlp(bal))` differs from the header.
81    #[error("BAL hash mismatch: computed {computed}, expected {expected}")]
82    HashMismatch {
83        /// What this codec computed.
84        computed: B256,
85        /// What the header says.
86        expected: B256,
87    },
88    /// The JSON form did not match the expected shape (`json` feature).
89    #[error("json: {0}")]
90    Json(String),
91}
92
93impl BlockAccessList {
94    /// Decode from RLP and validate ordering/uniqueness. The whole input must
95    /// be consumed; trailing bytes are an error.
96    pub fn decode(mut bytes: &[u8]) -> Result<Self, CodecError> {
97        let bal = <Self as Decodable>::decode(&mut bytes)?;
98        if !bytes.is_empty() {
99            return Err(CodecError::Trailing(bytes.len()));
100        }
101        validate::validate(&bal)?;
102        Ok(bal)
103    }
104
105    /// Canonical RLP encoding.
106    pub fn encode_rlp(&self) -> Vec<u8> {
107        let mut out = Vec::with_capacity(self.length());
108        Encodable::encode(self, &mut out);
109        out
110    }
111
112    /// `keccak256(rlp(self))` — the value that lives in
113    /// `header.block_access_list_hash`.
114    pub fn hash(&self) -> B256 {
115        keccak256(self.encode_rlp())
116    }
117
118    /// Compare against the hash taken from the block header.
119    pub fn verify(&self, expected_bal_hash: B256) -> Result<(), CodecError> {
120        let computed = self.hash();
121        if computed == expected_bal_hash {
122            Ok(())
123        } else {
124            Err(CodecError::HashMismatch {
125                computed,
126                expected: expected_bal_hash,
127            })
128        }
129    }
130
131    /// Binary search by address (accounts are sorted by construction).
132    pub fn account(&self, addr: &Address) -> Option<&AccountChanges> {
133        self.accounts
134            .binary_search_by(|a| a.address.cmp(addr))
135            .ok()
136            .map(|i| &self.accounts[i])
137    }
138
139    /// `true` when no account was touched in the block.
140    pub fn is_empty(&self) -> bool {
141        self.accounts.is_empty()
142    }
143}
144
145impl AccountChanges {
146    /// Binary search by slot key.
147    pub fn slot(&self, key: &B256) -> Option<&SlotChanges> {
148        let k = U256::from_be_bytes(key.0);
149        self.storage_changes
150            .binary_search_by(|s| s.slot.cmp(&k))
151            .ok()
152            .map(|i| &self.storage_changes[i])
153    }
154
155    /// `true` iff the account has at least one storage write. Presence in the
156    /// BAL alone (reads, balance touches) does NOT count.
157    pub fn has_storage_changes(&self) -> bool {
158        !self.storage_changes.is_empty()
159    }
160}
161
162impl SlotChanges {
163    /// Slot key as a 32-byte word.
164    pub fn slot_b256(&self) -> B256 {
165        B256::from(self.slot.to_be_bytes::<32>())
166    }
167
168    /// Last write in block order — the end-of-block value.
169    ///
170    /// Validation guarantees `changes` is non-empty; a `SlotChanges` built by
171    /// hand with no changes yields a zero change rather than a panic.
172    pub fn final_change(&self) -> &StorageChange {
173        static EMPTY: StorageChange = StorageChange {
174            block_access_index: 0,
175            value: U256::ZERO,
176        };
177        self.changes.last().unwrap_or(&EMPTY)
178    }
179}
180
181impl StorageChange {
182    /// Post-value as a 32-byte word.
183    pub fn value_b256(&self) -> B256 {
184        B256::from(self.value.to_be_bytes::<32>())
185    }
186}