risc0_steel/host/
mod.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
15//! Functionality that is only needed for the host and not the guest.
16
17use crate::{
18    beacon::BeaconCommit,
19    block::BlockInput,
20    config::ChainSpec,
21    ethereum::{EthEvmEnv, EthEvmInput},
22    history::HistoryCommit,
23    BlockHeaderCommit, Commitment, ComposeInput, EvmBlockHeader, EvmEnv, EvmFactory, EvmInput,
24};
25use alloy::{
26    eips::{
27        eip1898::{HexStringMissingPrefixError, ParseBlockNumberError},
28        BlockId as AlloyBlockId,
29    },
30    network::{Ethereum, Network},
31    providers::Provider,
32    rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag,
33};
34use alloy_primitives::{BlockHash, B256};
35use anyhow::{ensure, Result};
36use db::{ProofDb, ProviderDb};
37use std::{
38    fmt::{self, Debug, Display},
39    str::FromStr,
40};
41
42pub use builder::EvmEnvBuilder;
43
44mod builder;
45pub mod db;
46
47/// A Block Identifier.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
49enum BlockId {
50    /// A block hash
51    Hash(BlockHash),
52    /// A block number or tag (e.g. latest)
53    Number(BlockNumberOrTag),
54}
55
56impl BlockId {
57    /// Converts the `BlockId` into the corresponding RPC type.
58    async fn into_rpc_type<N, P>(self, provider: P) -> Result<AlloyBlockId>
59    where
60        N: Network,
61        P: Provider<N>,
62    {
63        let id = match self {
64            BlockId::Hash(hash) => hash.into(),
65            BlockId::Number(number) => match number {
66                BlockNumberOrTag::Latest => AlloyBlockNumberOrTag::Latest,
67                BlockNumberOrTag::Parent => {
68                    let latest = provider.get_block_number().await?;
69                    ensure!(latest > 0, "genesis does not have a parent");
70                    AlloyBlockNumberOrTag::Number(latest - 1)
71                }
72                BlockNumberOrTag::Safe => AlloyBlockNumberOrTag::Safe,
73                BlockNumberOrTag::Finalized => AlloyBlockNumberOrTag::Finalized,
74                BlockNumberOrTag::Number(n) => AlloyBlockNumberOrTag::Number(n),
75            }
76            .into(),
77        };
78        Ok(id)
79    }
80}
81
82impl Default for BlockId {
83    fn default() -> Self {
84        BlockId::Number(BlockNumberOrTag::default())
85    }
86}
87
88impl Display for BlockId {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::Hash(hash) => Display::fmt(&hash, f),
92            Self::Number(num) => Display::fmt(&num, f),
93        }
94    }
95}
96
97/// A block number (or tag - "latest", "safe", "finalized").
98/// This enum is used to specify which block to query when interacting with the blockchain.
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
100pub enum BlockNumberOrTag {
101    /// The most recent block in the canonical chain observed by the client.
102    #[default]
103    Latest,
104    /// The parent of the most recent block in the canonical chain observed by the client.
105    /// This is equivalent to `Latest - 1`.
106    Parent,
107    /// The most recent block considered "safe" by the client. This typically refers to a block
108    /// that is sufficiently deep in the chain to be considered irreversible.
109    Safe,
110    /// The most recent finalized block in the chain. Finalized blocks are guaranteed to be
111    /// part of the canonical chain.
112    Finalized,
113    /// A specific block number in the canonical chain.
114    Number(u64),
115}
116
117impl FromStr for BlockNumberOrTag {
118    type Err = ParseBlockNumberError;
119
120    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
121        let block = match s {
122            "latest" => Self::Latest,
123            "parent" => Self::Parent,
124            "safe" => Self::Safe,
125            "finalized" => Self::Finalized,
126            _ => {
127                if let Some(hex_val) = s.strip_prefix("0x") {
128                    let number = u64::from_str_radix(hex_val, 16);
129                    Self::Number(number?)
130                } else {
131                    return Err(HexStringMissingPrefixError::default().into());
132                }
133            }
134        };
135        Ok(block)
136    }
137}
138
139impl Display for BlockNumberOrTag {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            Self::Number(x) => write!(f, "0x{x:x}"),
143            Self::Latest => f.write_str("latest"),
144            Self::Parent => f.write_str("parent"),
145            Self::Safe => f.write_str("safe"),
146            Self::Finalized => f.write_str("finalized"),
147        }
148    }
149}
150
151/// Alias for readability, do not make public.
152pub(crate) type HostEvmEnv<D, F, C> = EvmEnv<ProofDb<D>, F, HostCommit<C>>;
153type EthHostEvmEnv<D, C> = EthEvmEnv<ProofDb<D>, HostCommit<C>>;
154
155/// Wrapper for the commit on the host.
156pub struct HostCommit<C> {
157    inner: C,
158    config_id: B256,
159}
160
161impl<C> HostCommit<C> {
162    /// Returns the config ID.
163    #[inline]
164    pub(super) fn config_id(&self) -> B256 {
165        self.config_id
166    }
167}
168
169impl<D, FACTORY: EvmFactory, C> HostEvmEnv<D, FACTORY, C>
170where
171    D: Send + 'static,
172{
173    /// Runs the provided closure that requires mutable access to the database on a thread where
174    /// blocking is acceptable.
175    ///
176    /// It panics if the closure panics.
177    /// This function is necessary because mutable references to the database cannot be passed
178    /// directly to `tokio::task::spawn_blocking`. Instead, the database is temporarily taken out of
179    /// the `HostEvmEnv`, moved into the blocking task, and then restored after the task completes.
180    pub(crate) async fn spawn_with_db<F, R>(&mut self, f: F) -> R
181    where
182        F: FnOnce(&mut ProofDb<D>) -> R + Send + 'static,
183        R: Send + 'static,
184    {
185        // as mutable references are not possible, the DB must be moved in and out of the task
186        let mut db = self.db.take().unwrap();
187
188        let (result, db) = tokio::task::spawn_blocking(move || (f(&mut db), db))
189            .await
190            .expect("DB execution panicked");
191
192        // restore the DB, so that we never return an env without a DB
193        self.db = Some(db);
194
195        result
196    }
197}
198
199impl<D, F: EvmFactory, C> HostEvmEnv<D, F, C> {
200    /// Sets the chain ID and specification ID from the given chain spec.
201    ///
202    /// This will panic when there is no valid specification ID for the current block.
203    pub fn with_chain_spec(mut self, chain_spec: &ChainSpec<F::Spec>) -> Self {
204        self.chain_id = chain_spec.chain_id;
205        self.spec = *chain_spec
206            .active_fork(self.header.number(), self.header.timestamp())
207            .unwrap();
208        self.commit.config_id = chain_spec.digest();
209
210        self
211    }
212
213    /// Extends the environment with the contents of another compatible environment.
214    ///
215    /// ### Errors
216    ///
217    /// It returns an error if the environments are inconsistent, specifically if:
218    /// - The configurations don't match
219    /// - The headers don't match
220    ///
221    /// ### Panics
222    ///
223    /// It panics if the database states conflict.
224    ///
225    /// ### Use Cases
226    ///
227    /// This method is particularly useful for combining results from parallel preflights,
228    /// allowing you to execute multiple independent operations and merge their environments.
229    ///
230    /// ### Example
231    /// ```rust,no_run
232    /// # use risc0_steel::{ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, Contract};
233    /// # use alloy_primitives::address;
234    /// # use alloy_sol_types::sol;
235    /// # #[tokio::main(flavor = "current_thread")]
236    /// # async fn main() -> anyhow::Result<()> {
237    /// # sol! {
238    /// #    interface IERC20 {
239    /// #        function balanceOf(address account) external view returns (uint);
240    /// #    }
241    /// # }
242    /// let call =
243    ///     IERC20::balanceOfCall { account: address!("F977814e90dA44bFA03b6295A0616a897441aceC") };
244    /// # let usdt_addr = address!("dAC17F958D2ee523a2206206994597C13D831ec7");
245    /// # let usdc_addr = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
246    ///
247    /// let url = "https://ethereum-rpc.publicnode.com".parse()?;
248    /// let builder = EthEvmEnv::builder().rpc(url).chain_spec(&ETH_MAINNET_CHAIN_SPEC);
249    ///
250    /// let mut env1 = builder.clone().build().await?;
251    /// let block_hash = env1.header().seal();
252    /// let mut contract1 = Contract::preflight(usdt_addr, &mut env1);
253    /// // build second env on the same block
254    /// let mut env2 = builder.block_hash(block_hash).build().await?;
255    /// let mut contract2 = Contract::preflight(usdc_addr, &mut env2);
256    ///
257    /// // Perform parallel operations (these would typically modify the state within env1/env2's dbs)
258    /// tokio::join!(contract1.call_builder(&call).call(), contract2.call_builder(&call).call());
259    ///
260    /// let env = env1.merge(env2)?;
261    /// let evm_input = env.into_input().await?;
262    /// # _ = evm_input.into_env(&ETH_MAINNET_CHAIN_SPEC);
263    /// # Ok(())
264    /// # }
265    /// ```
266    pub fn merge(self, mut other: Self) -> Result<Self> {
267        let Self {
268            mut db,
269            chain_id,
270            spec,
271            header,
272            commit,
273        } = self;
274
275        ensure!(chain_id == other.chain_id, "configuration mismatch");
276        ensure!(spec == other.spec, "configuration mismatch");
277        ensure!(
278            header.seal() == other.header.seal(),
279            "execution header mismatch"
280        );
281        // the commitments do not need to match as long as the cfg_env is consistent
282
283        // safe unwrap: EvmEnv is never returned without a DB
284        let db = db.take().unwrap();
285        let db_other = other.db.take().unwrap();
286
287        Ok(Self {
288            db: Some(db.merge(db_other)),
289            chain_id,
290            spec,
291            header,
292            commit,
293        })
294    }
295}
296
297impl<N, P, F> HostEvmEnv<ProviderDb<N, P>, F, ()>
298where
299    N: Network,
300    P: Provider<N>,
301    F: EvmFactory,
302    F::Header: TryFrom<<N as Network>::HeaderResponse>,
303    <F::Header as TryFrom<<N as Network>::HeaderResponse>>::Error: Display,
304{
305    /// Converts the environment into a [EvmInput] committing to an execution block hash.
306    pub async fn into_input(self) -> Result<EvmInput<F>> {
307        let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?;
308
309        Ok(EvmInput::Block(input))
310    }
311}
312
313impl<D, F: EvmFactory, C: Clone + BlockHeaderCommit<F::Header>> HostEvmEnv<D, F, C> {
314    /// Returns the [Commitment] used to validate the environment.
315    pub fn commitment(&self) -> Commitment {
316        self.commit
317            .inner
318            .clone()
319            .commit(&self.header, self.commit.config_id)
320    }
321}
322
323impl<P> EthHostEvmEnv<ProviderDb<Ethereum, P>, BeaconCommit>
324where
325    P: Provider<Ethereum>,
326{
327    /// Converts the environment into a [EvmInput] committing to a Beacon Chain block root.
328    pub async fn into_input(self) -> Result<EthEvmInput> {
329        let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?;
330
331        Ok(EvmInput::Beacon(ComposeInput::new(
332            input,
333            self.commit.inner,
334        )))
335    }
336}
337
338impl<P> EthHostEvmEnv<ProviderDb<Ethereum, P>, HistoryCommit>
339where
340    P: Provider<Ethereum>,
341{
342    /// Converts the environment into a [EvmInput] recursively committing to multiple Beacon Chain
343    /// block roots.
344    pub async fn into_input(self) -> Result<EthEvmInput> {
345        let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?;
346
347        Ok(EvmInput::History(ComposeInput::new(
348            input,
349            self.commit.inner,
350        )))
351    }
352}