Skip to main content

alloy_eip7928/
validation.rs

1//! Validation for decoded EIP-7928 block access lists.
2
3use crate::{AccountChanges, BlockAccessIndex};
4use alloy_primitives::{Address, U256};
5
6/// A change list within an EIP-7928 account entry.
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub enum BlockAccessListChangeKind {
9    /// Storage changes for one slot.
10    Storage,
11    /// Account balance changes.
12    Balance,
13    /// Account nonce changes.
14    Nonce,
15    /// Account code changes.
16    Code,
17}
18
19impl core::fmt::Display for BlockAccessListChangeKind {
20    #[inline]
21    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22        f.write_str(match self {
23            Self::Storage => "storage",
24            Self::Balance => "balance",
25            Self::Nonce => "nonce",
26            Self::Code => "code",
27        })
28    }
29}
30
31/// Error returned when a decoded EIP-7928 block access list is invalid.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, thiserror::Error)]
33pub enum BlockAccessListValidationError {
34    /// The same account occurs more than once.
35    #[error("account {address} occurs more than once in the block access list")]
36    DuplicateAccount {
37        /// Duplicated account address.
38        address: Address,
39    },
40    /// Account entries are not ordered lexicographically by address.
41    #[error(
42        "block access list account {address} appears after {previous}, violating canonical order"
43    )]
44    AccountsOutOfOrder {
45        /// Previous account address.
46        previous: Address,
47        /// Out-of-order account address.
48        address: Address,
49    },
50    /// The same storage key occurs more than once or in both storage lists.
51    #[error("storage slot {slot:#x} occurs more than once for account {address}")]
52    DuplicateStorageKey {
53        /// Account containing the duplicated key.
54        address: Address,
55        /// Duplicated storage key.
56        slot: U256,
57    },
58    /// Storage keys in one of the account's lists are not in canonical order.
59    #[error(
60        "storage slot {slot:#x} appears after {previous:#x} for account {address}, violating canonical order"
61    )]
62    StorageKeysOutOfOrder {
63        /// Account containing the out-of-order key.
64        address: Address,
65        /// Previous storage key.
66        previous: U256,
67        /// Out-of-order storage key.
68        slot: U256,
69    },
70    /// A storage change entry has no changes.
71    #[error("storage slot {slot:#x} has an empty change list for account {address}")]
72    EmptyStorageChanges {
73        /// Account containing the empty entry.
74        address: Address,
75        /// Storage key with no changes.
76        slot: U256,
77    },
78    /// The same block access index occurs more than once in one change list.
79    #[error(
80        "block access index {index} occurs more than once in a {kind} change list for account {address}"
81    )]
82    DuplicateBlockAccessIndex {
83        /// Account containing the change list.
84        address: Address,
85        /// Kind of change list.
86        kind: BlockAccessListChangeKind,
87        /// Duplicated block access index.
88        index: BlockAccessIndex,
89    },
90    /// A change list is not ordered by block access index.
91    #[error(
92        "block access index {index} appears after {previous} in a {kind} change list for account {address}"
93    )]
94    ChangeIndicesOutOfOrder {
95        /// Account containing the change list.
96        address: Address,
97        /// Kind of change list.
98        kind: BlockAccessListChangeKind,
99        /// Previous block access index.
100        previous: BlockAccessIndex,
101        /// Out-of-order block access index.
102        index: BlockAccessIndex,
103    },
104    /// A block access index does not fit the EIP-7928 `uint32` representation.
105    #[error(
106        "block access index {index} in a {kind} change list for account {address} exceeds uint32"
107    )]
108    BlockAccessIndexOutOfRange {
109        /// Account containing the change list.
110        address: Address,
111        /// Kind of change list.
112        kind: BlockAccessListChangeKind,
113        /// Invalid block access index.
114        index: BlockAccessIndex,
115    },
116    /// A block access index is greater than the block's post-execution index.
117    #[error(
118        "block access index {index} in a {kind} change list for account {address} exceeds the block maximum {max}"
119    )]
120    BlockAccessIndexExceedsBlock {
121        /// Account containing the change list.
122        address: Address,
123        /// Kind of change list.
124        kind: BlockAccessListChangeKind,
125        /// Invalid block access index.
126        index: BlockAccessIndex,
127        /// Post-execution index for the block.
128        max: BlockAccessIndex,
129    },
130}
131
132/// Validates a decoded EIP-7928 block access list.
133///
134/// This enforces the ordering and uniqueness rules on accounts, storage keys, and change indices,
135/// requires every storage change entry to be non-empty, and checks that every block access index
136/// fits its `uint32` wire type and is at most the block's post-execution index
137/// (`transaction_count + 1`).
138pub fn validate_block_access_list(
139    block_access_list: &[AccountChanges],
140    transaction_count: usize,
141) -> Result<(), BlockAccessListValidationError> {
142    let max_block_access_index = BlockAccessIndex::new(
143        u64::try_from(transaction_count).unwrap_or(u64::MAX).saturating_add(1),
144    );
145    let mut previous_address = None;
146
147    for account in block_access_list {
148        validate_account_order(previous_address, account.address)?;
149        previous_address = Some(account.address);
150        validate_account(account, max_block_access_index)?;
151    }
152
153    Ok(())
154}
155
156fn validate_account(
157    account: &AccountChanges,
158    max_block_access_index: BlockAccessIndex,
159) -> Result<(), BlockAccessListValidationError> {
160    let address = account.address;
161    let mut previous_slot = None;
162    for slot_changes in &account.storage_changes {
163        validate_storage_key_order(address, &mut previous_slot, slot_changes.slot)?;
164        if slot_changes.changes.is_empty() {
165            return Err(BlockAccessListValidationError::EmptyStorageChanges {
166                address,
167                slot: slot_changes.slot,
168            });
169        }
170        validate_change_indices(
171            address,
172            BlockAccessListChangeKind::Storage,
173            slot_changes.changes.iter().map(|change| change.block_access_index),
174            max_block_access_index,
175        )?;
176    }
177
178    previous_slot = None;
179    for &slot in &account.storage_reads {
180        validate_storage_key_order(address, &mut previous_slot, slot)?;
181    }
182    validate_storage_disjointness(account)?;
183
184    validate_change_indices(
185        address,
186        BlockAccessListChangeKind::Balance,
187        account.balance_changes.iter().map(|change| change.block_access_index),
188        max_block_access_index,
189    )?;
190    validate_change_indices(
191        address,
192        BlockAccessListChangeKind::Nonce,
193        account.nonce_changes.iter().map(|change| change.block_access_index),
194        max_block_access_index,
195    )?;
196    validate_change_indices(
197        address,
198        BlockAccessListChangeKind::Code,
199        account.code_changes.iter().map(|change| change.block_access_index),
200        max_block_access_index,
201    )
202}
203
204fn validate_account_order(
205    previous: Option<Address>,
206    address: Address,
207) -> Result<(), BlockAccessListValidationError> {
208    if let Some(previous) = previous {
209        if previous == address {
210            return Err(BlockAccessListValidationError::DuplicateAccount { address });
211        }
212        if previous > address {
213            return Err(BlockAccessListValidationError::AccountsOutOfOrder { previous, address });
214        }
215    }
216    Ok(())
217}
218
219fn validate_storage_key_order(
220    address: Address,
221    previous: &mut Option<U256>,
222    slot: U256,
223) -> Result<(), BlockAccessListValidationError> {
224    if let Some(previous) = *previous {
225        if previous == slot {
226            return Err(BlockAccessListValidationError::DuplicateStorageKey { address, slot });
227        }
228        if previous > slot {
229            return Err(BlockAccessListValidationError::StorageKeysOutOfOrder {
230                address,
231                previous,
232                slot,
233            });
234        }
235    }
236    *previous = Some(slot);
237    Ok(())
238}
239
240fn validate_storage_disjointness(
241    account: &AccountChanges,
242) -> Result<(), BlockAccessListValidationError> {
243    let mut changes = account.storage_changes.iter().peekable();
244    let mut reads = account.storage_reads.iter().peekable();
245
246    while let (Some(change), Some(read)) = (changes.peek(), reads.peek()) {
247        match change.slot.cmp(read) {
248            core::cmp::Ordering::Less => {
249                changes.next();
250            }
251            core::cmp::Ordering::Greater => {
252                reads.next();
253            }
254            core::cmp::Ordering::Equal => {
255                return Err(BlockAccessListValidationError::DuplicateStorageKey {
256                    address: account.address,
257                    slot: **read,
258                });
259            }
260        }
261    }
262
263    Ok(())
264}
265
266fn validate_change_indices(
267    address: Address,
268    kind: BlockAccessListChangeKind,
269    indices: impl IntoIterator<Item = BlockAccessIndex>,
270    max: BlockAccessIndex,
271) -> Result<(), BlockAccessListValidationError> {
272    let mut previous = None;
273    for index in indices {
274        if index.get() > u32::MAX as u64 {
275            return Err(BlockAccessListValidationError::BlockAccessIndexOutOfRange {
276                address,
277                kind,
278                index,
279            });
280        }
281        if index > max {
282            return Err(BlockAccessListValidationError::BlockAccessIndexExceedsBlock {
283                address,
284                kind,
285                index,
286                max,
287            });
288        }
289        if let Some(previous) = previous {
290            if previous == index {
291                return Err(BlockAccessListValidationError::DuplicateBlockAccessIndex {
292                    address,
293                    kind,
294                    index,
295                });
296            }
297            if previous > index {
298                return Err(BlockAccessListValidationError::ChangeIndicesOutOfOrder {
299                    address,
300                    kind,
301                    previous,
302                    index,
303                });
304            }
305        }
306        previous = Some(index);
307    }
308    Ok(())
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::{BalanceChange, CodeChange, NonceChange, SlotChanges, StorageChange};
315    use alloc::vec;
316    use alloy_primitives::{Bytes, U256};
317
318    const fn index(value: u64) -> BlockAccessIndex {
319        BlockAccessIndex::new(value)
320    }
321
322    fn account(address: u8) -> AccountChanges {
323        AccountChanges {
324            address: Address::with_last_byte(address),
325            storage_changes: vec![SlotChanges::new(
326                U256::from(1),
327                vec![StorageChange::new(index(0), U256::from(10))],
328            )],
329            storage_reads: vec![U256::from(2)],
330            balance_changes: vec![BalanceChange::new(index(1), U256::from(20))],
331            nonce_changes: vec![NonceChange::new(index(2), 1)],
332            code_changes: vec![CodeChange::new(index(3), Bytes::new())],
333        }
334    }
335
336    #[test]
337    fn accepts_canonical_block_access_list() {
338        let bal = crate::bal::Bal::new(vec![account(1), account(2)]);
339
340        assert_eq!(validate_block_access_list(bal.as_slice(), 2), Ok(()));
341        assert_eq!(bal.validate_structure(2), Ok(()));
342    }
343
344    #[test]
345    fn rejects_duplicate_and_out_of_order_accounts() {
346        let address = Address::with_last_byte(2);
347        assert_eq!(
348            validate_block_access_list(&[account(2), account(2)], 2),
349            Err(BlockAccessListValidationError::DuplicateAccount { address })
350        );
351
352        let previous = Address::with_last_byte(2);
353        let address = Address::with_last_byte(1);
354        assert_eq!(
355            validate_block_access_list(&[account(2), account(1)], 2),
356            Err(BlockAccessListValidationError::AccountsOutOfOrder { previous, address })
357        );
358    }
359
360    #[test]
361    fn rejects_invalid_storage_entries() {
362        let address = Address::with_last_byte(1);
363        let slot = U256::from(1);
364        let mut empty = account(1);
365        empty.storage_changes[0].changes.clear();
366        assert_eq!(
367            validate_block_access_list(&[empty], 2),
368            Err(BlockAccessListValidationError::EmptyStorageChanges { address, slot })
369        );
370
371        let mut duplicate = account(1);
372        duplicate.storage_reads.insert(0, slot);
373        assert_eq!(
374            validate_block_access_list(&[duplicate], 2),
375            Err(BlockAccessListValidationError::DuplicateStorageKey { address, slot })
376        );
377
378        let mut out_of_order = account(1);
379        out_of_order.storage_reads = vec![U256::from(3), U256::from(2)];
380        assert_eq!(
381            validate_block_access_list(&[out_of_order], 2),
382            Err(BlockAccessListValidationError::StorageKeysOutOfOrder {
383                address,
384                previous: U256::from(3),
385                slot: U256::from(2),
386            })
387        );
388    }
389
390    #[test]
391    fn rejects_invalid_change_indices() {
392        let address = Address::with_last_byte(1);
393        let mut duplicate = account(1);
394        duplicate.balance_changes.push(BalanceChange::new(index(1), U256::from(30)));
395        assert_eq!(
396            validate_block_access_list(&[duplicate], 2),
397            Err(BlockAccessListValidationError::DuplicateBlockAccessIndex {
398                address,
399                kind: BlockAccessListChangeKind::Balance,
400                index: index(1),
401            })
402        );
403
404        let mut out_of_order = account(1);
405        out_of_order.nonce_changes =
406            vec![NonceChange::new(index(2), 1), NonceChange::new(index(1), 2)];
407        assert_eq!(
408            validate_block_access_list(&[out_of_order], 2),
409            Err(BlockAccessListValidationError::ChangeIndicesOutOfOrder {
410                address,
411                kind: BlockAccessListChangeKind::Nonce,
412                previous: index(2),
413                index: index(1),
414            })
415        );
416
417        let mut exceeds_u32 = account(1);
418        exceeds_u32.code_changes[0].block_access_index = index(u32::MAX as u64 + 1);
419        assert_eq!(
420            validate_block_access_list(&[exceeds_u32], usize::MAX),
421            Err(BlockAccessListValidationError::BlockAccessIndexOutOfRange {
422                address,
423                kind: BlockAccessListChangeKind::Code,
424                index: index(u32::MAX as u64 + 1),
425            })
426        );
427
428        let mut exceeds_block = account(1);
429        exceeds_block.storage_changes[0].changes[0].block_access_index = index(4);
430        assert_eq!(
431            validate_block_access_list(&[exceeds_block], 2),
432            Err(BlockAccessListValidationError::BlockAccessIndexExceedsBlock {
433                address,
434                kind: BlockAccessListChangeKind::Storage,
435                index: index(4),
436                max: index(3),
437            })
438        );
439    }
440}