Skip to main content

evm_fork_cache/
access_set.rs

1//! Compact account/storage touch sets captured from EVM execution.
2//!
3//! This is intentionally smaller than an EIP-2930 transaction access list:
4//! it keeps accounts and `(account, slot)` pairs as sets so callers can merge
5//! simulation traces, estimate EIP-2929 warm-access savings, and prefetch cache
6//! entries without committing to a transaction encoding.
7
8use std::collections::HashSet;
9
10use alloy_eips::eip2930::{AccessList, AccessListItem};
11use alloy_primitives::{Address, B256, U256};
12use serde::{Deserialize, Serialize};
13
14/// Accounts and storage slots touched during EVM execution.
15///
16/// The shape is optimized for simulation bookkeeping: set union, overlap
17/// checks, warm-access gas estimation, and storage prefetching.
18#[derive(Clone, Debug, Default, Serialize, Deserialize)]
19pub struct StorageAccessList {
20    /// Contract addresses touched during execution.
21    pub accounts: HashSet<Address>,
22    /// `(contract, slot)` pairs read or written during execution.
23    pub slots: HashSet<(Address, U256)>,
24}
25
26impl StorageAccessList {
27    /// Returns true when no accounts or storage slots were captured.
28    pub fn is_empty(&self) -> bool {
29        self.accounts.is_empty() && self.slots.is_empty()
30    }
31
32    /// Number of distinct accounts touched by the execution.
33    pub fn account_count(&self) -> usize {
34        self.accounts.len()
35    }
36
37    /// Number of distinct storage slots touched by the execution.
38    pub fn slot_count(&self) -> usize {
39        self.slots.len()
40    }
41
42    /// Merge another touch set into this one (set union of accounts and slots).
43    ///
44    /// Duplicate accounts and `(account, slot)` pairs already present are not
45    /// counted twice, so [`StorageAccessList::account_count`] and
46    /// [`StorageAccessList::slot_count`] reflect distinct entries after merging.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use evm_fork_cache::StorageAccessList;
52    /// use alloy_primitives::{Address, U256};
53    ///
54    /// let acct_a = Address::repeat_byte(0x01);
55    /// let acct_b = Address::repeat_byte(0x02);
56    ///
57    /// let mut base = StorageAccessList::default();
58    /// base.accounts.insert(acct_a);
59    /// base.slots.insert((acct_a, U256::from(1)));
60    ///
61    /// let mut other = StorageAccessList::default();
62    /// other.accounts.insert(acct_a); // overlaps `base`, not double-counted
63    /// other.accounts.insert(acct_b);
64    /// other.slots.insert((acct_b, U256::from(2)));
65    ///
66    /// base.extend(&other);
67    ///
68    /// assert_eq!(base.account_count(), 2);
69    /// assert_eq!(base.slot_count(), 2);
70    /// assert!(!base.is_empty());
71    /// ```
72    pub fn extend(&mut self, other: &Self) {
73        self.accounts.extend(&other.accounts);
74        self.slots.extend(&other.slots);
75    }
76
77    /// Compute EIP-2929 gas saved when this touch set runs after `warm`.
78    ///
79    /// Cold account access costs 2600 gas versus 100 gas when warm, saving
80    /// 2500 gas. Cold SLOAD costs 2100 gas versus 100 gas when warm, saving
81    /// 2000 gas.
82    pub fn marginal_gas_savings(&self, warm: &Self) -> u64 {
83        let shared_accounts = self.accounts.intersection(&warm.accounts).count() as u64;
84        let shared_slots = self.slots.intersection(&warm.slots).count() as u64;
85        shared_accounts * 2500 + shared_slots * 2000
86    }
87
88    /// Convert this touch set into an EIP-2930 transaction access list.
89    pub fn to_eip2930(&self) -> AccessList {
90        let mut by_address: std::collections::BTreeMap<Address, Vec<B256>> = self
91            .accounts
92            .iter()
93            .copied()
94            .map(|addr| (addr, Vec::new()))
95            .collect();
96
97        for (address, slot) in &self.slots {
98            by_address
99                .entry(*address)
100                .or_default()
101                .push(B256::from(*slot));
102        }
103
104        AccessList(
105            by_address
106                .into_iter()
107                .map(|(address, mut storage_keys)| {
108                    storage_keys.sort_unstable();
109                    storage_keys.dedup();
110                    AccessListItem {
111                        address,
112                        storage_keys,
113                    }
114                })
115                .collect(),
116        )
117    }
118}
119
120impl From<&StorageAccessList> for AccessList {
121    fn from(value: &StorageAccessList) -> Self {
122        value.to_eip2930()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn marginal_gas_savings_counts_only_overlap() {
132        let account_a = Address::repeat_byte(0x01);
133        let account_b = Address::repeat_byte(0x02);
134        let slot_1 = U256::from(1);
135        let slot_2 = U256::from(2);
136
137        let al = StorageAccessList {
138            accounts: [account_a, account_b].into_iter().collect(),
139            slots: [(account_a, slot_1), (account_b, slot_2)]
140                .into_iter()
141                .collect(),
142        };
143        let warm = StorageAccessList {
144            accounts: [account_a].into_iter().collect(),
145            slots: [(account_b, slot_2)].into_iter().collect(),
146        };
147
148        assert_eq!(al.marginal_gas_savings(&warm), 4500);
149    }
150
151    #[test]
152    fn eip2930_conversion_includes_address_only_entries() {
153        let account = Address::repeat_byte(0x01);
154        let storage_contract = Address::repeat_byte(0x02);
155        let mut al = StorageAccessList::default();
156        al.accounts.insert(account);
157        al.slots.insert((storage_contract, U256::from(4)));
158
159        let encoded = al.to_eip2930();
160
161        assert_eq!(encoded.0.len(), 2);
162        assert!(
163            encoded
164                .0
165                .iter()
166                .any(|item| item.address == account && item.storage_keys.is_empty())
167        );
168        assert!(encoded.0.iter().any(|item| item.address == storage_contract
169            && item.storage_keys == vec![B256::from(U256::from(4))]));
170    }
171}