evm_fork_cache/
access_set.rs1use std::collections::HashSet;
9
10use alloy_eips::eip2930::{AccessList, AccessListItem};
11use alloy_primitives::{Address, B256, U256};
12use serde::{Deserialize, Serialize};
13
14#[derive(Clone, Debug, Default, Serialize, Deserialize)]
19pub struct StorageAccessList {
20 pub accounts: HashSet<Address>,
22 #[serde(default)]
24 pub code_hashes: HashSet<B256>,
25 pub slots: HashSet<(Address, U256)>,
27 #[serde(default)]
29 pub block_numbers: HashSet<u64>,
30}
31
32impl StorageAccessList {
33 pub fn is_empty(&self) -> bool {
35 self.accounts.is_empty()
36 && self.code_hashes.is_empty()
37 && self.slots.is_empty()
38 && self.block_numbers.is_empty()
39 }
40
41 pub fn account_count(&self) -> usize {
43 self.accounts.len()
44 }
45
46 pub fn slot_count(&self) -> usize {
48 self.slots.len()
49 }
50
51 pub fn code_hash_count(&self) -> usize {
53 self.code_hashes.len()
54 }
55
56 pub fn block_hash_count(&self) -> usize {
58 self.block_numbers.len()
59 }
60
61 pub fn extend(&mut self, other: &Self) {
92 self.accounts.extend(&other.accounts);
93 self.code_hashes.extend(&other.code_hashes);
94 self.slots.extend(&other.slots);
95 self.block_numbers.extend(&other.block_numbers);
96 }
97
98 pub fn missing_from(&self, available: &Self) -> Self {
100 Self {
101 accounts: self
102 .accounts
103 .difference(&available.accounts)
104 .copied()
105 .collect(),
106 code_hashes: self
107 .code_hashes
108 .difference(&available.code_hashes)
109 .copied()
110 .collect(),
111 slots: self.slots.difference(&available.slots).copied().collect(),
112 block_numbers: self
113 .block_numbers
114 .difference(&available.block_numbers)
115 .copied()
116 .collect(),
117 }
118 }
119
120 pub fn is_covered_by(&self, available: &Self) -> bool {
123 self.accounts.is_subset(&available.accounts)
124 && self.code_hashes.is_subset(&available.code_hashes)
125 && self.slots.is_subset(&available.slots)
126 && self.block_numbers.is_subset(&available.block_numbers)
127 }
128
129 pub fn marginal_gas_savings(&self, warm: &Self) -> u64 {
135 let shared_accounts = self.accounts.intersection(&warm.accounts).count() as u64;
136 let shared_slots = self.slots.intersection(&warm.slots).count() as u64;
137 shared_accounts * 2500 + shared_slots * 2000
138 }
139
140 pub fn to_eip2930(&self) -> AccessList {
142 let mut by_address: std::collections::BTreeMap<Address, Vec<B256>> = self
143 .accounts
144 .iter()
145 .copied()
146 .map(|addr| (addr, Vec::new()))
147 .collect();
148
149 for (address, slot) in &self.slots {
150 by_address
151 .entry(*address)
152 .or_default()
153 .push(B256::from(*slot));
154 }
155
156 AccessList(
157 by_address
158 .into_iter()
159 .map(|(address, mut storage_keys)| {
160 storage_keys.sort_unstable();
161 storage_keys.dedup();
162 AccessListItem {
163 address,
164 storage_keys,
165 }
166 })
167 .collect(),
168 )
169 }
170}
171
172impl From<&StorageAccessList> for AccessList {
173 fn from(value: &StorageAccessList) -> Self {
174 value.to_eip2930()
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn marginal_gas_savings_counts_only_overlap() {
184 let account_a = Address::repeat_byte(0x01);
185 let account_b = Address::repeat_byte(0x02);
186 let slot_1 = U256::from(1);
187 let slot_2 = U256::from(2);
188
189 let al = StorageAccessList {
190 accounts: [account_a, account_b].into_iter().collect(),
191 slots: [(account_a, slot_1), (account_b, slot_2)]
192 .into_iter()
193 .collect(),
194 ..Default::default()
195 };
196 let warm = StorageAccessList {
197 accounts: [account_a].into_iter().collect(),
198 slots: [(account_b, slot_2)].into_iter().collect(),
199 ..Default::default()
200 };
201
202 assert_eq!(al.marginal_gas_savings(&warm), 4500);
203 }
204
205 #[test]
206 fn eip2930_conversion_includes_address_only_entries() {
207 let account = Address::repeat_byte(0x01);
208 let storage_contract = Address::repeat_byte(0x02);
209 let mut al = StorageAccessList::default();
210 al.accounts.insert(account);
211 al.slots.insert((storage_contract, U256::from(4)));
212
213 let encoded = al.to_eip2930();
214
215 assert_eq!(encoded.0.len(), 2);
216 assert!(
217 encoded
218 .0
219 .iter()
220 .any(|item| item.address == account && item.storage_keys.is_empty())
221 );
222 assert!(encoded.0.iter().any(|item| item.address == storage_contract
223 && item.storage_keys == vec![B256::from(U256::from(4))]));
224 }
225
226 #[test]
227 fn missing_from_returns_only_unwarmed_accounts_and_slots() {
228 let warm_account = Address::repeat_byte(0x01);
229 let cold_account = Address::repeat_byte(0x02);
230 let warm_slot = (warm_account, U256::from(1));
231 let cold_slot = (cold_account, U256::from(2));
232 let required = StorageAccessList {
233 accounts: [warm_account, cold_account].into_iter().collect(),
234 slots: [warm_slot, cold_slot].into_iter().collect(),
235 ..Default::default()
236 };
237 let available = StorageAccessList {
238 accounts: [warm_account].into_iter().collect(),
239 slots: [warm_slot].into_iter().collect(),
240 ..Default::default()
241 };
242
243 let missing = required.missing_from(&available);
244
245 assert_eq!(missing.accounts, [cold_account].into_iter().collect());
246 assert_eq!(missing.slots, [cold_slot].into_iter().collect());
247 assert!(!required.is_covered_by(&available));
248 assert!(available.is_covered_by(&required));
249 }
250
251 #[test]
252 fn missing_from_includes_code_and_block_hash_dependencies() {
253 let warm_code = B256::repeat_byte(0x11);
254 let cold_code = B256::repeat_byte(0x22);
255 let required = StorageAccessList {
256 code_hashes: [warm_code, cold_code].into_iter().collect(),
257 block_numbers: [90, 91].into_iter().collect(),
258 ..Default::default()
259 };
260 let available = StorageAccessList {
261 code_hashes: [warm_code].into_iter().collect(),
262 block_numbers: [90].into_iter().collect(),
263 ..Default::default()
264 };
265
266 let missing = required.missing_from(&available);
267
268 assert_eq!(missing.code_hashes, [cold_code].into_iter().collect());
269 assert_eq!(missing.block_numbers, [91].into_iter().collect());
270 assert!(!required.is_covered_by(&available));
271 }
272}