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(Ð_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(Ð_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}