Skip to main content

alloy_eip7928/
block_access_list.rs

1//! Contains the [`BlockAccessList`] type, which represents a simple list of account changes.
2
3use crate::account_changes::AccountChanges;
4use alloc::vec::Vec;
5
6#[cfg(not(feature = "std"))]
7use once_cell::race::OnceBox as OnceLock;
8#[cfg(feature = "std")]
9use std::sync::OnceLock;
10
11/// This struct is used to store `account_changes` in a block.
12pub type BlockAccessList = Vec<AccountChanges>;
13
14/// Computes the hash of the given block access list.
15#[cfg(feature = "rlp")]
16pub fn compute_block_access_list_hash(bal: &[AccountChanges]) -> alloy_primitives::B256 {
17    let mut buf = Vec::new();
18    alloy_rlp::encode_list(bal, &mut buf);
19    alloy_primitives::keccak256(&buf)
20}
21
22/// Computes the total number of items in the block access list, counting each account and unique
23/// storage slot.
24pub fn total_bal_items(bal: &[AccountChanges]) -> u64 {
25    let mut bal_items: u64 = 0;
26
27    for account in bal {
28        // Count address
29        bal_items += 1;
30
31        // Collect unique storage slots across reads + writes
32        let mut unique_slots = alloy_primitives::map::HashSet::new();
33
34        for change in account.storage_changes() {
35            unique_slots.insert(change.slot);
36        }
37
38        for slot in account.storage_reads() {
39            unique_slots.insert(*slot);
40        }
41
42        // Count unique storage keys
43        bal_items += unique_slots.len() as u64;
44    }
45    bal_items
46}
47
48/// Block-Level Access List wrapper type with helper methods for metrics and validation.
49pub mod bal {
50    use super::OnceLock;
51    use crate::account_changes::AccountChanges;
52    use alloc::vec::{IntoIter, Vec};
53    use alloy_primitives::Bytes;
54    use core::{
55        ops::{Deref, Index},
56        slice::Iter,
57    };
58
59    /// A wrapper around [`Vec<AccountChanges>`] that provides helper methods for
60    /// computing metrics and statistics about the block access list.
61    ///
62    /// This type implements `Deref` to `[AccountChanges]` for easy access to the
63    /// underlying data while providing additional utility methods for BAL analysis.
64    #[derive(Clone, Debug, Default, PartialEq, Eq)]
65    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66    #[cfg_attr(
67        feature = "rlp",
68        derive(alloy_rlp::RlpEncodableWrapper, alloy_rlp::RlpDecodableWrapper)
69    )]
70    #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
71    #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
72    pub struct Bal(Vec<AccountChanges>);
73
74    impl From<Bal> for Vec<AccountChanges> {
75        fn from(this: Bal) -> Self {
76            this.0
77        }
78    }
79
80    impl From<Vec<AccountChanges>> for Bal {
81        fn from(list: Vec<AccountChanges>) -> Self {
82            Self(list)
83        }
84    }
85
86    #[cfg(feature = "rlp")]
87    impl alloy_primitives::Sealable for Bal {
88        fn hash_slow(&self) -> alloy_primitives::B256 {
89            self.compute_hash()
90        }
91    }
92
93    impl Deref for Bal {
94        type Target = [AccountChanges];
95
96        fn deref(&self) -> &Self::Target {
97            self.as_slice()
98        }
99    }
100
101    impl IntoIterator for Bal {
102        type Item = AccountChanges;
103        type IntoIter = IntoIter<AccountChanges>;
104
105        fn into_iter(self) -> Self::IntoIter {
106            self.0.into_iter()
107        }
108    }
109
110    impl<'a> IntoIterator for &'a Bal {
111        type Item = &'a AccountChanges;
112        type IntoIter = Iter<'a, AccountChanges>;
113
114        fn into_iter(self) -> Self::IntoIter {
115            self.iter()
116        }
117    }
118
119    impl FromIterator<AccountChanges> for Bal {
120        fn from_iter<I: IntoIterator<Item = AccountChanges>>(iter: I) -> Self {
121            Self(iter.into_iter().collect())
122        }
123    }
124
125    impl<I> Index<I> for Bal
126    where
127        I: core::slice::SliceIndex<[AccountChanges]>,
128    {
129        type Output = I::Output;
130
131        #[inline]
132        fn index(&self, index: I) -> &Self::Output {
133            &self.0[index]
134        }
135    }
136
137    impl Bal {
138        /// Creates a new [`Bal`] from the provided account changes.
139        pub const fn new(account_changes: Vec<AccountChanges>) -> Self {
140            Self(account_changes)
141        }
142
143        /// Adds a new [`AccountChanges`] entry to the list.
144        pub fn push(&mut self, account_changes: AccountChanges) {
145            self.0.push(account_changes)
146        }
147
148        /// Returns `true` if the list contains no elements.
149        #[inline]
150        pub const fn is_empty(&self) -> bool {
151            self.0.is_empty()
152        }
153
154        /// Returns the number of account change entries contained in the list.
155        #[inline]
156        pub const fn len(&self) -> usize {
157            self.0.len()
158        }
159
160        /// Returns an iterator over the [`AccountChanges`] entries.
161        #[inline]
162        pub fn iter(&self) -> Iter<'_, AccountChanges> {
163            self.0.iter()
164        }
165
166        /// Returns a slice of the contained [`AccountChanges`].
167        #[inline]
168        pub const fn as_slice(&self) -> &[AccountChanges] {
169            self.0.as_slice()
170        }
171
172        /// Returns a vector of [`AccountChanges`].
173        pub fn into_inner(self) -> Vec<AccountChanges> {
174            self.0
175        }
176
177        /// Sorts this block access list in-place according to the canonical EIP-7928 ordering
178        /// rules.
179        ///
180        /// This applies the ordering required by the "Ordering, Uniqueness and Determinism"
181        /// section of EIP-7928:
182        ///
183        /// - accounts are sorted lexicographically by address
184        /// - `storage_changes` are sorted lexicographically by storage key
185        /// - each per-slot `StorageChange` list is sorted by block access index in ascending order
186        /// - `storage_reads` are sorted lexicographically by storage key
187        /// - `balance_changes`, `nonce_changes`, and `code_changes` are sorted by block access
188        ///   index in ascending order
189        ///
190        /// The account-local ordering is delegated to [`AccountChanges::sort`], so callers may
191        /// sort account internals independently when a parallel sort strategy is useful.
192        ///
193        /// This method only canonicalizes ordering. It does not enforce the EIP-7928 uniqueness
194        /// constraints for accounts, storage keys, or block access indexes.
195        pub fn sort(&mut self) {
196            self.0.sort_unstable_by_key(|account| account.address);
197
198            for account in &mut self.0 {
199                account.sort();
200            }
201        }
202
203        /// Returns the total number of accounts with changes in this BAL.
204        #[inline]
205        pub const fn account_count(&self) -> usize {
206            self.0.len()
207        }
208
209        /// Returns the total number of storage changes across all accounts.
210        pub fn total_storage_changes(&self) -> usize {
211            self.0.iter().map(|a| a.storage_changes.len()).sum()
212        }
213
214        /// Returns the total number of storage reads across all accounts.
215        pub fn total_storage_reads(&self) -> usize {
216            self.0.iter().map(|a| a.storage_reads.len()).sum()
217        }
218
219        /// Returns the total number of storage slots (both changes and reads) across all accounts.
220        pub fn total_slots(&self) -> usize {
221            self.0.iter().map(|a| a.storage_changes.len() + a.storage_reads.len()).sum()
222        }
223
224        /// Returns the total number of balance changes across all accounts.
225        pub fn total_balance_changes(&self) -> usize {
226            self.0.iter().map(|a| a.balance_changes.len()).sum()
227        }
228
229        /// Returns the total number of nonce changes across all accounts.
230        pub fn total_nonce_changes(&self) -> usize {
231            self.0.iter().map(|a| a.nonce_changes.len()).sum()
232        }
233
234        /// Returns the total number of code changes across all accounts.
235        pub fn total_code_changes(&self) -> usize {
236            self.0.iter().map(|a| a.code_changes.len()).sum()
237        }
238
239        /// Returns a summary of all change counts for metrics reporting.
240        pub fn change_counts(&self) -> BalChangeCounts {
241            let mut counts = BalChangeCounts::default();
242            for account in &self.0 {
243                counts.accounts += 1;
244                counts.storage += account.storage_changes.len();
245                counts.balance += account.balance_changes.len();
246                counts.nonce += account.nonce_changes.len();
247                counts.code += account.code_changes.len();
248            }
249            counts
250        }
251
252        /// Computes the total number of items in this block access list, counting each account and
253        /// unique storage slot.
254        pub fn total_bal_items(&self) -> u64 {
255            super::total_bal_items(&self.0)
256        }
257
258        /// Computes the hash of this block access list.
259        #[cfg(feature = "rlp")]
260        pub fn compute_hash(&self) -> alloy_primitives::B256 {
261            if self.0.is_empty() {
262                return crate::constants::EMPTY_BLOCK_ACCESS_LIST_HASH;
263            }
264            super::compute_block_access_list_hash(&self.0)
265        }
266    }
267
268    /// Summary of change counts in a BAL for metrics reporting.
269    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
270    pub struct BalChangeCounts {
271        /// Number of accounts with changes.
272        pub accounts: usize,
273        /// Total number of storage changes.
274        pub storage: usize,
275        /// Total number of balance changes.
276        pub balance: usize,
277        /// Total number of nonce changes.
278        pub nonce: usize,
279        /// Total number of code changes.
280        pub code: usize,
281    }
282
283    /// A decoded block access list with lazy hash computation.
284    ///
285    /// This type wraps a decoded [`Bal`] along with the original raw RLP bytes,
286    /// allowing efficient hash computation on demand without re-encoding.
287    #[derive(Clone, Debug)]
288    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
289    pub struct DecodedBal {
290        /// The decoded block access list.
291        decoded: Bal,
292        /// The original raw RLP bytes.
293        raw: Bytes,
294        /// Lazily computed hash of the block access list.
295        #[cfg_attr(feature = "serde", serde(skip, default))]
296        hash: OnceLock<alloy_primitives::B256>,
297    }
298
299    impl PartialEq for DecodedBal {
300        fn eq(&self, other: &Self) -> bool {
301            self.decoded == other.decoded && self.raw == other.raw
302        }
303    }
304
305    impl Eq for DecodedBal {}
306
307    impl DecodedBal {
308        /// Creates a new [`DecodedBal`] from decoded data and raw bytes.
309        pub const fn new(decoded: Bal, raw: Bytes) -> Self {
310            Self { decoded, raw, hash: OnceLock::new() }
311        }
312
313        /// Creates a new [`DecodedBal`] by decoding from raw RLP bytes.
314        #[cfg(feature = "rlp")]
315        pub fn from_rlp_bytes(raw: Bytes) -> Result<Self, alloy_rlp::Error> {
316            let mut slice = raw.as_ref();
317            let decoded = <Bal as alloy_rlp::Decodable>::decode(&mut slice)?;
318            if !slice.is_empty() {
319                return Err(alloy_rlp::Error::UnexpectedLength);
320            }
321            Ok(Self::new(decoded, raw))
322        }
323
324        /// Returns a reference to the decoded block access list.
325        pub const fn as_bal(&self) -> &Bal {
326            &self.decoded
327        }
328
329        /// Returns the original raw RLP bytes.
330        pub const fn as_raw(&self) -> &Bytes {
331            &self.raw
332        }
333
334        /// Returns the decoded BAL as a sealed borrowed value.
335        #[cfg(feature = "rlp")]
336        pub fn as_sealed_bal(&self) -> alloy_primitives::Sealed<&Bal> {
337            alloy_primitives::Sealable::seal_ref_unchecked(&self.decoded, self.hash())
338        }
339
340        /// Splits this struct into the decoded BAL and raw bytes.
341        pub fn split(self) -> (Bal, Bytes) {
342            (self.decoded, self.raw)
343        }
344
345        /// Splits this struct into the decoded BAL, raw bytes, and hash.
346        pub fn into_parts(self) -> (Bal, Bytes, alloy_primitives::B256) {
347            let hash = self.hash();
348            let (decoded, raw) = self.split();
349            (decoded, raw, hash)
350        }
351
352        /// Consumes this struct and returns the decoded BAL together with its hash.
353        #[cfg(feature = "rlp")]
354        pub fn into_sealed(self) -> alloy_primitives::Sealed<Bal> {
355            let seal = self.hash();
356            let (decoded, _) = self.split();
357            alloy_primitives::Sealable::seal_unchecked(decoded, seal)
358        }
359
360        /// Returns the hash of this block access list.
361        ///
362        /// The hash is computed lazily on first call and cached for subsequent calls.
363        pub fn hash(&self) -> alloy_primitives::B256 {
364            #[allow(clippy::useless_conversion)]
365            *self.hash.get_or_init(|| alloy_primitives::keccak256(self.raw.as_ref()).into())
366        }
367    }
368
369    #[cfg(feature = "rlp")]
370    impl alloy_rlp::Decodable for DecodedBal {
371        fn decode(buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
372            let original = *buf;
373            let decoded = <Bal as alloy_rlp::Decodable>::decode(buf)?;
374            let consumed = original.len() - buf.len();
375            let raw = Bytes::copy_from_slice(&original[..consumed]);
376            Ok(Self::new(decoded, raw))
377        }
378    }
379
380    #[cfg(feature = "rlp")]
381    impl alloy_rlp::Encodable for DecodedBal {
382        fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
383            out.put_slice(&self.raw);
384        }
385
386        fn length(&self) -> usize {
387            self.raw.len()
388        }
389    }
390}
391
392#[cfg(test)]
393mod hash_tests {
394    use super::bal::{Bal, DecodedBal};
395    use crate::{
396        AccountChanges, BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges,
397        StorageChange,
398    };
399    use alloy_primitives::{Address, Bytes, U256};
400
401    #[test]
402    fn decoded_bal_hash_uses_raw_bytes_without_rlp_feature() {
403        let raw = Bytes::from_static(&[0xc0]);
404        let decoded = DecodedBal::new(Bal::default(), raw.clone());
405
406        assert_eq!(decoded.hash(), alloy_primitives::keccak256(raw.as_ref()));
407
408        let (bal, split_raw, split_hash) = decoded.into_parts();
409        assert!(bal.is_empty());
410        assert_eq!(split_raw, raw);
411        assert_eq!(split_hash, alloy_primitives::keccak256(raw.as_ref()));
412    }
413
414    #[test]
415    fn bal_sort_orders_all_eip7928_lists() {
416        let address_1 = Address::from([0x11; 20]);
417        let address_2 = Address::from([0x22; 20]);
418        let mut bal = Bal::new(vec![
419            AccountChanges {
420                address: address_2,
421                storage_changes: vec![
422                    SlotChanges::new(
423                        U256::from(3),
424                        vec![
425                            StorageChange::new(BlockAccessIndex::new(8), U256::from(0x80)),
426                            StorageChange::new(BlockAccessIndex::new(2), U256::from(0x20)),
427                        ],
428                    ),
429                    SlotChanges::new(
430                        U256::from(1),
431                        vec![
432                            StorageChange::new(BlockAccessIndex::new(5), U256::from(0x50)),
433                            StorageChange::new(BlockAccessIndex::new(1), U256::from(0x10)),
434                        ],
435                    ),
436                ],
437                storage_reads: vec![U256::from(4), U256::from(2)],
438                balance_changes: vec![
439                    BalanceChange::new(BlockAccessIndex::new(6), U256::from(600)),
440                    BalanceChange::new(BlockAccessIndex::new(3), U256::from(300)),
441                ],
442                nonce_changes: vec![
443                    NonceChange::new(BlockAccessIndex::new(7), 70),
444                    NonceChange::new(BlockAccessIndex::new(4), 40),
445                ],
446                code_changes: vec![
447                    CodeChange::new(BlockAccessIndex::new(9), Bytes::from_static(&[0x60, 0x09])),
448                    CodeChange::new(BlockAccessIndex::new(5), Bytes::from_static(&[0x60, 0x05])),
449                ],
450            },
451            AccountChanges {
452                address: address_1,
453                storage_changes: vec![
454                    SlotChanges::new(
455                        U256::from(2),
456                        vec![
457                            StorageChange::new(BlockAccessIndex::new(4), U256::from(0x40)),
458                            StorageChange::new(BlockAccessIndex::new(0), U256::from(0x00)),
459                        ],
460                    ),
461                    SlotChanges::new(
462                        U256::from(1),
463                        vec![
464                            StorageChange::new(BlockAccessIndex::new(3), U256::from(0x30)),
465                            StorageChange::new(BlockAccessIndex::new(1), U256::from(0x10)),
466                        ],
467                    ),
468                ],
469                storage_reads: vec![U256::from(5), U256::from(3)],
470                balance_changes: vec![
471                    BalanceChange::new(BlockAccessIndex::new(5), U256::from(500)),
472                    BalanceChange::new(BlockAccessIndex::new(2), U256::from(200)),
473                ],
474                nonce_changes: vec![
475                    NonceChange::new(BlockAccessIndex::new(8), 80),
476                    NonceChange::new(BlockAccessIndex::new(1), 10),
477                ],
478                code_changes: vec![
479                    CodeChange::new(BlockAccessIndex::new(4), Bytes::from_static(&[0x60, 0x04])),
480                    CodeChange::new(BlockAccessIndex::new(2), Bytes::from_static(&[0x60, 0x02])),
481                ],
482            },
483        ]);
484
485        bal.sort();
486
487        assert_eq!(bal[0].address, address_1);
488        assert_eq!(bal[1].address, address_2);
489
490        for account in bal.iter() {
491            assert!(account.storage_changes.windows(2).all(|slots| slots[0].slot <= slots[1].slot));
492            for slot_changes in &account.storage_changes {
493                assert!(
494                    slot_changes
495                        .changes
496                        .windows(2)
497                        .all(|changes| changes[0].block_access_index
498                            <= changes[1].block_access_index)
499                );
500            }
501            assert!(account.storage_reads.windows(2).all(|slots| slots[0] <= slots[1]));
502            assert!(
503                account
504                    .balance_changes
505                    .windows(2)
506                    .all(|changes| changes[0].block_access_index <= changes[1].block_access_index)
507            );
508            assert!(
509                account
510                    .nonce_changes
511                    .windows(2)
512                    .all(|changes| changes[0].block_access_index <= changes[1].block_access_index)
513            );
514            assert!(
515                account
516                    .code_changes
517                    .windows(2)
518                    .all(|changes| changes[0].block_access_index <= changes[1].block_access_index)
519            );
520        }
521    }
522}
523
524#[cfg(all(test, feature = "rlp"))]
525mod tests {
526    use super::bal::{Bal, DecodedBal};
527    use crate::{
528        AccountChanges, BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges,
529        StorageChange, constants::EMPTY_BLOCK_ACCESS_LIST_HASH,
530    };
531    use alloy_primitives::{Address, Bytes, U256};
532
533    fn sample_bal() -> Bal {
534        Bal::new(vec![
535            AccountChanges::new(Address::from([0x11; 20]))
536                .with_storage_read(U256::from(0x10))
537                .with_storage_change(SlotChanges::new(
538                    U256::from(0x01),
539                    vec![StorageChange::new(BlockAccessIndex::new(0), U256::from(0xaa))],
540                ))
541                .with_balance_change(BalanceChange::new(
542                    BlockAccessIndex::new(1),
543                    U256::from(1_000),
544                ))
545                .with_nonce_change(NonceChange::new(BlockAccessIndex::new(2), 7))
546                .with_code_change(CodeChange::new(
547                    BlockAccessIndex::new(3),
548                    Bytes::from(vec![0x60, 0x00]),
549                )),
550            AccountChanges::new(Address::from([0x22; 20]))
551                .with_storage_read(U256::from(0x20))
552                .with_storage_change(SlotChanges::new(
553                    U256::from(0x02),
554                    vec![StorageChange::new(BlockAccessIndex::new(4), U256::from(0xbb))],
555                )),
556        ])
557    }
558
559    #[test]
560    fn bal_compute_hash_returns_empty_hash_for_empty_bal() {
561        let bal = Bal::default();
562
563        assert_eq!(bal.compute_hash(), EMPTY_BLOCK_ACCESS_LIST_HASH);
564    }
565
566    #[test]
567    fn bal_compute_hash_matches_free_function_for_non_empty_bal() {
568        let bal = sample_bal();
569
570        assert_eq!(bal.compute_hash(), super::compute_block_access_list_hash(bal.as_slice()));
571        assert_ne!(bal.compute_hash(), EMPTY_BLOCK_ACCESS_LIST_HASH);
572    }
573
574    #[test]
575    fn decoded_bal_from_rlp_bytes_preserves_raw_and_hash() {
576        let bal = sample_bal();
577        let raw = Bytes::from(alloy_rlp::encode(&bal));
578        let decoded = DecodedBal::from_rlp_bytes(raw.clone()).unwrap();
579
580        assert_eq!(decoded.as_bal(), &bal);
581        assert_eq!(decoded.as_raw(), &raw);
582        assert_eq!(decoded.hash(), bal.compute_hash());
583        assert_eq!(decoded.hash(), alloy_primitives::keccak256(raw.as_ref()));
584        assert_eq!(decoded.as_sealed_bal().hash(), bal.compute_hash());
585        assert_eq!(decoded.as_sealed_bal().inner(), &decoded.as_bal());
586
587        let (split_bal, split_raw) = decoded.clone().split();
588        assert_eq!(split_bal, bal);
589        assert_eq!(split_raw, raw);
590
591        let (split_bal, split_raw, split_hash) = decoded.clone().into_parts();
592        assert_eq!(split_bal, bal);
593        assert_eq!(split_raw, raw);
594        assert_eq!(split_hash, bal.compute_hash());
595
596        let sealed = decoded.into_sealed();
597        assert_eq!(sealed.hash(), bal.compute_hash());
598        assert_eq!(sealed.inner(), &bal);
599    }
600
601    #[test]
602    fn decoded_bal_decode_consumes_exact_raw_rlp_item() {
603        let bal = sample_bal();
604        let raw = alloy_rlp::encode(&bal);
605        let mut buf = raw.as_ref();
606        let decoded = <DecodedBal as alloy_rlp::Decodable>::decode(&mut buf).unwrap();
607
608        assert!(buf.is_empty());
609        assert_eq!(decoded.as_bal(), &bal);
610        assert_eq!(decoded.as_raw().as_ref(), raw.as_slice());
611        assert_eq!(alloy_rlp::encode(&decoded), raw);
612    }
613}