risc0_steel/host/db/
proof.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::{event, host::db::ProviderDb, mpt::EMPTY_ROOT_HASH, MerkleTrie, StateAccount};
16use alloy::{
17    consensus::BlockHeader,
18    eips::eip2930::{AccessList, AccessListItem},
19    network::{BlockResponse, Network},
20    providers::Provider,
21    rpc::types::EIP1186AccountProofResponse,
22};
23use alloy_consensus::ReceiptEnvelope;
24use alloy_eips::eip2718::Encodable2718;
25use alloy_primitives::{
26    map::{hash_map, AddressHashMap, B256HashMap, B256HashSet, Entry, HashMap, HashSet},
27    Address, BlockNumber, Bytes, Log, StorageKey, StorageValue, B256, U256,
28};
29use alloy_rpc_types::{Filter, TransactionReceipt};
30use anyhow::{ensure, Context, Result};
31use revm::{
32    primitives::KECCAK_EMPTY,
33    state::{AccountInfo, Bytecode},
34    Database as RevmDatabase,
35};
36use std::{
37    fmt::{self, Debug},
38    hash::{BuildHasher, Hash},
39};
40
41/// A simple revm [RevmDatabase] wrapper that records all DB queries.
42pub struct ProofDb<D> {
43    accounts: AddressHashMap<B256HashSet>,
44    contracts: B256HashMap<Bytes>,
45    block_hash_numbers: HashSet<BlockNumber>,
46    log_filters: Vec<Filter>,
47    proofs: AddressHashMap<AccountProof>,
48    inner: D,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
52struct AccountProof {
53    /// The account information as stored in the account trie.
54    account: StateAccount,
55    /// The inclusion proof for this account.
56    account_proof: Vec<Bytes>,
57    /// The MPT inclusion proofs for several storage slots.
58    storage_proofs: B256HashMap<StorageProof>,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62struct StorageProof {
63    /// The value that this key holds.
64    value: StorageValue,
65    /// In MPT inclusion proof for this particular slot.
66    proof: Vec<Bytes>,
67}
68
69impl<D> Debug for ProofDb<D> {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.debug_struct("PreflightDb")
72            .field("accounts", &self.accounts)
73            .field("contracts", &self.contracts)
74            .field("block_hash_numbers", &self.block_hash_numbers)
75            .field("log_filters", &self.log_filters)
76            .finish_non_exhaustive()
77    }
78}
79
80impl<D> ProofDb<D> {
81    /// Creates a new ProofDb instance, with a [RevmDatabase].
82    pub(crate) fn new(db: D) -> Self
83    where
84        D: RevmDatabase,
85    {
86        Self {
87            accounts: Default::default(),
88            contracts: Default::default(),
89            block_hash_numbers: Default::default(),
90            log_filters: Default::default(),
91            proofs: Default::default(),
92            inner: db,
93        }
94    }
95
96    /// Adds a new response for EIP-1186 account proof `eth_getProof`.
97    ///
98    /// The proof data will be used for lookups of the referenced storage keys.
99    pub(crate) fn add_proof(&mut self, proof: EIP1186AccountProofResponse) -> Result<()> {
100        add_proof(&mut self.proofs, proof)
101    }
102
103    /// Returns the referenced contracts
104    pub(crate) fn contracts(&self) -> &B256HashMap<Bytes> {
105        &self.contracts
106    }
107
108    /// Returns the underlying [RevmDatabase].
109    pub(crate) fn inner(&self) -> &D {
110        &self.inner
111    }
112
113    /// Merges this `ProofDb` with another, consuming both and returning a new one.
114    ///
115    /// It Panics if inconsistent data is found between `self` and `other`.
116    #[must_use = "merge consumes self and returns a new ProofDb"]
117    pub(crate) fn merge(self, other: Self) -> Self {
118        let accounts = merge_checked_maps(self.accounts, other.accounts);
119        let contracts = merge_checked_maps(self.contracts, other.contracts);
120        let proofs = merge_checked_maps(self.proofs, other.proofs);
121        // HashSet::extend naturally handles duplicates
122        let mut block_hash_numbers = self.block_hash_numbers;
123        block_hash_numbers.extend(other.block_hash_numbers);
124        // use a HashSet to remove duplicates, the order does not matter for filters
125        let log_filters = self
126            .log_filters
127            .into_iter()
128            .chain(other.log_filters)
129            .collect::<HashSet<_>>()
130            .into_iter()
131            .collect();
132
133        // construct the new ProofDb using the struct literal for compile-time safety
134        ProofDb {
135            accounts,
136            contracts,
137            block_hash_numbers,
138            log_filters,
139            proofs,
140            inner: self.inner,
141        }
142    }
143}
144
145impl<N: Network, P: Provider<N>> ProofDb<ProviderDb<N, P>> {
146    /// Fetches all the EIP-1186 storage proofs from the `access_list` and stores them in the DB.
147    pub(crate) async fn add_access_list(&mut self, access_list: AccessList) -> Result<()> {
148        for AccessListItem {
149            address,
150            storage_keys,
151        } in access_list.0
152        {
153            let storage_keys: Vec<_> = storage_keys
154                .into_iter()
155                .filter(filter_existing_keys(self.proofs.get(&address)))
156                .collect();
157
158            let proof = self.get_proof(address, storage_keys).await?;
159            self.add_proof(proof)
160                .context("invalid eth_getProof response")?;
161        }
162
163        Ok(())
164    }
165
166    /// Returns the StateAccount information for the given address.
167    pub(crate) async fn state_account(&mut self, address: Address) -> Result<StateAccount> {
168        log::trace!("ACCOUNT: address={address}");
169        self.accounts.entry(address).or_default();
170
171        if !self.proofs.contains_key(&address) {
172            let proof = self.get_proof(address, vec![]).await?;
173            self.add_proof(proof)
174                .context("invalid eth_getProof response")?;
175        }
176        let proof = self.proofs.get(&address).unwrap();
177
178        Ok(proof.account)
179    }
180
181    /// Returns the proof (hash chain) of all `blockhash` calls recorded by the [RevmDatabase].
182    pub(crate) async fn ancestor_proof(
183        &self,
184        block_number: BlockNumber,
185    ) -> Result<Vec<<N as Network>::HeaderResponse>> {
186        let mut ancestors = Vec::new();
187        if let Some(&block_hash_min_number) = self.block_hash_numbers.iter().min() {
188            assert!(block_hash_min_number <= block_number);
189
190            let provider = self.inner.provider();
191            for number in (block_hash_min_number..block_number).rev() {
192                let rpc_block = provider
193                    .get_block_by_number(number.into())
194                    .await
195                    .context("eth_getBlockByNumber failed")?
196                    .with_context(|| format!("block {number} not found"))?;
197                ancestors.push(rpc_block.header().clone());
198            }
199        }
200
201        Ok(ancestors)
202    }
203
204    /// Returns the merkle proofs (sparse [MerkleTrie]) for the state and all storage queries
205    /// recorded by the [RevmDatabase].
206    pub(crate) async fn state_proof(&mut self) -> Result<(MerkleTrie, Vec<MerkleTrie>)> {
207        ensure!(
208            !self.accounts.is_empty()
209                || !self.block_hash_numbers.is_empty()
210                || !self.log_filters.is_empty(),
211            "no accounts accessed: use Contract::preflight"
212        );
213
214        // if no accounts were accessed, use the state root of the corresponding block as is
215        if self.accounts.is_empty() {
216            let hash = self.inner.block();
217            let block = self
218                .inner
219                .provider()
220                .get_block_by_hash(hash)
221                .await
222                .context("eth_getBlockByHash failed")?
223                .with_context(|| format!("block {hash} not found"))?;
224
225            return Ok((
226                MerkleTrie::from_digest(block.header().state_root()),
227                Vec::default(),
228            ));
229        }
230
231        let proofs = &mut self.proofs;
232        for (address, storage_keys) in &self.accounts {
233            let account_proof = proofs.get(address);
234            let storage_keys: Vec<_> = storage_keys
235                .iter()
236                .cloned()
237                .filter(filter_existing_keys(account_proof))
238                .collect();
239
240            if account_proof.is_none() || !storage_keys.is_empty() {
241                log::trace!("PROOF: address={}, #keys={}", address, storage_keys.len());
242                let proof = self
243                    .inner
244                    .get_proof(*address, storage_keys)
245                    .await
246                    .context("eth_getProof failed")?;
247                ensure!(
248                    &proof.address == address,
249                    "eth_getProof response does not match request"
250                );
251                add_proof(proofs, proof).context("invalid eth_getProof response")?;
252            }
253        }
254
255        let state_nodes = self
256            .accounts
257            .keys()
258            .filter_map(|address| proofs.get(address))
259            .flat_map(|proof| proof.account_proof.iter());
260        let state_trie = MerkleTrie::from_rlp_nodes(state_nodes).context("accountProof invalid")?;
261
262        let mut storage_tries: B256HashMap<MerkleTrie> = B256HashMap::default();
263        for (address, storage_keys) in &self.accounts {
264            // if no storage keys have been accessed, we don't need to prove anything
265            if storage_keys.is_empty() {
266                continue;
267            }
268
269            // safe unwrap: added a proof for each account in the previous loop
270            let proof = proofs.get(address).unwrap();
271
272            let storage_nodes = storage_keys
273                .iter()
274                .filter_map(|key| proof.storage_proofs.get(key))
275                .flat_map(|proof| proof.proof.iter());
276            let storage_root = proof.account.storage_root;
277
278            match storage_tries.entry(storage_root) {
279                Entry::Occupied(mut entry) => {
280                    // add nodes to existing trie for this root
281                    entry
282                        .get_mut()
283                        .hydrate_from_rlp_nodes(storage_nodes)
284                        .with_context(|| format!("invalid storage proof for address {address}"))?;
285                    ensure!(
286                        entry.get().hash_slow() == storage_root,
287                        "storage root mismatch"
288                    );
289                }
290                Entry::Vacant(entry) => {
291                    // create a new trie for this root
292                    let storage_trie = MerkleTrie::from_rlp_nodes(storage_nodes)
293                        .with_context(|| format!("invalid storage proof for address {address}"))?;
294                    ensure!(
295                        storage_trie.hash_slow() == storage_root,
296                        "storage root mismatch"
297                    );
298                    entry.insert(storage_trie);
299                }
300            }
301        }
302        let storage_tries = storage_tries.into_values().collect();
303
304        Ok((state_trie, storage_tries))
305    }
306
307    pub async fn receipt_proof(&self) -> Result<Option<Vec<ReceiptEnvelope>>> {
308        if self.log_filters.is_empty() {
309            return Ok(None);
310        }
311
312        let provider = self.inner.provider();
313        let block_hash = self.inner.block();
314
315        let block = provider
316            .get_block_by_hash(block_hash)
317            .await
318            .context("eth_getBlockByHash failed")?
319            .with_context(|| format!("block {block_hash} not found"))?;
320        let header = block.header();
321
322        // we don't need to include any receipts, if the Bloom filter proves the exclusion
323        let bloom_match = self
324            .log_filters
325            .iter()
326            .any(|filter| event::matches_filter(header.logs_bloom(), filter));
327        if !bloom_match {
328            return Ok(None);
329        }
330
331        let rpc_receipts = provider
332            .get_block_receipts(block_hash.into())
333            .await
334            .context("eth_getBlockReceipts failed")?
335            .with_context(|| format!("block {block_hash} not found"))?;
336
337        // convert the receipts so that they can be RLP-encoded
338        let receipts = convert_rpc_receipts::<N>(rpc_receipts, header.receipts_root())
339            .context("invalid receipts; inconsistent API response or incompatible response type")?;
340
341        Ok(Some(receipts))
342    }
343
344    async fn get_proof(
345        &self,
346        address: Address,
347        storage_keys: Vec<StorageKey>,
348    ) -> Result<EIP1186AccountProofResponse> {
349        log::trace!("PROOF: address={}, #keys={}", address, storage_keys.len());
350        let proof = self
351            .inner
352            .get_proof(address, storage_keys)
353            .await
354            .context("eth_getProof failed")?;
355        ensure!(
356            proof.address == address,
357            "eth_getProof response does not match request"
358        );
359
360        Ok(proof)
361    }
362}
363
364impl<DB: RevmDatabase> RevmDatabase for ProofDb<DB> {
365    type Error = DB::Error;
366
367    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
368        log::trace!("BASIC: address={address}");
369        self.accounts.entry(address).or_default();
370
371        // Because RevmDatabase requires that basic is always called before code_by_hash, it is just
372        // simpler to forward the query to the underlying DB.
373        self.inner.basic(address)
374    }
375
376    fn code_by_hash(&mut self, hash: B256) -> Result<Bytecode, Self::Error> {
377        log::trace!("CODE: hash={hash}");
378        let code = self.inner.code_by_hash(hash)?;
379        self.contracts.insert(hash, code.original_bytes());
380
381        Ok(code)
382    }
383
384    fn storage(&mut self, address: Address, index: U256) -> Result<U256, Self::Error> {
385        let key = StorageKey::from(index);
386        self.accounts.entry(address).or_default().insert(key);
387
388        // try to get the storage value from the loaded proofs before querying the underlying DB
389        match self
390            .proofs
391            .get(&address)
392            .and_then(|account| account.storage_proofs.get(&key))
393        {
394            Some(storage_proof) => Ok(storage_proof.value),
395            None => {
396                log::trace!("STORAGE: address={address}, index={key}");
397                self.inner.storage(address, index)
398            }
399        }
400    }
401
402    fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error> {
403        log::trace!("BLOCK: number={number}");
404        self.block_hash_numbers.insert(number);
405
406        self.inner.block_hash(number)
407    }
408}
409
410impl<DB: crate::EvmDatabase> crate::EvmDatabase for ProofDb<DB> {
411    fn logs(&mut self, filter: Filter) -> Result<Vec<Log>, <Self as RevmDatabase>::Error> {
412        log::trace!("LOGS: filter={:?}", &filter);
413        let logs = self.inner.logs(filter.clone())?;
414
415        self.log_filters.push(filter);
416
417        Ok(logs)
418    }
419}
420
421/// Merges two HashMaps, checking for consistency on overlapping keys.
422/// Panics if values for the same key are different. Consumes both maps.
423fn merge_checked_maps<K, V, S, T>(mut map: HashMap<K, V, S>, iter: T) -> HashMap<K, V, S>
424where
425    K: Eq + Hash + Debug,
426    V: PartialEq + Debug,
427    S: BuildHasher,
428    T: IntoIterator<Item = (K, V)>,
429{
430    let iter = iter.into_iter();
431    let (lower_bound, _) = iter.size_hint();
432    map.reserve(lower_bound);
433
434    for (key, value2) in iter {
435        match map.entry(key) {
436            hash_map::Entry::Vacant(entry) => {
437                entry.insert(value2);
438            }
439            hash_map::Entry::Occupied(entry) => {
440                let value1 = entry.get();
441                if value1 != &value2 {
442                    panic!(
443                        "mismatching values for key {:?}: existing={:?}, other={:?}",
444                        entry.key(),
445                        value1,
446                        value2
447                    );
448                }
449            }
450        }
451    }
452
453    map
454}
455
456fn filter_existing_keys(account_proof: Option<&AccountProof>) -> impl Fn(&StorageKey) -> bool + '_ {
457    move |key| {
458        !account_proof
459            .map(|p| p.storage_proofs.contains_key(key))
460            .unwrap_or_default()
461    }
462}
463
464fn add_proof(
465    proofs: &mut AddressHashMap<AccountProof>,
466    proof_response: EIP1186AccountProofResponse,
467) -> Result<()> {
468    // convert the response into a StorageProof
469    let storage_proofs = proof_response
470        .storage_proof
471        .into_iter()
472        .map(|proof| {
473            (
474                proof.key.as_b256(),
475                StorageProof {
476                    value: proof.value,
477                    proof: proof.proof,
478                },
479            )
480        })
481        .collect();
482
483    // eth_getProof returns an account object. However, the returned data is not always consistent.
484    // See https://github.com/ethereum/go-ethereum/issues/28441
485    let account = StateAccount {
486        nonce: proof_response.nonce,
487        balance: proof_response.balance,
488        storage_root: default_if_zero(proof_response.storage_hash, EMPTY_ROOT_HASH),
489        code_hash: default_if_zero(proof_response.code_hash, KECCAK_EMPTY),
490    };
491
492    match proofs.entry(proof_response.address) {
493        hash_map::Entry::Occupied(mut entry) => {
494            let account_proof = entry.get_mut();
495            ensure!(
496                account_proof.account == account
497                    && account_proof.account_proof == proof_response.account_proof,
498                "inconsistent proof response"
499            );
500            account_proof.storage_proofs = merge_checked_maps(
501                std::mem::take(&mut account_proof.storage_proofs),
502                storage_proofs,
503            );
504        }
505        hash_map::Entry::Vacant(entry) => {
506            entry.insert(AccountProof {
507                account,
508                account_proof: proof_response.account_proof,
509                storage_proofs,
510            });
511        }
512    }
513
514    Ok(())
515}
516
517fn default_if_zero(hash: B256, default: B256) -> B256 {
518    if hash.is_zero() {
519        default
520    } else {
521        hash
522    }
523}
524
525/// Converts an API ReceiptResponse into a vector of ReceiptEnvelope.
526fn convert_rpc_receipts<N: Network>(
527    rpc_receipts: impl IntoIterator<Item = <N as Network>::ReceiptResponse>,
528    receipts_root: B256,
529) -> Result<Vec<ReceiptEnvelope>> {
530    let receipts = rpc_receipts
531        .into_iter()
532        .map(|rpc_receipt| {
533            // Unfortunately ReceiptResponse does not implement ReceiptEnvelope, so we have to
534            // manually convert it. We convert to a TransactionReceipt which is the default and
535            // works for Ethereum-compatible networks.
536            // Use serde here for the conversion as it is much safer than mem::transmute.
537            // TODO(https://github.com/alloy-rs/alloy/issues/854): use ReceiptEnvelope directly
538            let json = serde_json::to_value(rpc_receipt).context("failed to serialize")?;
539            let tx_receipt: TransactionReceipt = serde_json::from_value(json)
540                .context("failed to parse as Ethereum transaction receipt")?;
541
542            Ok(tx_receipt.inner.into_primitives_receipt())
543        })
544        .collect::<Result<Vec<_>>>()?;
545
546    // in case the conversion did not work correctly, we check the receipts root in the header
547    let root =
548        alloy_trie::root::ordered_trie_root_with_encoder(&receipts, |r, out| r.encode_2718(out));
549    ensure!(root == receipts_root, "receipts root mismatch");
550
551    Ok(receipts)
552}