Skip to main content

evm_fork_cache/cold_start/
storage_round.rs

1//! Protocol-neutral, hash-pinned storage fetch artifacts for background cold start.
2
3use std::collections::{HashMap, HashSet};
4use std::fmt;
5use std::sync::Arc;
6
7use alloy_eips::BlockId;
8use alloy_network::AnyNetwork;
9use alloy_primitives::{Address, B256, U256};
10use alloy_provider::Provider;
11
12use crate::cache::{
13    EvmCache, StorageBatchConfig, StorageBatchFetchFn, StorageFetchStrategy,
14    provider_storage_fetcher,
15};
16use crate::errors::StorageFetchResult;
17use crate::freshness::{SlotFetch, SlotOutcome};
18use crate::state_update::{StateDiff, StateUpdate};
19
20/// One storage identity requested by a split cold-start round.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct StorageSlotRequest {
23    address: Address,
24    slot: U256,
25}
26
27impl StorageSlotRequest {
28    /// Construct one storage identity.
29    pub const fn new(address: Address, slot: U256) -> Self {
30        Self { address, slot }
31    }
32
33    /// Storage-owning contract.
34    pub const fn address(self) -> Address {
35        self.address
36    }
37
38    /// Storage key.
39    pub const fn slot(self) -> U256 {
40        self.slot
41    }
42}
43
44impl From<(Address, U256)> for StorageSlotRequest {
45    fn from((address, slot): (Address, U256)) -> Self {
46        Self::new(address, slot)
47    }
48}
49
50/// A slot value fetched authoritatively at one exact block hash.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
52pub struct PreparedStorageValue {
53    address: Address,
54    slot: U256,
55    value: U256,
56}
57
58impl PreparedStorageValue {
59    /// Construct one prepared storage value.
60    pub const fn new(address: Address, slot: U256, value: U256) -> Self {
61        Self {
62            address,
63            slot,
64            value,
65        }
66    }
67
68    /// Storage-owning contract.
69    pub const fn address(self) -> Address {
70        self.address
71    }
72
73    /// Storage key.
74    pub const fn slot(self) -> U256 {
75        self.slot
76    }
77
78    /// Authoritative value at [`PreparedStoragePatch::block_hash`].
79    pub const fn value(self) -> U256 {
80        self.value
81    }
82}
83
84/// Worker-produced storage values ready for one serialized cache commit.
85///
86/// Construction is intentionally infallible: the cache-owner commit remains the
87/// trust boundary and validates duplicate identities and baseline freshness
88/// immediately before mutating either cache layer.
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct PreparedStoragePatch {
91    block_hash: B256,
92    values: Vec<PreparedStorageValue>,
93}
94
95impl EvmCache {
96    /// Atomically apply a worker-produced storage patch at the cache's exact hash.
97    ///
98    /// Validation completes before the first write: duplicate identities and a
99    /// cache pin that does not match the patch hash reject the entire patch. A
100    /// valid patch is written through the cache's batched absolute-slot funnel,
101    /// healing layer 2 and every already-materialized layer-1 account together.
102    /// This method performs no provider work.
103    pub fn apply_prepared_storage_patch(
104        &mut self,
105        patch: &PreparedStoragePatch,
106    ) -> Result<StateDiff, PreparedStoragePatchError> {
107        let mut identities = HashSet::with_capacity(patch.values.len());
108        for value in &patch.values {
109            if !identities.insert((value.address, value.slot)) {
110                return Err(PreparedStoragePatchError::DuplicateSlot {
111                    address: value.address,
112                    slot: value.slot,
113                });
114            }
115        }
116
117        let cache_hash = match self.block() {
118            BlockId::Hash(hash) => Some(hash.block_hash),
119            BlockId::Number(_) => None,
120        };
121        if cache_hash != Some(patch.block_hash) {
122            return Err(PreparedStoragePatchError::BaselineMismatch {
123                prepared: patch.block_hash,
124                cache: cache_hash,
125            });
126        }
127
128        let updates: Vec<_> = patch
129            .values
130            .iter()
131            .map(|value| StateUpdate::slot(value.address, value.slot, value.value))
132            .collect();
133        Ok(self.apply_updates(&updates))
134    }
135}
136
137impl PreparedStoragePatch {
138    /// Construct a patch tied to one exact post-block hash.
139    pub fn new(block_hash: B256, values: impl IntoIterator<Item = PreparedStorageValue>) -> Self {
140        Self {
141            block_hash,
142            values: values.into_iter().collect(),
143        }
144    }
145
146    /// Exact hash used to fetch every value.
147    pub const fn block_hash(&self) -> B256 {
148        self.block_hash
149    }
150
151    /// Values in request order.
152    pub fn values(&self) -> &[PreparedStorageValue] {
153        &self.values
154    }
155}
156
157/// Slot-only work for one exact-hash cold-start round.
158#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct StorageRoundRequest {
160    block_hash: B256,
161    verify: Vec<StorageSlotRequest>,
162    probe: Vec<StorageSlotRequest>,
163}
164
165impl StorageRoundRequest {
166    /// Construct a slot-only round. Request identity validation happens at
167    /// [`StorageRoundFetcher::fetch`] before provider work begins.
168    pub fn new<V, P, VI, PI>(block_hash: B256, verify: V, probe: P) -> Self
169    where
170        V: IntoIterator<Item = VI>,
171        P: IntoIterator<Item = PI>,
172        VI: Into<StorageSlotRequest>,
173        PI: Into<StorageSlotRequest>,
174    {
175        Self {
176            block_hash,
177            verify: verify.into_iter().map(Into::into).collect(),
178            probe: probe.into_iter().map(Into::into).collect(),
179        }
180    }
181
182    /// Exact canonical hash all provider reads must use.
183    pub const fn block_hash(&self) -> B256 {
184        self.block_hash
185    }
186
187    /// Slots whose successful values should become a prepared commit patch.
188    pub fn verify(&self) -> &[StorageSlotRequest] {
189        &self.verify
190    }
191
192    /// Slots whose outcomes are observational and must not be committed.
193    pub fn probe(&self) -> &[StorageSlotRequest] {
194        &self.probe
195    }
196}
197
198/// Complete, non-mutating result of one storage-only provider round.
199#[derive(Clone, Debug, PartialEq, Eq)]
200pub struct StorageRoundFetch {
201    block_hash: B256,
202    verified: Vec<SlotOutcome>,
203    probed: Vec<SlotOutcome>,
204    patch: PreparedStoragePatch,
205}
206
207impl StorageRoundFetch {
208    /// Exact hash used by the provider read.
209    pub const fn block_hash(&self) -> B256 {
210        self.block_hash
211    }
212
213    /// One classified outcome per verify request, in request order.
214    pub fn verified(&self) -> &[SlotOutcome] {
215        &self.verified
216    }
217
218    /// One classified outcome per probe request, in request order.
219    pub fn probed(&self) -> &[SlotOutcome] {
220        &self.probed
221    }
222
223    /// Successfully fetched verify values ready for atomic cache-owner commit.
224    pub const fn patch(&self) -> &PreparedStoragePatch {
225        &self.patch
226    }
227
228    /// Consume the result and return its prepared commit patch.
229    pub fn into_patch(self) -> PreparedStoragePatch {
230        self.patch
231    }
232
233    /// Consume the result without cloning planner outcomes or prepared values.
234    pub fn into_parts(self) -> (Vec<SlotOutcome>, Vec<SlotOutcome>, PreparedStoragePatch) {
235        (self.verified, self.probed, self.patch)
236    }
237}
238
239/// Cloneable, thread-safe provider handle for non-mutating storage rounds.
240#[derive(Clone)]
241pub struct StorageRoundFetcher {
242    fetcher: StorageBatchFetchFn,
243}
244
245impl fmt::Debug for StorageRoundFetcher {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        f.debug_struct("StorageRoundFetcher")
248            .finish_non_exhaustive()
249    }
250}
251
252impl StorageRoundFetcher {
253    /// Wrap an existing protocol-neutral storage batch provider.
254    pub fn new(fetcher: StorageBatchFetchFn) -> Self {
255        Self { fetcher }
256    }
257
258    /// Build a worker-owned provider fetcher without constructing an
259    /// [`EvmCache`].
260    pub fn from_provider<P>(
261        provider: Arc<P>,
262        batch_config: StorageBatchConfig,
263        strategy: StorageFetchStrategy,
264    ) -> Self
265    where
266        P: Provider<AnyNetwork> + 'static,
267    {
268        Self::new(provider_storage_fetcher(provider, batch_config, strategy))
269    }
270
271    /// Fetch one slot-only round without holding or mutating an [`EvmCache`](crate::cache::EvmCache).
272    ///
273    /// The provider receives an exact canonical hash pin. Its response is
274    /// checked for missing, unexpected, or duplicate identities before a result
275    /// is returned; per-slot provider failures remain classified outcomes so a
276    /// resumable planner can decide its next round.
277    pub fn fetch(
278        &self,
279        request: &StorageRoundRequest,
280    ) -> Result<StorageRoundFetch, StorageRoundFetchError> {
281        let mut requested = HashSet::with_capacity(request.verify.len() + request.probe.len());
282        for slot in request.verify.iter().chain(&request.probe) {
283            if !requested.insert((slot.address, slot.slot)) {
284                return Err(StorageRoundFetchError::DuplicateRequest {
285                    address: slot.address,
286                    slot: slot.slot,
287                });
288            }
289        }
290
291        let provider_requests: Vec<_> = request
292            .verify
293            .iter()
294            .chain(&request.probe)
295            .map(|slot| (slot.address, slot.slot))
296            .collect();
297        let response = (self.fetcher)(
298            provider_requests,
299            BlockId::from((request.block_hash, Some(true))),
300        );
301
302        let mut returned: HashMap<(Address, U256), StorageFetchResult<U256>> =
303            HashMap::with_capacity(response.len());
304        for (address, slot, value) in response {
305            if !requested.contains(&(address, slot)) {
306                return Err(StorageRoundFetchError::UnexpectedResult { address, slot });
307            }
308            if returned.insert((address, slot), value).is_some() {
309                return Err(StorageRoundFetchError::DuplicateResult { address, slot });
310            }
311        }
312
313        for &(address, slot) in &requested {
314            if !returned.contains_key(&(address, slot)) {
315                return Err(StorageRoundFetchError::MissingResult { address, slot });
316            }
317        }
318
319        let mut patch = Vec::with_capacity(request.verify.len());
320        let verified = take_outcomes(&request.verify, &mut returned, Some(&mut patch));
321        let probed = take_outcomes(&request.probe, &mut returned, None);
322        Ok(StorageRoundFetch {
323            block_hash: request.block_hash,
324            verified,
325            probed,
326            patch: PreparedStoragePatch::new(request.block_hash, patch),
327        })
328    }
329}
330
331fn take_outcomes(
332    requests: &[StorageSlotRequest],
333    returned: &mut HashMap<(Address, U256), StorageFetchResult<U256>>,
334    mut patch: Option<&mut Vec<PreparedStorageValue>>,
335) -> Vec<SlotOutcome> {
336    requests
337        .iter()
338        .map(|request| {
339            let fetched = returned
340                .remove(&(request.address, request.slot))
341                .expect("provider response completeness validated above");
342            let fetch = match fetched {
343                Ok(value) => {
344                    if let Some(values) = patch.as_deref_mut() {
345                        values.push(PreparedStorageValue::new(
346                            request.address,
347                            request.slot,
348                            value,
349                        ));
350                    }
351                    if value == U256::ZERO {
352                        SlotFetch::Zero
353                    } else {
354                        SlotFetch::Value(value)
355                    }
356                }
357                Err(error) => SlotFetch::FetchFailed {
358                    reason: error.to_string(),
359                },
360            };
361            SlotOutcome {
362                address: request.address,
363                slot: request.slot,
364                fetch,
365            }
366        })
367        .collect()
368}
369
370/// Invalid storage-round request or malformed provider response.
371#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
372#[non_exhaustive]
373pub enum StorageRoundFetchError {
374    /// The same identity appeared more than once across verify/probe work.
375    #[error("duplicate storage-round request for ({address}, {slot})")]
376    DuplicateRequest {
377        /// Storage-owning contract.
378        address: Address,
379        /// Storage key.
380        slot: U256,
381    },
382    /// The provider returned an identity that was not requested.
383    #[error("storage provider returned unexpected slot ({address}, {slot})")]
384    UnexpectedResult {
385        /// Storage-owning contract.
386        address: Address,
387        /// Storage key.
388        slot: U256,
389    },
390    /// The provider returned the same identity more than once.
391    #[error("storage provider returned duplicate slot ({address}, {slot})")]
392    DuplicateResult {
393        /// Storage-owning contract.
394        address: Address,
395        /// Storage key.
396        slot: U256,
397    },
398    /// The provider omitted a requested identity.
399    #[error("storage provider omitted requested slot ({address}, {slot})")]
400    MissingResult {
401        /// Storage-owning contract.
402        address: Address,
403        /// Storage key.
404        slot: U256,
405    },
406}
407
408/// Invalid worker-produced storage patch.
409#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
410#[non_exhaustive]
411pub enum PreparedStoragePatchError {
412    /// The artifact contains multiple values for one storage identity.
413    #[error("prepared storage patch contains duplicate slot ({address}, {slot})")]
414    DuplicateSlot {
415        /// Storage-owning contract.
416        address: Address,
417        /// Storage key.
418        slot: U256,
419    },
420    /// The actor cache is no longer pinned to the artifact's exact block hash.
421    #[error("prepared storage patch baseline mismatch: prepared {prepared}, cache {cache:?}")]
422    BaselineMismatch {
423        /// Hash used by the background provider fetch.
424        prepared: B256,
425        /// Current cache hash, or `None` when the cache is number/tag-pinned.
426        cache: Option<B256>,
427    },
428}