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::{Beacon, EvmEnvBuilder, History};
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 From<u64> for BlockNumberOrTag {
140    fn from(number: u64) -> Self {
141        Self::Number(number)
142    }
143}
144
145impl Display for BlockNumberOrTag {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Self::Number(x) => write!(f, "0x{x:x}"),
149            Self::Latest => f.write_str("latest"),
150            Self::Parent => f.write_str("parent"),
151            Self::Safe => f.write_str("safe"),
152            Self::Finalized => f.write_str("finalized"),
153        }
154    }
155}
156
157/// Alias for readability, do not make public.
158pub(crate) type HostEvmEnv<D, F, C> = EvmEnv<ProofDb<D>, F, HostCommit<C>>;
159type EthHostEvmEnv<D, C> = EthEvmEnv<ProofDb<D>, HostCommit<C>>;
160
161/// Wrapper for the commit on the host.
162pub struct HostCommit<C> {
163    inner: C,
164    config_id: B256,
165}
166
167impl<C> HostCommit<C> {
168    /// Returns the config ID.
169    #[inline]
170    pub(super) fn config_id(&self) -> B256 {
171        self.config_id
172    }
173}
174
175impl<D, FACTORY: EvmFactory, C> HostEvmEnv<D, FACTORY, C>
176where
177    D: Send + 'static,
178{
179    /// Runs the provided closure that requires mutable access to the database on a thread where
180    /// blocking is acceptable.
181    ///
182    /// It panics if the closure panics.
183    /// This function is necessary because mutable references to the database cannot be passed
184    /// directly to `tokio::task::spawn_blocking`. Instead, the database is temporarily taken out of
185    /// the `HostEvmEnv`, moved into the blocking task, and then restored after the task completes.
186    pub(crate) async fn spawn_with_db<F, R>(&mut self, f: F) -> R
187    where
188        F: FnOnce(&mut ProofDb<D>) -> R + Send + 'static,
189        R: Send + 'static,
190    {
191        // as mutable references are not possible, the DB must be moved in and out of the task
192        let mut db = self.db.take().unwrap();
193
194        let (result, db) = tokio::task::spawn_blocking(move || (f(&mut db), db))
195            .await
196            .expect("DB execution panicked");
197
198        // restore the DB, so that we never return an env without a DB
199        self.db = Some(db);
200
201        result
202    }
203}
204
205impl<D, F: EvmFactory, C> HostEvmEnv<D, F, C> {
206    /// Sets the chain ID and specification ID from the given chain spec.
207    ///
208    /// This will panic when there is no valid specification ID for the current block.
209    pub fn with_chain_spec(mut self, chain_spec: &ChainSpec<F::Spec>) -> Self {
210        self.chain_id = chain_spec.chain_id;
211        self.spec = *chain_spec
212            .active_fork(self.header.number(), self.header.timestamp())
213            .unwrap();
214        self.commit.config_id = chain_spec.digest();
215
216        self
217    }
218
219    /// Extends the environment with the contents of another compatible environment.
220    ///
221    /// ### Errors
222    ///
223    /// It returns an error if the environments are inconsistent, specifically if:
224    /// - The configurations don't match
225    /// - The headers don't match
226    ///
227    /// ### Panics
228    ///
229    /// It panics if the database states conflict.
230    ///
231    /// ### Use Cases
232    ///
233    /// This method is particularly useful for combining results from parallel preflights,
234    /// allowing you to execute multiple independent operations and merge their environments.
235    ///
236    /// ### Example
237    /// ```rust,no_run
238    /// # use risc0_steel::{ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, Contract};
239    /// # use alloy_primitives::address;
240    /// # use alloy_sol_types::sol;
241    /// # #[tokio::main(flavor = "current_thread")]
242    /// # async fn main() -> anyhow::Result<()> {
243    /// # sol! {
244    /// #    interface IERC20 {
245    /// #        function balanceOf(address account) external view returns (uint);
246    /// #    }
247    /// # }
248    /// let call =
249    ///     IERC20::balanceOfCall { account: address!("F977814e90dA44bFA03b6295A0616a897441aceC") };
250    /// # let usdt_addr = address!("dAC17F958D2ee523a2206206994597C13D831ec7");
251    /// # let usdc_addr = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
252    ///
253    /// let url = "https://ethereum-rpc.publicnode.com".parse()?;
254    /// let builder = EthEvmEnv::builder().rpc(url).chain_spec(&ETH_MAINNET_CHAIN_SPEC);
255    ///
256    /// let mut env1 = builder.clone().build().await?;
257    /// let block_hash = env1.header().seal();
258    /// let mut contract1 = Contract::preflight(usdt_addr, &mut env1);
259    /// // build second env on the same block
260    /// let mut env2 = builder.block_hash(block_hash).build().await?;
261    /// let mut contract2 = Contract::preflight(usdc_addr, &mut env2);
262    ///
263    /// // Perform parallel operations (these would typically modify the state within env1/env2's dbs)
264    /// tokio::join!(contract1.call_builder(&call).call(), contract2.call_builder(&call).call());
265    ///
266    /// let env = env1.merge(env2)?;
267    /// let evm_input = env.into_input().await?;
268    /// # _ = evm_input.into_env(&ETH_MAINNET_CHAIN_SPEC);
269    /// # Ok(())
270    /// # }
271    /// ```
272    pub fn merge(self, mut other: Self) -> Result<Self> {
273        let Self {
274            mut db,
275            chain_id,
276            spec,
277            header,
278            commit,
279        } = self;
280
281        ensure!(chain_id == other.chain_id, "configuration mismatch");
282        ensure!(spec == other.spec, "configuration mismatch");
283        ensure!(
284            header.seal() == other.header.seal(),
285            "execution header mismatch"
286        );
287        // the commitments do not need to match as long as the cfg_env is consistent
288
289        // safe unwrap: EvmEnv is never returned without a DB
290        let db = db.take().unwrap();
291        let db_other = other.db.take().unwrap();
292
293        Ok(Self {
294            db: Some(db.merge(db_other)),
295            chain_id,
296            spec,
297            header,
298            commit,
299        })
300    }
301}
302
303impl<N, P, F> HostEvmEnv<ProviderDb<N, P>, F, ()>
304where
305    N: Network,
306    P: Provider<N>,
307    F: EvmFactory,
308    F::Header: TryFrom<<N as Network>::HeaderResponse>,
309    <F::Header as TryFrom<<N as Network>::HeaderResponse>>::Error: Display,
310{
311    /// Converts the environment into a [EvmInput] committing to an execution block hash.
312    pub async fn into_input(self) -> Result<EvmInput<F>> {
313        let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?;
314
315        Ok(EvmInput::Block(input))
316    }
317}
318
319impl<D, F: EvmFactory, C: Clone + BlockHeaderCommit<F::Header>> HostEvmEnv<D, F, C> {
320    /// Returns the [Commitment] used to validate the environment.
321    pub fn commitment(&self) -> Commitment {
322        self.commit
323            .inner
324            .clone()
325            .commit(&self.header, self.commit.config_id)
326    }
327}
328
329impl<P> EthHostEvmEnv<ProviderDb<Ethereum, P>, BeaconCommit>
330where
331    P: Provider<Ethereum>,
332{
333    /// Converts the environment into a [EvmInput] committing to a Beacon Chain block root.
334    pub async fn into_input(self) -> Result<EthEvmInput> {
335        let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?;
336
337        Ok(EvmInput::Beacon(ComposeInput::new(
338            input,
339            self.commit.inner,
340        )))
341    }
342}
343
344impl<P> EthHostEvmEnv<ProviderDb<Ethereum, P>, HistoryCommit>
345where
346    P: Provider<Ethereum>,
347{
348    /// Converts the environment into a [EvmInput] recursively committing to multiple Beacon Chain
349    /// block roots.
350    pub async fn into_input(self) -> Result<EthEvmInput> {
351        let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?;
352
353        Ok(EvmInput::History(ComposeInput::new(
354            input,
355            self.commit.inner,
356        )))
357    }
358}