risc0_steel/host/
builder.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
15use super::BlockId;
16use crate::{
17    beacon::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::Spec>,
114    ) -> EvmEnvBuilder<P, F, &ChainSpec<F::Spec>, 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::Spec>, ()> {
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::info!(
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>::Spec>, 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::info!(
369            "Environment initialized with block {} ({})",
370            header.number(),
371            header.seal()
372        );
373
374        let beacon_url = self.beacon_config.url;
375        let version = self.beacon_config.commitment_version;
376        let commit = HostCommit {
377            inner: BeaconCommit::from_header(&header, version, &self.provider, beacon_url).await?,
378            config_id: self.chain_spec.digest(),
379        };
380        let db = ProofDb::new(ProviderDb::new(
381            self.provider,
382            self.provider_config,
383            header.seal(),
384        ));
385
386        Ok(EvmEnv::new(db, self.chain_spec, header, commit))
387    }
388}
389
390impl<P> EvmEnvBuilder<P, EthEvmFactory, &ChainSpec<<EthEvmFactory as EvmFactory>::Spec>, History> {
391    /// Configures the environment builder to generate consensus commitments.
392    ///
393    /// See [EvmEnvBuilder<P, EthBlockHeader, Beacon>::consensus_commitment] for more info.
394    pub fn consensus_commitment(mut self) -> Self {
395        self.beacon_config.beacon_config.commitment_version = CommitmentVersion::Consensus;
396        self
397    }
398    /// Builds and returns an [EvmEnv] with the configured settings, using a dedicated commitment
399    /// block that is different from the execution block.
400    pub async fn build(self) -> Result<EthHostEvmEnv<ProviderDb<Ethereum, P>, HistoryCommit>>
401    where
402        P: Provider<Ethereum>,
403    {
404        let evm_header = self.get_header(None).await?;
405        let commitment_header = self
406            .get_header(Some(self.beacon_config.commitment_block))
407            .await?;
408        ensure!(
409            evm_header.number() < commitment_header.number(),
410            "EVM execution block not before commitment block"
411        );
412
413        log::info!(
414            "Environment initialized with block {} ({})",
415            evm_header.number(),
416            evm_header.seal()
417        );
418
419        let beacon_url = self.beacon_config.beacon_config.url;
420        let commitment_version = self.beacon_config.beacon_config.commitment_version;
421        let history_commit = HistoryCommit::from_headers(
422            &evm_header,
423            &commitment_header,
424            commitment_version,
425            &self.provider,
426            beacon_url,
427        )
428        .await?;
429        let commit = HostCommit {
430            inner: history_commit,
431            config_id: self.chain_spec.digest(),
432        };
433        let db = ProofDb::new(ProviderDb::new(
434            self.provider,
435            self.provider_config,
436            evm_header.seal(),
437        ));
438
439        Ok(EvmEnv::new(db, self.chain_spec, evm_header, commit))
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use crate::{
447        ethereum::{EthEvmEnv, ETH_MAINNET_CHAIN_SPEC},
448        test_utils::{get_cl_url, get_el_url},
449        BlockHeaderCommit, Commitment, CommitmentVersion,
450    };
451    use test_log::test;
452
453    #[test(tokio::test)]
454    #[cfg_attr(
455        any(not(feature = "rpc-tests"), no_auth),
456        ignore = "RPC tests are disabled"
457    )]
458    async fn build_block_env() {
459        let builder = EthEvmEnv::builder()
460            .rpc(get_el_url())
461            .chain_spec(&ETH_MAINNET_CHAIN_SPEC);
462        // the builder should be cloneable
463        builder.clone().build().await.unwrap();
464    }
465
466    #[test(tokio::test)]
467    #[cfg_attr(
468        any(not(feature = "rpc-tests"), no_auth),
469        ignore = "RPC tests are disabled"
470    )]
471    async fn build_beacon_env() {
472        let provider = ProviderBuilder::default().connect_http(get_el_url());
473
474        let builder = EthEvmEnv::builder()
475            .provider(&provider)
476            .beacon_api(get_cl_url())
477            .block_number_or_tag(BlockNumberOrTag::Parent)
478            .chain_spec(&ETH_MAINNET_CHAIN_SPEC);
479        let env = builder.clone().build().await.unwrap();
480        let commit = env.commit.inner.commit(&env.header, env.commit.config_id);
481
482        // the commitment should verify against the parent_beacon_block_root of the child
483        let child_block = provider
484            .get_block_by_number((env.header.number() + 1).into())
485            .await
486            .unwrap();
487        let header = child_block.unwrap().header;
488        assert_eq!(
489            commit,
490            Commitment::new(
491                CommitmentVersion::Beacon as u16,
492                header.timestamp,
493                header.parent_beacon_block_root.unwrap(),
494                ETH_MAINNET_CHAIN_SPEC.digest(),
495            )
496        );
497    }
498
499    #[test(tokio::test)]
500    #[cfg_attr(
501        any(not(feature = "rpc-tests"), no_auth),
502        ignore = "RPC tests are disabled"
503    )]
504    async fn build_history_env() {
505        let provider = ProviderBuilder::default().connect_http(get_el_url());
506
507        // initialize the env at latest - 100 while committing to latest - 1
508        let latest = provider.get_block_number().await.unwrap();
509        let builder = EthEvmEnv::builder()
510            .provider(&provider)
511            .block_number_or_tag(BlockNumberOrTag::Number(latest - 8191))
512            .beacon_api(get_cl_url())
513            .commitment_block_number(latest - 1)
514            .chain_spec(&ETH_MAINNET_CHAIN_SPEC);
515        let env = builder.clone().build().await.unwrap();
516        let commit = env.commit.inner.commit(&env.header, env.commit.config_id);
517
518        // the commitment should verify against the parent_beacon_block_root of the latest block
519        let child_block = provider.get_block_by_number(latest.into()).await.unwrap();
520        let header = child_block.unwrap().header;
521        assert_eq!(
522            commit,
523            Commitment::new(
524                CommitmentVersion::Beacon as u16,
525                header.timestamp,
526                header.parent_beacon_block_root.unwrap(),
527                ETH_MAINNET_CHAIN_SPEC.digest(),
528            )
529        );
530    }
531}