Skip to main content

risc0_steel/host/
builder.rs

1// Copyright 2026 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 super::BlockId;
16use crate::{
17    beacon::{host::client::BeaconClient, BeaconCommit},
18    config::ChainSpec,
19    ethereum::EthEvmFactory,
20    history::HistoryCommit,
21    host::{
22        db::{ProofDb, ProviderConfig, ProviderDb},
23        BlockNumberOrTag, EthHostEvmEnv, HostCommit, HostEvmEnv,
24    },
25    CommitmentVersion, EvmBlockHeader, EvmEnv, EvmFactory,
26};
27use alloy::{
28    network::{primitives::HeaderResponse, BlockResponse, Ethereum, Network},
29    providers::{Provider, ProviderBuilder, RootProvider},
30};
31use alloy_primitives::{BlockHash, BlockNumber, Sealable, Sealed, B256};
32use anyhow::{anyhow, ensure, Context, Result};
33use std::{fmt::Display, marker::PhantomData};
34use url::Url;
35
36impl<F: EvmFactory> EvmEnv<(), F, ()> {
37    /// Creates a builder for building an environment.
38    ///
39    /// Create an Ethereum environment bast on the latest block:
40    /// ```rust,no_run
41    /// # use risc0_steel::ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv};
42    /// # use url::Url;
43    /// # #[tokio::main(flavor = "current_thread")]
44    /// # async fn main() -> anyhow::Result<()> {
45    /// let url = Url::parse("https://ethereum-rpc.publicnode.com")?;
46    /// let env = EthEvmEnv::builder().rpc(url).chain_spec(&ETH_MAINNET_CHAIN_SPEC).build().await?;
47    /// # Ok(())
48    /// # }
49    /// ```
50    pub fn builder() -> EvmEnvBuilder<(), F, (), ()> {
51        EvmEnvBuilder {
52            provider: (),
53            provider_config: ProviderConfig::default(),
54            block: BlockId::default(),
55            chain_spec: (),
56            beacon_config: (),
57            phantom: PhantomData,
58        }
59    }
60}
61
62/// Builder for constructing an [EvmEnv] instance on the host.
63///
64/// The [EvmEnvBuilder] is used to configure and create an [EvmEnv], which is the environment in
65/// which the Ethereum Virtual Machine (EVM) operates. This builder provides flexibility in setting
66/// up the EVM environment by allowing configuration of RPC endpoints, block numbers, and other
67/// parameters.
68///
69/// # Usage
70/// The builder can be created using [EvmEnv::builder()]. Various configurations can be chained to
71/// customize the environment before calling the `build` function to create the final [EvmEnv].
72#[derive(Clone, Debug)]
73pub struct EvmEnvBuilder<P, F, S, B> {
74    provider: P,
75    provider_config: ProviderConfig,
76    block: BlockId,
77    chain_spec: S,
78    beacon_config: B,
79    phantom: PhantomData<F>,
80}
81
82impl<S> EvmEnvBuilder<(), EthEvmFactory, S, ()> {
83    /// Sets the Ethereum HTTP RPC endpoint that will be used by the [EvmEnv].
84    pub fn rpc(self, url: Url) -> EvmEnvBuilder<RootProvider<Ethereum>, EthEvmFactory, S, ()> {
85        self.provider(ProviderBuilder::default().connect_http(url))
86    }
87}
88
89impl<F: EvmFactory, S> EvmEnvBuilder<(), F, S, ()> {
90    /// Sets a custom [Provider] that will be used by the [EvmEnv].
91    pub fn provider<N, P>(self, provider: P) -> EvmEnvBuilder<P, F, S, ()>
92    where
93        N: Network,
94        P: Provider<N>,
95        F::Header: TryFrom<<N as Network>::HeaderResponse>,
96        <F::Header as TryFrom<<N as Network>::HeaderResponse>>::Error: Display,
97    {
98        EvmEnvBuilder {
99            provider,
100            provider_config: self.provider_config,
101            block: self.block,
102            chain_spec: self.chain_spec,
103            beacon_config: self.beacon_config,
104            phantom: self.phantom,
105        }
106    }
107}
108
109impl<P, F: EvmFactory, B> EvmEnvBuilder<P, F, (), B> {
110    /// Sets the [ChainSpec] that will be used by the [EvmEnv].
111    pub fn chain_spec(
112        self,
113        chain_spec: &ChainSpec<F::SpecId>,
114    ) -> EvmEnvBuilder<P, F, &ChainSpec<F::SpecId>, B> {
115        EvmEnvBuilder {
116            provider: self.provider,
117            provider_config: self.provider_config,
118            block: self.block,
119            chain_spec,
120            beacon_config: self.beacon_config,
121            phantom: self.phantom,
122        }
123    }
124}
125
126/// Config for commitments to the beacon chain state.
127#[derive(Clone, Debug)]
128pub struct Beacon {
129    url: Url,
130    commitment_version: CommitmentVersion,
131}
132
133impl<P, S> EvmEnvBuilder<P, EthEvmFactory, S, ()> {
134    /// Sets the Beacon API URL for retrieving Ethereum Beacon block root commitments.
135    ///
136    /// This function configures the [EvmEnv] to interact with an Ethereum Beacon chain.
137    /// It assumes the use of the [mainnet](https://github.com/ethereum/consensus-specs/blob/v1.4.0/configs/mainnet.yaml) preset for consensus specs.
138    pub fn beacon_api(self, url: Url) -> EvmEnvBuilder<P, EthEvmFactory, S, Beacon> {
139        EvmEnvBuilder {
140            provider: self.provider,
141            provider_config: self.provider_config,
142            block: self.block,
143            chain_spec: self.chain_spec,
144            beacon_config: Beacon {
145                url,
146                commitment_version: CommitmentVersion::Beacon,
147            },
148            phantom: self.phantom,
149        }
150    }
151}
152
153impl<P, F, S, B> EvmEnvBuilder<P, F, S, B> {
154    /// Sets the block number to be used for the EVM execution.
155    pub fn block_number(self, number: u64) -> Self {
156        self.block_number_or_tag(BlockNumberOrTag::Number(number))
157    }
158
159    /// Sets the block number or block tag ("latest", "earliest", "pending") to be used for the EVM
160    /// execution.
161    pub fn block_number_or_tag(mut self, block: BlockNumberOrTag) -> Self {
162        self.block = BlockId::Number(block);
163        self
164    }
165
166    /// Sets the block hash to be used for the EVM execution.
167    pub fn block_hash(mut self, hash: B256) -> Self {
168        self.block = BlockId::Hash(hash);
169        self
170    }
171
172    /// Sets the chunk size for `eth_getProof` calls (EIP-1186).
173    ///
174    /// This configures the number of storage keys to request in a single call.
175    /// The default is 1000, but this can be adjusted based on the RPC node configuration.
176    pub fn eip1186_proof_chunk_size(mut self, chunk_size: usize) -> Self {
177        assert_ne!(chunk_size, 0, "chunk size must be non-zero");
178        self.provider_config.eip1186_proof_chunk_size = chunk_size;
179        self
180    }
181
182    /// Returns the [EvmBlockHeader] of the specified block.
183    ///
184    /// If `block` is `None`, the block based on the current builder configuration is used instead.
185    async fn get_header<N>(&self, block: Option<BlockId>) -> Result<Sealed<F::Header>>
186    where
187        F: EvmFactory,
188        N: Network,
189        P: Provider<N>,
190        F::Header: TryFrom<<N as Network>::HeaderResponse>,
191        <F::Header as TryFrom<<N as Network>::HeaderResponse>>::Error: Display,
192    {
193        let block = block.unwrap_or(self.block);
194        let block = block.into_rpc_type(&self.provider).await?;
195
196        let rpc_block = self
197            .provider
198            .get_block(block)
199            .await
200            .context("eth_getBlock1 failed")?
201            .with_context(|| format!("block {block} not found"))?;
202
203        let rpc_header = rpc_block.header().clone();
204        let header: F::Header = rpc_header
205            .try_into()
206            .map_err(|err| anyhow!("header invalid: {err}"))?;
207        let header = header.seal_slow();
208        ensure!(
209            header.seal() == rpc_block.header().hash(),
210            "computed block hash does not match the hash returned by the API"
211        );
212
213        Ok(header)
214    }
215}
216
217impl<P, F: EvmFactory> EvmEnvBuilder<P, F, &ChainSpec<F::SpecId>, ()> {
218    /// Builds and returns an [EvmEnv] with the configured settings that commits to a block hash.
219    pub async fn build<N>(self) -> Result<HostEvmEnv<ProviderDb<N, P>, F, ()>>
220    where
221        N: Network,
222        P: Provider<N>,
223        F::Header: TryFrom<<N as Network>::HeaderResponse>,
224        <F::Header as TryFrom<<N as Network>::HeaderResponse>>::Error: Display,
225    {
226        let header = self.get_header(None).await?;
227        log::debug!(
228            "Environment initialized with block {} ({})",
229            header.number(),
230            header.seal()
231        );
232
233        let db = ProofDb::new(ProviderDb::new(
234            self.provider,
235            self.provider_config,
236            header.seal(),
237        ));
238        let commit = HostCommit {
239            inner: (),
240            config_id: self.chain_spec.digest(),
241        };
242
243        Ok(EvmEnv::new(db, self.chain_spec, header, commit))
244    }
245}
246
247/// Config for separating the execution block from the commitment block.
248#[derive(Clone, Debug)]
249pub struct History {
250    beacon_config: Beacon,
251    commitment_block: BlockId,
252}
253
254impl<P, S> EvmEnvBuilder<P, EthEvmFactory, S, Beacon> {
255    /// Configures the environment builder to generate consensus commitments.
256    ///
257    /// A consensus commitment contains the beacon block root indexed directly by its slot number.
258    /// This is in contrast to the default mechanism, which relies on timestamps for lookups, for
259    /// verification using the EIP-4788 beacon root contract deployed at the execution layer.
260    ///
261    /// The use of slot-based indexing is particularly beneficial for verification methods that have
262    /// direct access to the state of the beacon chain, such as systems using beacon light clients.
263    /// This allows the commitment to be verified directly against the state of the consensus layer.
264    ///
265    /// # Example
266    /// ```rust,no_run
267    /// # use risc0_steel::ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv};
268    /// # use alloy_primitives::B256;
269    /// # use url::Url;
270    /// # use std::str::FromStr;
271    /// # #[tokio::main(flavor = "current_thread")]
272    /// # async fn main() -> anyhow::Result<()> {
273    /// let builder = EthEvmEnv::builder()
274    ///     .rpc(Url::parse("https://ethereum-rpc.publicnode.com")?)
275    ///     .beacon_api(Url::parse("https://ethereum-beacon-api.publicnode.com")?)
276    ///     .chain_spec(&ETH_MAINNET_CHAIN_SPEC)
277    ///     // Configure the builder to use slot-indexed consensus commitments.
278    ///     .consensus_commitment();
279    ///
280    /// // The resulting 'env' will be configured to generate a consensus commitment
281    /// // (beacon root indexed by slot) when processing blocks or state.
282    /// let env = builder.build().await?;
283    /// # Ok(())
284    /// # }
285    /// ```
286    pub fn consensus_commitment(mut self) -> Self {
287        self.beacon_config.commitment_version = CommitmentVersion::Consensus;
288        self
289    }
290
291    /// Sets the block hash for the commitment block, which can be different from the execution
292    /// block.
293    ///
294    /// This allows for historical state execution while maintaining security through a more recent
295    /// commitment. The commitment block must be more recent than the execution block.
296    ///
297    /// Note that this feature requires a Beacon chain RPC provider, as it relies on
298    /// [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788).
299    ///
300    /// # Example
301    /// ```rust,no_run
302    /// # use risc0_steel::ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv};
303    /// # use alloy_primitives::B256;
304    /// # use url::Url;
305    /// # use std::str::FromStr;
306    /// # #[tokio::main(flavor = "current_thread")]
307    /// # async fn main() -> anyhow::Result<()> {
308    /// let commitment_hash = B256::from_str("0x1234...")?;
309    /// let builder = EthEvmEnv::builder()
310    ///     .rpc(Url::parse("https://ethereum-rpc.publicnode.com")?)
311    ///     .beacon_api(Url::parse("https://ethereum-beacon-api.publicnode.com")?)
312    ///     .block_number(1_000_000) // execute against historical state
313    ///     .commitment_block_hash(commitment_hash) // commit to recent block
314    ///     .chain_spec(&ETH_MAINNET_CHAIN_SPEC);
315    /// let env = builder.build().await?;
316    /// # Ok(())
317    /// # }
318    /// ```
319    pub fn commitment_block_hash(
320        self,
321        hash: BlockHash,
322    ) -> EvmEnvBuilder<P, EthEvmFactory, S, History> {
323        self.commitment_block(BlockId::Hash(hash))
324    }
325
326    /// Sets the block number or block tag ("latest", "earliest", "pending")  for the commitment.
327    ///
328    /// See [EvmEnvBuilder::commitment_block_hash] for detailed documentation.
329    pub fn commitment_block_number_or_tag(
330        self,
331        block: BlockNumberOrTag,
332    ) -> EvmEnvBuilder<P, EthEvmFactory, S, History> {
333        self.commitment_block(BlockId::Number(block))
334    }
335
336    /// Sets the block number for the commitment.
337    ///
338    /// See [EvmEnvBuilder::commitment_block_hash] for detailed documentation.
339    pub fn commitment_block_number(
340        self,
341        number: BlockNumber,
342    ) -> EvmEnvBuilder<P, EthEvmFactory, S, History> {
343        self.commitment_block_number_or_tag(BlockNumberOrTag::Number(number))
344    }
345
346    fn commitment_block(self, block: BlockId) -> EvmEnvBuilder<P, EthEvmFactory, S, History> {
347        EvmEnvBuilder {
348            provider: self.provider,
349            provider_config: self.provider_config,
350            block: self.block,
351            chain_spec: self.chain_spec,
352            beacon_config: History {
353                beacon_config: self.beacon_config,
354                commitment_block: block,
355            },
356            phantom: Default::default(),
357        }
358    }
359}
360
361impl<P> EvmEnvBuilder<P, EthEvmFactory, &ChainSpec<<EthEvmFactory as EvmFactory>::SpecId>, Beacon> {
362    /// Builds and returns an [EvmEnv] with the configured settings that commits to a beacon root.
363    pub async fn build(self) -> Result<EthHostEvmEnv<ProviderDb<Ethereum, P>, BeaconCommit>>
364    where
365        P: Provider<Ethereum>,
366    {
367        let header = self.get_header(None).await?;
368        log::debug!(
369            "Environment initialized with block {} ({})",
370            header.number(),
371            header.seal()
372        );
373
374        let beacon_client = BeaconClient::new(self.beacon_config.url)
375            .map_err(|e| anyhow::anyhow!("invalid beacon URL: {e}"))?;
376        let version = self.beacon_config.commitment_version;
377        let commit = HostCommit {
378            inner: BeaconCommit::from_header(&header, version, &self.provider, &beacon_client)
379                .await?,
380            config_id: self.chain_spec.digest(),
381        };
382        let db = ProofDb::new(ProviderDb::new(
383            self.provider,
384            self.provider_config,
385            header.seal(),
386        ));
387
388        Ok(EvmEnv::new(db, self.chain_spec, header, commit))
389    }
390}
391
392impl<P>
393    EvmEnvBuilder<P, EthEvmFactory, &ChainSpec<<EthEvmFactory as EvmFactory>::SpecId>, History>
394{
395    /// Configures the environment builder to generate consensus commitments.
396    ///
397    /// See [EvmEnvBuilder<P, EthBlockHeader, Beacon>::consensus_commitment] for more info.
398    pub fn consensus_commitment(mut self) -> Self {
399        self.beacon_config.beacon_config.commitment_version = CommitmentVersion::Consensus;
400        self
401    }
402    /// Builds and returns an [EvmEnv] with the configured settings, using a dedicated commitment
403    /// block that is different from the execution block.
404    pub async fn build(self) -> Result<EthHostEvmEnv<ProviderDb<Ethereum, P>, HistoryCommit>>
405    where
406        P: Provider<Ethereum>,
407    {
408        let evm_header = self.get_header(None).await?;
409        let commitment_header = self
410            .get_header(Some(self.beacon_config.commitment_block))
411            .await?;
412        ensure!(
413            evm_header.number() < commitment_header.number(),
414            "EVM execution block not before commitment block"
415        );
416
417        log::debug!(
418            "Environment initialized with block {} ({})",
419            evm_header.number(),
420            evm_header.seal()
421        );
422
423        let beacon_url = self.beacon_config.beacon_config.url;
424        let commitment_version = self.beacon_config.beacon_config.commitment_version;
425        let history_commit = HistoryCommit::from_headers(
426            &evm_header,
427            &commitment_header,
428            commitment_version,
429            &self.provider,
430            beacon_url,
431        )
432        .await?;
433        let commit = HostCommit {
434            inner: history_commit,
435            config_id: self.chain_spec.digest(),
436        };
437        let db = ProofDb::new(ProviderDb::new(
438            self.provider,
439            self.provider_config,
440            evm_header.seal(),
441        ));
442
443        Ok(EvmEnv::new(db, self.chain_spec, evm_header, commit))
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use crate::{
451        ethereum::{EthEvmEnv, ETH_MAINNET_CHAIN_SPEC},
452        test_utils::{get_cl_url, get_el_url},
453        BlockHeaderCommit, Commitment, CommitmentVersion,
454    };
455    use test_log::test;
456
457    #[test(tokio::test)]
458    #[cfg_attr(
459        any(not(feature = "rpc-tests"), no_auth),
460        ignore = "RPC tests are disabled"
461    )]
462    async fn build_block_env() {
463        let builder = EthEvmEnv::builder()
464            .rpc(get_el_url())
465            .chain_spec(&ETH_MAINNET_CHAIN_SPEC);
466        // the builder should be cloneable
467        builder.clone().build().await.unwrap();
468    }
469
470    #[test(tokio::test)]
471    #[cfg_attr(
472        any(not(feature = "rpc-tests"), no_auth),
473        ignore = "RPC tests are disabled"
474    )]
475    async fn build_beacon_env() {
476        let provider = ProviderBuilder::default().connect_http(get_el_url());
477
478        let builder = EthEvmEnv::builder()
479            .provider(&provider)
480            .beacon_api(get_cl_url())
481            .block_number_or_tag(BlockNumberOrTag::Parent)
482            .chain_spec(&ETH_MAINNET_CHAIN_SPEC);
483        let env = builder.clone().build().await.unwrap();
484        let commit = env.commit.inner.commit(&env.header, env.commit.config_id);
485
486        // the commitment should verify against the parent_beacon_block_root of the child
487        let child_block = provider
488            .get_block_by_number((env.header.number() + 1).into())
489            .await
490            .unwrap();
491        let header = child_block.unwrap().header;
492        assert_eq!(
493            commit,
494            Commitment::new(
495                CommitmentVersion::Beacon as u16,
496                header.timestamp,
497                header.parent_beacon_block_root.unwrap(),
498                ETH_MAINNET_CHAIN_SPEC.digest(),
499            )
500        );
501    }
502
503    #[test(tokio::test)]
504    #[cfg_attr(
505        any(not(feature = "rpc-tests"), no_auth),
506        ignore = "RPC tests are disabled"
507    )]
508    async fn build_history_env() {
509        let provider = ProviderBuilder::default().connect_http(get_el_url());
510
511        // initialize the env at latest - 100 while committing to latest - 1
512        let latest = provider.get_block_number().await.unwrap();
513        let builder = EthEvmEnv::builder()
514            .provider(&provider)
515            .block_number_or_tag(BlockNumberOrTag::Number(latest - 8191))
516            .beacon_api(get_cl_url())
517            .commitment_block_number(latest - 1)
518            .chain_spec(&ETH_MAINNET_CHAIN_SPEC);
519        let env = builder.clone().build().await.unwrap();
520        let commit = env.commit.inner.commit(&env.header, env.commit.config_id);
521
522        // the commitment should verify against the parent_beacon_block_root of the latest block
523        let child_block = provider.get_block_by_number(latest.into()).await.unwrap();
524        let header = child_block.unwrap().header;
525        assert_eq!(
526            commit,
527            Commitment::new(
528                CommitmentVersion::Beacon as u16,
529                header.timestamp,
530                header.parent_beacon_block_root.unwrap(),
531                ETH_MAINNET_CHAIN_SPEC.digest(),
532            )
533        );
534    }
535}