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