1use std::collections::{BTreeMap, HashMap, HashSet};
4use std::sync::Arc;
5
6use alloy_eips::BlockId;
7use alloy_primitives::{Address, U256};
8use alloy_rpc_types_eth::TransactionRequest;
9
10use super::{EvmCache, PrewarmReport};
11use crate::access_set::StorageAccessList;
12use crate::errors::{AccessListError, StorageFetchError};
13
14#[derive(Clone, Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum ReadSetHydrationFailure {
19 #[error("no account proof fetcher is installed for {address}")]
21 ProofFetcherUnavailable {
22 address: Address,
24 },
25 #[error("account proof fetcher omitted requested address {address}")]
27 ProofResultMissing {
28 address: Address,
30 },
31 #[error("account proof fetcher returned duplicate results for {address}")]
33 ProofResultDuplicate {
34 address: Address,
36 },
37 #[error("account proof fetcher returned unexpected address {address}")]
39 ProofResultUnexpected {
40 address: Address,
42 },
43 #[error("account proof for {address} returned duplicate storage slot {slot}")]
46 StorageSlotDuplicate {
47 address: Address,
49 slot: U256,
51 },
52 #[error("account proof for {address} returned unexpected storage slot {slot}")]
55 StorageSlotUnexpected {
56 address: Address,
58 slot: U256,
60 },
61 #[error("account proof fetch failed for {address}: {source}")]
63 ProofFetch {
64 address: Address,
66 #[source]
68 source: StorageFetchError,
69 },
70 #[error("runtime code {code_hash} is not resident for deployed account {address}")]
73 RuntimeCodeUnavailable {
74 address: Address,
76 code_hash: alloy_primitives::B256,
78 },
79 #[error("account proof for {address} omitted requested storage slot {slot}")]
81 StorageSlotMissing {
82 address: Address,
84 slot: U256,
86 },
87}
88
89#[derive(Clone, Debug)]
91pub struct ReadSetHydrationReport {
92 pub block: BlockId,
94 pub accounts_refreshed: usize,
96 pub slots_refreshed: usize,
98 pub failures: Vec<ReadSetHydrationFailure>,
100 pub code_changes: Vec<(Address, alloy_primitives::B256, alloy_primitives::B256)>,
102 pub missing_after: StorageAccessList,
104}
105
106impl ReadSetHydrationReport {
107 pub fn is_complete(&self) -> bool {
110 self.failures.is_empty() && self.code_changes.is_empty() && self.missing_after.is_empty()
111 }
112}
113
114pub type AccessListFetchFn = Arc<
120 dyn Fn(
121 Vec<TransactionRequest>,
122 BlockId,
123 ) -> Vec<std::result::Result<StorageAccessList, AccessListError>>
124 + Send
125 + Sync,
126>;
127
128#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
130#[non_exhaustive]
131pub enum ReadSetWarmupError {
132 #[error("access-list discovery was required for {calls} call(s), but no fetcher is installed")]
135 AccessListFetcherUnavailable {
136 calls: usize,
138 },
139 #[error("access-list fetcher returned {actual} result(s) for {expected} request(s)")]
141 AccessListResultCountMismatch {
142 expected: usize,
144 actual: usize,
146 },
147}
148
149#[derive(Clone, Debug, Default)]
151pub struct ReadSetWarmupCall {
152 pub tx: TransactionRequest,
154 pub expected_slots: Option<usize>,
156 pub restrict_to: Option<Vec<Address>>,
158}
159
160#[derive(Clone, Debug, Default)]
162pub struct ReadSetWarmupBatch {
163 pub known_slots: Vec<(Address, U256)>,
165 pub calls: Vec<ReadSetWarmupCall>,
167}
168
169#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
171pub enum ReadSetWarmupStrategy {
172 #[default]
174 Auto,
175 LocalOnly,
177 AccessList,
179}
180
181#[derive(Clone, Debug, PartialEq, Eq)]
183pub struct ReadSetWarmupConfig {
184 pub strategy: ReadSetWarmupStrategy,
186 pub min_expected_slots_for_access_list: usize,
188 pub min_unhinted_calls_for_access_list: usize,
190}
191
192impl Default for ReadSetWarmupConfig {
193 fn default() -> Self {
194 Self {
195 strategy: ReadSetWarmupStrategy::Auto,
196 min_expected_slots_for_access_list: 32,
197 min_unhinted_calls_for_access_list: 8,
198 }
199 }
200}
201
202impl ReadSetWarmupConfig {
203 fn should_use_access_lists(&self, calls: &[ReadSetWarmupCall]) -> bool {
204 match self.strategy {
205 ReadSetWarmupStrategy::LocalOnly => false,
206 ReadSetWarmupStrategy::AccessList => !calls.is_empty(),
207 ReadSetWarmupStrategy::Auto => {
208 let expected = calls
209 .iter()
210 .filter_map(|call| call.expected_slots)
211 .fold(0usize, usize::saturating_add);
212 let unhinted = calls
213 .iter()
214 .filter(|call| call.expected_slots.is_none())
215 .count();
216 expected >= self.min_expected_slots_for_access_list
217 || unhinted >= self.min_unhinted_calls_for_access_list
218 }
219 }
220 }
221}
222
223#[derive(Debug, Default)]
225pub struct ReadSetWarmupReport {
226 pub known: PrewarmReport,
228 pub used_access_lists: bool,
230 pub skipped_calls: usize,
232 pub access_list_successes: usize,
234 pub access_list_failures: Vec<(usize, AccessListError)>,
236 pub discovered_access: StorageAccessList,
238 pub discovered: PrewarmReport,
240}
241
242impl EvmCache {
243 pub fn access_list_fetcher(&self) -> Option<&AccessListFetchFn> {
245 self.access_list_fetcher.as_ref()
246 }
247
248 pub fn set_access_list_fetcher(&mut self, fetcher: AccessListFetchFn) {
250 self.access_list_fetcher = Some(fetcher);
251 }
252
253 pub fn prewarm_read_sets(
268 &mut self,
269 batch: ReadSetWarmupBatch,
270 config: ReadSetWarmupConfig,
271 ) -> Result<ReadSetWarmupReport, ReadSetWarmupError> {
272 let discovery_results =
273 if batch.calls.is_empty() || !config.should_use_access_lists(&batch.calls) {
274 None
275 } else {
276 let Some(fetcher) = self.access_list_fetcher.clone() else {
277 return Err(ReadSetWarmupError::AccessListFetcherUnavailable {
278 calls: batch.calls.len(),
279 });
280 };
281 let requests = batch.calls.iter().map(|call| call.tx.clone()).collect();
282 let results = fetcher(requests, self.block);
283 if results.len() != batch.calls.len() {
284 return Err(ReadSetWarmupError::AccessListResultCountMismatch {
285 expected: batch.calls.len(),
286 actual: results.len(),
287 });
288 }
289 Some(results)
290 };
291
292 let known = if batch.known_slots.is_empty() {
293 PrewarmReport::default()
294 } else {
295 self.prewarm_slots(&batch.known_slots)
296 };
297 let mut report = ReadSetWarmupReport {
298 known,
299 ..Default::default()
300 };
301 if batch.calls.is_empty() {
302 return Ok(report);
303 }
304 let Some(results) = discovery_results else {
305 report.skipped_calls = batch.calls.len();
306 return Ok(report);
307 };
308
309 report.used_access_lists = true;
310 let mut results = results.into_iter();
311 let mut discovered = StorageAccessList::default();
312 for (index, call) in batch.calls.iter().enumerate() {
313 let result = results
314 .next()
315 .expect("access-list result count was checked above");
316 match result {
317 Ok(mut access) => {
318 if let Some(restrict_to) = &call.restrict_to {
319 let keep: HashSet<_> = restrict_to.iter().copied().collect();
320 access.accounts.retain(|address| keep.contains(address));
321 access.slots.retain(|(address, _)| keep.contains(address));
322 }
323 discovered.extend(&access);
324 report.access_list_successes += 1;
325 }
326 Err(error) => report.access_list_failures.push((index, error)),
327 }
328 }
329
330 let mut slots: Vec<_> = discovered.slots.iter().copied().collect();
331 slots.sort_unstable();
332 report.discovered_access = discovered;
333 if !slots.is_empty() {
334 report.discovered = self.prewarm_slots(&slots);
335 }
336 Ok(report)
337 }
338
339 pub fn hydrate_read_set(&mut self, required: &StorageAccessList) -> ReadSetHydrationReport {
355 let block = self.block;
356 let mut requests: BTreeMap<Address, Vec<U256>> = BTreeMap::new();
357 for address in &required.accounts {
358 requests.entry(*address).or_default();
359 }
360 for (address, slot) in &required.slots {
361 requests.entry(*address).or_default().push(*slot);
362 }
363 for slots in requests.values_mut() {
364 slots.sort_unstable();
365 slots.dedup();
366 }
367
368 let mut report = ReadSetHydrationReport {
369 block,
370 accounts_refreshed: 0,
371 slots_refreshed: 0,
372 failures: Vec::new(),
373 code_changes: Vec::new(),
374 missing_after: required.clone(),
375 };
376 if requests.is_empty() {
377 report.missing_after = self.snapshot().missing_read_set(required);
378 return report;
379 }
380 let Some(fetcher) = self.account_proof_fetcher.clone() else {
381 report.failures.extend(
382 requests
383 .keys()
384 .copied()
385 .map(|address| ReadSetHydrationFailure::ProofFetcherUnavailable { address }),
386 );
387 return report;
388 };
389
390 let requested: Vec<_> = requests
391 .iter()
392 .map(|(address, slots)| (*address, slots.clone()))
393 .collect();
394 let mut fetched = HashMap::new();
395 let mut duplicate_addresses = HashSet::new();
396 for (address, result) in fetcher(requested, block) {
397 if !requests.contains_key(&address) {
398 report
399 .failures
400 .push(ReadSetHydrationFailure::ProofResultUnexpected { address });
401 continue;
402 }
403 if duplicate_addresses.contains(&address) || fetched.contains_key(&address) {
404 if duplicate_addresses.insert(address) {
405 report
406 .failures
407 .push(ReadSetHydrationFailure::ProofResultDuplicate { address });
408 }
409 fetched.remove(&address);
410 continue;
411 }
412 fetched.insert(address, result);
413 }
414 let mut fresh_slots = Vec::new();
415
416 for (address, expected_slots) in requests {
417 if duplicate_addresses.contains(&address) {
418 continue;
419 }
420 let Some(result) = fetched.get(&address) else {
421 report
422 .failures
423 .push(ReadSetHydrationFailure::ProofResultMissing { address });
424 continue;
425 };
426 let proof = match result {
427 Ok(proof) => proof,
428 Err(error) => {
429 report.failures.push(ReadSetHydrationFailure::ProofFetch {
430 address,
431 source: error.clone(),
432 });
433 continue;
434 }
435 };
436
437 let current = self.local_account_info(address);
438 if let Some(current) = current.as_ref()
439 && current.code_hash != proof.code_hash
440 {
441 report
442 .code_changes
443 .push((address, current.code_hash, proof.code_hash));
444 continue;
445 }
446 if current.is_none()
447 && proof.code_hash != alloy_primitives::B256::ZERO
448 && proof.code_hash != revm::primitives::KECCAK_EMPTY
449 {
450 report
451 .failures
452 .push(ReadSetHydrationFailure::RuntimeCodeUnavailable {
453 address,
454 code_hash: proof.code_hash,
455 });
456 continue;
457 }
458
459 let mut info = current.unwrap_or_default();
460 info.balance = proof.balance;
461 info.nonce = proof.nonce;
462 info.code_hash = proof.code_hash;
463 self.write_account_info_through(address, info);
464 report.accounts_refreshed += 1;
465
466 let expected_slot_set: HashSet<_> = expected_slots.iter().copied().collect();
467 let mut by_slot = HashMap::new();
468 let mut duplicate_slots = HashSet::new();
469 for (slot, value) in proof.slots.iter().copied() {
470 if !expected_slot_set.contains(&slot) {
471 report
472 .failures
473 .push(ReadSetHydrationFailure::StorageSlotUnexpected { address, slot });
474 continue;
475 }
476 if duplicate_slots.contains(&slot) || by_slot.contains_key(&slot) {
477 if duplicate_slots.insert(slot) {
478 report
479 .failures
480 .push(ReadSetHydrationFailure::StorageSlotDuplicate { address, slot });
481 }
482 by_slot.remove(&slot);
483 continue;
484 }
485 by_slot.insert(slot, value);
486 }
487 let mut complete_slots = true;
488 for slot in expected_slots {
489 if duplicate_slots.contains(&slot) {
490 complete_slots = false;
491 continue;
492 }
493 let Some(value) = by_slot.get(&slot).copied() else {
494 report
495 .failures
496 .push(ReadSetHydrationFailure::StorageSlotMissing { address, slot });
497 complete_slots = false;
498 continue;
499 };
500 fresh_slots.push((address, slot, value));
501 }
502 if !complete_slots {
503 continue;
504 }
505 }
506
507 report.slots_refreshed = fresh_slots.len();
508 if !fresh_slots.is_empty() {
509 self.inject_storage_batch_fresh(&fresh_slots);
510 }
511 report.missing_after = self.snapshot().missing_read_set(required);
512 report
513 }
514}