Skip to main content

evm_fork_cache/cold_start/
account_round.rs

1//! Exact-hash account/code verification 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, Bytes};
10use alloy_provider::Provider;
11
12use crate::cache::{AccountProof, AccountProofFetchFn, account_proof_fetcher};
13use crate::errors::StorageFetchResult;
14
15/// One canonical runtime-code hash claim to verify through `eth_getProof`.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub struct AccountCodeClaim {
18    address: Address,
19    expected_code_hash: B256,
20}
21
22impl AccountCodeClaim {
23    /// Construct a code claim. Callers normally hash known runtime bytes
24    /// locally and pass that hash here.
25    pub const fn new(address: Address, expected_code_hash: B256) -> Self {
26        Self {
27            address,
28            expected_code_hash,
29        }
30    }
31
32    /// Claimed contract address.
33    pub const fn address(self) -> Address {
34        self.address
35    }
36
37    /// Expected `codeHash` committed by the account proof.
38    pub const fn expected_code_hash(self) -> B256 {
39        self.expected_code_hash
40    }
41}
42
43/// Root-only account/code checks at one exact canonical block hash.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct AccountProofRoundRequest {
46    block_hash: B256,
47    claims: Vec<AccountCodeClaim>,
48}
49
50impl AccountProofRoundRequest {
51    /// Construct a code-verification round. Duplicate-address validation occurs
52    /// before provider IO in [`AccountProofRoundFetcher::fetch`].
53    pub fn new(block_hash: B256, claims: impl IntoIterator<Item = AccountCodeClaim>) -> Self {
54        Self {
55            block_hash,
56            claims: claims.into_iter().collect(),
57        }
58    }
59
60    /// Exact canonical block hash for every proof request.
61    pub const fn block_hash(&self) -> B256 {
62        self.block_hash
63    }
64
65    /// Code claims in caller order.
66    pub fn claims(&self) -> &[AccountCodeClaim] {
67        &self.claims
68    }
69}
70
71/// Classified result of one exact-hash account/code claim.
72#[derive(Clone, Debug, PartialEq, Eq)]
73#[non_exhaustive]
74pub enum AccountProofOutcome {
75    /// The proof's `codeHash` matches the worker's expected runtime-code hash.
76    Verified {
77        /// Verified account.
78        address: Address,
79        /// Exact-hash account fields and storage root.
80        proof: AccountProof,
81    },
82    /// The proof exists but commits to another code hash (including absent and
83    /// code-less account hashes).
84    Mismatch {
85        /// Contradicted account.
86        address: Address,
87        /// Worker-claimed runtime-code hash.
88        expected: B256,
89        /// Proof-observed code hash.
90        actual: B256,
91        /// Exact-hash account fields and storage root.
92        proof: AccountProof,
93    },
94    /// The provider returned an explicit per-account failure.
95    FetchFailed {
96        /// Unverified account.
97        address: Address,
98        /// Provider error text.
99        reason: String,
100    },
101}
102
103impl AccountProofOutcome {
104    /// Account associated with this outcome.
105    pub const fn address(&self) -> Address {
106        match self {
107            Self::Verified { address, .. }
108            | Self::Mismatch { address, .. }
109            | Self::FetchFailed { address, .. } => *address,
110        }
111    }
112
113    /// Borrow a verified proof, or `None` for mismatch/failure outcomes.
114    pub const fn verified_proof(&self) -> Option<&AccountProof> {
115        match self {
116            Self::Verified { proof, .. } => Some(proof),
117            Self::Mismatch { .. } | Self::FetchFailed { .. } => None,
118        }
119    }
120}
121
122/// Complete non-mutating account/code proof result.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct AccountProofRoundFetch {
125    block_hash: B256,
126    outcomes: Vec<AccountProofOutcome>,
127}
128
129/// Runtime code and exact-hash account fields ready for canonical cache commit.
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct PreparedAccountValue {
132    address: Address,
133    proof: AccountProof,
134    code: Bytes,
135}
136
137impl PreparedAccountValue {
138    /// Pair known runtime bytes with the exact-hash proof that commits to them.
139    /// Byte/hash validation remains at the cache-owner apply boundary.
140    pub const fn new(address: Address, proof: AccountProof, code: Bytes) -> Self {
141        Self {
142            address,
143            proof,
144            code,
145        }
146    }
147
148    /// Account whose runtime code was verified.
149    pub const fn address(&self) -> Address {
150        self.address
151    }
152
153    /// Exact-hash account proof fields.
154    pub const fn proof(&self) -> &AccountProof {
155        &self.proof
156    }
157
158    /// Runtime bytecode committed by [`AccountProof::code_hash`].
159    pub const fn code(&self) -> &Bytes {
160        &self.code
161    }
162}
163
164/// Worker-produced verified accounts ready for one serialized cache commit.
165#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct PreparedAccountPatch {
167    block_hash: B256,
168    verified_at_block: u64,
169    values: Vec<PreparedAccountValue>,
170}
171
172impl PreparedAccountPatch {
173    /// Construct a patch tied to one exact post-block point.
174    pub fn new(
175        block_hash: B256,
176        verified_at_block: u64,
177        values: impl IntoIterator<Item = PreparedAccountValue>,
178    ) -> Self {
179        Self {
180            block_hash,
181            verified_at_block,
182            values: values.into_iter().collect(),
183        }
184    }
185
186    /// Exact hash used to fetch every proof.
187    pub const fn block_hash(&self) -> B256 {
188        self.block_hash
189    }
190
191    /// Block height recorded on each durable verified-code mark.
192    pub const fn verified_at_block(&self) -> u64 {
193        self.verified_at_block
194    }
195
196    /// Verified accounts in worker order.
197    pub fn values(&self) -> &[PreparedAccountValue] {
198        &self.values
199    }
200}
201
202impl AccountProofRoundFetch {
203    /// Exact canonical hash used by the proof provider.
204    pub const fn block_hash(&self) -> B256 {
205        self.block_hash
206    }
207
208    /// One outcome per claim, in claim order.
209    pub fn outcomes(&self) -> &[AccountProofOutcome] {
210        &self.outcomes
211    }
212
213    /// Consume the result without cloning outcomes.
214    pub fn into_outcomes(self) -> Vec<AccountProofOutcome> {
215        self.outcomes
216    }
217}
218
219/// Cloneable, thread-safe provider handle for exact-hash account/code proofs.
220#[derive(Clone)]
221pub struct AccountProofRoundFetcher {
222    fetcher: AccountProofFetchFn,
223}
224
225impl fmt::Debug for AccountProofRoundFetcher {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        f.debug_struct("AccountProofRoundFetcher")
228            .finish_non_exhaustive()
229    }
230}
231
232impl AccountProofRoundFetcher {
233    /// Wrap an existing protocol-neutral account proof provider.
234    pub fn new(fetcher: AccountProofFetchFn) -> Self {
235        Self { fetcher }
236    }
237
238    /// Build a worker-owned proof fetcher without constructing an [`EvmCache`](crate::cache::EvmCache).
239    pub fn from_provider<P>(provider: Arc<P>, max_concurrent_proofs: usize) -> Self
240    where
241        P: Provider<AnyNetwork> + 'static,
242    {
243        Self::new(account_proof_fetcher(provider, max_concurrent_proofs))
244    }
245
246    /// Verify every claim through a root-only proof pinned to the requested
247    /// EIP-1898 canonical hash, without holding or mutating an EVM cache.
248    pub fn fetch(
249        &self,
250        request: &AccountProofRoundRequest,
251    ) -> Result<AccountProofRoundFetch, AccountProofRoundFetchError> {
252        let mut requested = HashSet::with_capacity(request.claims.len());
253        for claim in &request.claims {
254            if !requested.insert(claim.address) {
255                return Err(AccountProofRoundFetchError::DuplicateRequest {
256                    address: claim.address,
257                });
258            }
259        }
260
261        let provider_requests = request
262            .claims
263            .iter()
264            .map(|claim| (claim.address, Vec::new()))
265            .collect();
266        let response = (self.fetcher)(
267            provider_requests,
268            BlockId::from((request.block_hash, Some(true))),
269        );
270
271        let mut returned: HashMap<Address, StorageFetchResult<AccountProof>> =
272            HashMap::with_capacity(response.len());
273        for (address, proof) in response {
274            if !requested.contains(&address) {
275                return Err(AccountProofRoundFetchError::UnexpectedResult { address });
276            }
277            if returned.insert(address, proof).is_some() {
278                return Err(AccountProofRoundFetchError::DuplicateResult { address });
279            }
280        }
281        for address in requested {
282            if !returned.contains_key(&address) {
283                return Err(AccountProofRoundFetchError::MissingResult { address });
284            }
285        }
286
287        let outcomes = request
288            .claims
289            .iter()
290            .map(|claim| {
291                match returned
292                    .remove(&claim.address)
293                    .expect("provider response completeness validated above")
294                {
295                    Ok(proof) if proof.code_hash == claim.expected_code_hash => {
296                        AccountProofOutcome::Verified {
297                            address: claim.address,
298                            proof,
299                        }
300                    }
301                    Ok(proof) => AccountProofOutcome::Mismatch {
302                        address: claim.address,
303                        expected: claim.expected_code_hash,
304                        actual: proof.code_hash,
305                        proof,
306                    },
307                    Err(error) => AccountProofOutcome::FetchFailed {
308                        address: claim.address,
309                        reason: error.to_string(),
310                    },
311                }
312            })
313            .collect();
314        Ok(AccountProofRoundFetch {
315            block_hash: request.block_hash,
316            outcomes,
317        })
318    }
319}
320
321/// Invalid account-proof request or malformed provider response.
322#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
323#[non_exhaustive]
324pub enum AccountProofRoundFetchError {
325    /// More than one claim targeted the same account.
326    #[error("duplicate account-proof request for {address}")]
327    DuplicateRequest {
328        /// Duplicated account.
329        address: Address,
330    },
331    /// The provider returned an account that was not requested.
332    #[error("account proof provider returned unexpected account {address}")]
333    UnexpectedResult {
334        /// Unexpected account.
335        address: Address,
336    },
337    /// The provider returned one account more than once.
338    #[error("account proof provider returned duplicate account {address}")]
339    DuplicateResult {
340        /// Duplicated account.
341        address: Address,
342    },
343    /// The provider omitted a requested account.
344    #[error("account proof provider omitted requested account {address}")]
345    MissingResult {
346        /// Omitted account.
347        address: Address,
348    },
349}
350
351/// Invalid worker-produced account/code patch.
352#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
353#[non_exhaustive]
354pub enum PreparedAccountPatchError {
355    /// The actor cache is no longer pinned to the proof hash.
356    #[error("prepared account patch baseline mismatch: prepared {prepared}, cache {cache:?}")]
357    BaselineMismatch {
358        /// Hash used by the background proof fetch.
359        prepared: B256,
360        /// Current cache hash, or `None` when number/tag-pinned.
361        cache: Option<B256>,
362    },
363    /// The patch contains multiple values for one account.
364    #[error("prepared account patch contains duplicate account {address}")]
365    DuplicateAccount {
366        /// Duplicated account.
367        address: Address,
368    },
369    /// A verified-code value carried empty runtime bytes.
370    #[error("prepared account patch contains empty runtime code for {address}")]
371    EmptyCode {
372        /// Invalid account.
373        address: Address,
374    },
375    /// A root-only account proof unexpectedly carried storage slot payloads.
376    #[error("prepared account proof for {address} unexpectedly contains {slots} storage slots")]
377    UnexpectedProofSlots {
378        /// Invalid account.
379        address: Address,
380        /// Unexpected slot count.
381        slots: usize,
382    },
383    /// Runtime bytes do not match the proof's code hash.
384    #[error("prepared runtime code hash mismatch for {address}: proof {expected}, bytes {actual}")]
385    CodeHashMismatch {
386        /// Invalid account.
387        address: Address,
388        /// Hash committed by the exact-hash proof.
389        expected: B256,
390        /// Hash of the prepared runtime bytes.
391        actual: B256,
392    },
393    /// Canonical prepared state cannot overwrite deliberate local divergence.
394    #[error("prepared canonical account patch cannot overwrite etched account {address}")]
395    EtchedAccount {
396        /// Etched account.
397        address: Address,
398    },
399    /// A newer/different seed generation already owns the account.
400    #[error(
401        "prepared account seed conflict for {address}: existing {existing}, prepared {prepared}"
402    )]
403    SeedConflict {
404        /// Conflicting account.
405        address: Address,
406        /// Existing marked-code hash.
407        existing: B256,
408        /// Worker-prepared verified hash.
409        prepared: B256,
410    },
411}