risc0_steel/host/db/
provider.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 alloy::{
16    network::{primitives::HeaderResponse, BlockResponse, Network},
17    providers::Provider,
18    rpc::types::EIP1186AccountProofResponse,
19    transports::TransportError,
20};
21use alloy_primitives::{map::B256HashMap, Address, BlockHash, Log, StorageKey, B256, U256};
22use alloy_rpc_types::Filter;
23use revm::{
24    database::DBErrorMarker,
25    primitives::KECCAK_EMPTY,
26    state::{AccountInfo, Bytecode},
27    Database as RevmDatabase,
28};
29use std::{future::IntoFuture, marker::PhantomData};
30use tokio::runtime::Handle;
31
32/// Errors returned by the [ProviderDb].
33#[derive(Debug, thiserror::Error)]
34pub enum Error {
35    #[error("{0} failed")]
36    Rpc(&'static str, #[source] TransportError),
37    #[error("block not found")]
38    BlockNotFound,
39    #[error("inconsistent RPC response: {0}")]
40    InconsistentResponse(&'static str),
41}
42
43impl DBErrorMarker for Error {}
44
45/// A [RevmDatabase] backed by an alloy [Provider].
46///
47/// When accessing the database, it'll use the given provider to fetch the corresponding account's
48/// data. It will block the current thread to execute provider calls, Therefore, its methods
49/// must *not* be executed inside an async runtime, or it will panic when trying to block. If the
50/// immediate context is only synchronous, but a transitive caller is async, use
51/// [tokio::task::spawn_blocking] around the calls that need to be blocked.
52pub struct ProviderDb<N: Network, P: Provider<N>> {
53    /// Provider to fetch the data from.
54    provider: P,
55    /// Configuration of the provider.
56    provider_config: ProviderConfig,
57    /// Hash of the block on which the queries will be based.
58    block: BlockHash,
59    /// Handle to the Tokio runtime.
60    handle: Handle,
61    /// Bytecode cache to allow querying bytecode by hash instead of address.
62    contracts: B256HashMap<Bytecode>,
63
64    phantom: PhantomData<N>,
65}
66
67/// Additional configuration for a [Provider].
68#[derive(Clone, Debug)]
69#[non_exhaustive]
70pub(crate) struct ProviderConfig {
71    /// Max number of storage keys to request in a single `eth_getProof` call.
72    pub eip1186_proof_chunk_size: usize,
73}
74
75impl Default for ProviderConfig {
76    fn default() -> Self {
77        Self {
78            eip1186_proof_chunk_size: 1000,
79        }
80    }
81}
82
83impl<N: Network, P: Provider<N>> ProviderDb<N, P> {
84    /// Creates a new AlloyDb instance, with a [Provider] and a block.
85    ///
86    /// This will panic if called outside the context of a Tokio runtime.
87    pub(crate) fn new(provider: P, config: ProviderConfig, block_hash: BlockHash) -> Self {
88        Self {
89            provider,
90            provider_config: config,
91            block: block_hash,
92            handle: Handle::current(),
93            contracts: Default::default(),
94            phantom: PhantomData,
95        }
96    }
97
98    /// Returns the [Provider].
99    pub(crate) fn provider(&self) -> &P {
100        &self.provider
101    }
102
103    /// Returns the block hash used for the queries.
104    pub(crate) fn block(&self) -> BlockHash {
105        self.block
106    }
107
108    /// Get the EIP-1186 account and storage merkle proofs.
109    pub(crate) async fn get_proof(
110        &self,
111        address: Address,
112        mut keys: Vec<StorageKey>,
113    ) -> Result<EIP1186AccountProofResponse, Error> {
114        let block = self.block();
115
116        // for certain RPC nodes it seemed beneficial when the keys are in the correct order
117        keys.sort_unstable();
118
119        let mut iter = keys.chunks(self.provider_config.eip1186_proof_chunk_size);
120        // always make at least one call even if the keys are empty
121        let mut account_proof = self
122            .provider()
123            .get_proof(address, iter.next().unwrap_or_default().into())
124            .hash(block)
125            .await
126            .map_err(|err| Error::Rpc("eth_getProof", err))?;
127        for keys in iter {
128            let proof = self
129                .provider()
130                .get_proof(address, keys.into())
131                .hash(block)
132                .await
133                .map_err(|err| Error::Rpc("eth_getProof", err))?;
134            // only the keys have changed, the account proof should not change
135            if proof.account_proof != account_proof.account_proof {
136                return Err(Error::InconsistentResponse(
137                    "account_proof not consistent between calls",
138                ));
139            }
140
141            account_proof.storage_proof.extend(proof.storage_proof);
142        }
143
144        Ok(account_proof)
145    }
146}
147
148impl<N: Network, P: Provider<N>> RevmDatabase for ProviderDb<N, P> {
149    type Error = Error;
150
151    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
152        let f = async {
153            let get_nonce = self
154                .provider
155                .get_transaction_count(address)
156                .hash(self.block);
157            let get_balance = self.provider.get_balance(address).hash(self.block);
158            let get_code = self.provider.get_code_at(address).hash(self.block);
159
160            tokio::join!(
161                get_nonce.into_future(),
162                get_balance.into_future(),
163                get_code.into_future()
164            )
165        };
166        let (nonce, balance, code) = self.handle.block_on(f);
167
168        let nonce = nonce.map_err(|err| Error::Rpc("eth_getTransactionCount", err))?;
169        let balance = balance.map_err(|err| Error::Rpc("eth_getBalance", err))?;
170        let code = code.map_err(|err| Error::Rpc("eth_getCode", err))?;
171        let bytecode = Bytecode::new_raw(code.0.into());
172
173        // if the account is empty return None
174        // in the EVM, emptiness is treated as equivalent to nonexistence
175        if nonce == 0 && balance.is_zero() && bytecode.is_empty() {
176            return Ok(None);
177        }
178
179        // index the code by its hash, so that we can later use code_by_hash
180        let code_hash = bytecode.hash_slow();
181        self.contracts.insert(code_hash, bytecode);
182
183        Ok(Some(AccountInfo {
184            nonce,
185            balance,
186            code_hash,
187            code: None, // will be queried later using code_by_hash
188        }))
189    }
190
191    fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
192        if code_hash == KECCAK_EMPTY {
193            return Ok(Bytecode::new());
194        }
195
196        // this works because `basic` is always called first
197        let code = self
198            .contracts
199            .get(&code_hash)
200            .expect("`basic` must be called first for the corresponding account");
201
202        Ok(code.clone())
203    }
204
205    fn storage(&mut self, address: Address, index: U256) -> Result<U256, Self::Error> {
206        let storage = self
207            .handle
208            .block_on(
209                self.provider
210                    .get_storage_at(address, index)
211                    .hash(self.block)
212                    .into_future(),
213            )
214            .map_err(|err| Error::Rpc("eth_getStorageAt", err))?;
215
216        Ok(storage)
217    }
218
219    fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error> {
220        let block_response = self
221            .handle
222            .block_on(
223                self.provider
224                    .get_block_by_number(number.into())
225                    .into_future(),
226            )
227            .map_err(|err| Error::Rpc("eth_getBlockByNumber", err))?;
228        let block = block_response.ok_or(Error::BlockNotFound)?;
229
230        Ok(block.header().hash())
231    }
232}
233
234impl<N: Network, P: Provider<N>> crate::EvmDatabase for ProviderDb<N, P> {
235    fn logs(&mut self, filter: Filter) -> Result<Vec<Log>, <Self as RevmDatabase>::Error> {
236        assert_eq!(filter.get_block_hash(), Some(self.block()));
237        let rpc_logs = self
238            .handle
239            .block_on(self.provider.get_logs(&filter))
240            .map_err(|err| Error::Rpc("eth_getLogs", err))?;
241
242        Ok(rpc_logs.into_iter().map(|log| log.inner).collect())
243    }
244}