Skip to main content

alloy_eip7928/
account_info.rs

1//! Contains the [`BalAccountInfo`] struct, which holds the account-level fields an
2//! [`AccountChanges`] entry commits to at the end of the block.
3
4use crate::AccountChanges;
5use alloy_primitives::{B256, U256};
6
7/// The post-block account-level state recorded by an [`AccountChanges`] entry.
8///
9/// A block access list only records the fields a block actually changed, so every field here is
10/// `Some` only when the entry carries a change for it. Fields left `None` are unchanged by the
11/// block and must be taken from the account as it was before the block.
12///
13/// Values are read through the `*_post_state` accessors of [`AccountChanges`], which take the last
14/// recorded change ("last write wins"). This matches canonical EIP-7928 ordering; call
15/// [`AccountChanges::sort`] first if the entry may be out of order.
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
17pub struct BalAccountInfo {
18    /// The post-block balance, if the block changed it.
19    pub balance: Option<U256>,
20    /// The post-block nonce, if the block changed it.
21    pub nonce: Option<u64>,
22    /// The hash of the post-block code, if the block changed the code.
23    ///
24    /// [`KECCAK256_EMPTY`](alloy_primitives::KECCAK256_EMPTY) means the code was set to empty,
25    /// which is distinct from `None`.
26    pub code_hash: Option<B256>,
27}
28
29impl BalAccountInfo {
30    /// Extracts the post-block account-level fields from the given [`AccountChanges`].
31    pub fn from_changes(changes: &AccountChanges) -> Self {
32        Self {
33            balance: changes.balance_post_state(),
34            nonce: changes.nonce_post_state(),
35            code_hash: changes.code_hash_post_state(),
36        }
37    }
38
39    /// Returns `true` if the block changed none of the account-level fields.
40    ///
41    /// Storage-only and read-only entries are empty; an empty entry must not overwrite the
42    /// account's existing balance, nonce or code.
43    #[inline]
44    pub const fn is_empty(&self) -> bool {
45        self.balance.is_none() && self.nonce.is_none() && self.code_hash.is_none()
46    }
47
48    /// Returns `true` if the entry this info came from contributes to the block's post-state,
49    /// and therefore to the state root: it changed an account-level field, or it wrote storage.
50    ///
51    /// `changes` must be the entry this info was extracted from. Prefer this over
52    /// [`AccountChanges::has_changes`] once the info has been extracted, it reuses the
53    /// account-level fields instead of scanning their change lists again.
54    #[inline]
55    pub fn changes_state_root(&self, changes: &AccountChanges) -> bool {
56        !self.is_empty() || changes.has_storage_changes()
57    }
58
59    /// Returns `true` if the block changed every account-level field.
60    ///
61    /// A complete entry describes the account's post-block state on its own, so consumers
62    /// reconstructing state do not have to read the account as it was before the block.
63    #[inline]
64    pub const fn is_complete(&self) -> bool {
65        self.balance.is_some() && self.nonce.is_some() && self.code_hash.is_some()
66    }
67}
68
69impl From<&AccountChanges> for BalAccountInfo {
70    fn from(changes: &AccountChanges) -> Self {
71        Self::from_changes(changes)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::{
79        BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges, StorageChange,
80    };
81    use alloy_primitives::{Address, Bytes, KECCAK256_EMPTY, bytes, keccak256};
82
83    const fn index(value: u64) -> BlockAccessIndex {
84        BlockAccessIndex::new(value)
85    }
86
87    #[test]
88    fn changed_fields_take_the_last_recorded_value() {
89        let code = bytes!("6002");
90        let changes = AccountChanges::new(Address::repeat_byte(0xaa))
91            .with_balance_change(BalanceChange::new(index(1), U256::from(10)))
92            .with_balance_change(BalanceChange::new(index(3), U256::from(30)))
93            .with_nonce_change(NonceChange::new(index(1), 5))
94            .with_nonce_change(NonceChange::new(index(2), 7))
95            .with_code_change(CodeChange::new(index(1), bytes!("6001")))
96            .with_code_change(CodeChange::new(index(2), code.clone()));
97
98        let info = BalAccountInfo::from_changes(&changes);
99
100        assert!(info.is_complete());
101        assert_eq!(info.balance, Some(U256::from(30)));
102        assert_eq!(info.nonce, Some(7));
103        assert_eq!(info.code_hash, Some(keccak256(&code)));
104        assert_eq!(info, BalAccountInfo::from(&changes));
105    }
106
107    #[test]
108    fn unchanged_fields_stay_absent() {
109        let changes = AccountChanges::new(Address::repeat_byte(0xaa))
110            .with_balance_change(BalanceChange::new(index(1), U256::from(10)));
111
112        let info = BalAccountInfo::from_changes(&changes);
113
114        assert!(!info.is_empty());
115        assert!(!info.is_complete());
116        assert_eq!(info, BalAccountInfo { balance: Some(U256::from(10)), ..Default::default() });
117    }
118
119    #[test]
120    fn read_only_entries_are_empty() {
121        let changes =
122            AccountChanges::new(Address::repeat_byte(0xdd)).with_storage_read(U256::from(1));
123
124        let info = BalAccountInfo::from_changes(&changes);
125
126        assert!(info.is_empty());
127        assert!(!info.changes_state_root(&changes));
128        assert_eq!(info, BalAccountInfo::default());
129    }
130
131    #[test]
132    fn storage_only_entries_change_the_state_root_while_empty() {
133        let changes = AccountChanges::new(Address::repeat_byte(0xbb)).with_storage_change(
134            SlotChanges::new(U256::from(1), vec![StorageChange::new(index(0), U256::from(2))]),
135        );
136
137        let info = BalAccountInfo::from_changes(&changes);
138
139        assert!(info.is_empty());
140        assert!(info.changes_state_root(&changes));
141    }
142
143    #[test]
144    fn changing_the_state_root_agrees_with_the_entry() {
145        let entries = [
146            AccountChanges::new(Address::ZERO).with_storage_read(U256::from(1)),
147            AccountChanges::new(Address::ZERO)
148                .with_storage_change(SlotChanges::new(U256::from(1), vec![])),
149            AccountChanges::new(Address::ZERO).with_storage_change(SlotChanges::new(
150                U256::from(1),
151                vec![StorageChange::new(index(0), U256::from(2))],
152            )),
153            AccountChanges::new(Address::ZERO)
154                .with_balance_change(BalanceChange::new(index(0), U256::from(1))),
155            AccountChanges::new(Address::ZERO).with_nonce_change(NonceChange::new(index(0), 1)),
156            AccountChanges::new(Address::ZERO)
157                .with_code_change(CodeChange::new(index(0), Bytes::new())),
158        ];
159
160        for changes in entries {
161            let info = BalAccountInfo::from_changes(&changes);
162            assert_eq!(info.changes_state_root(&changes), changes.has_changes());
163        }
164    }
165
166    #[test]
167    fn cleared_code_hashes_to_the_empty_code_hash() {
168        let changes = AccountChanges::new(Address::repeat_byte(0xcc))
169            .with_code_change(CodeChange::new(index(1), Bytes::new()));
170
171        let info = BalAccountInfo::from_changes(&changes);
172
173        assert!(!info.is_empty());
174        assert_eq!(info.code_hash, Some(KECCAK256_EMPTY));
175    }
176}