risc0_steel/
verifier.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 crate::{
16    history::beacon_roots::BeaconRootsContract, state::WrapStateDb, Commitment, EvmBlockHeader,
17    EvmFactory, GuestEvmEnv,
18};
19use alloy_primitives::{B256, U256};
20use anyhow::{ensure, Context};
21
22/// Represents a verifier for validating Steel commitments within Steel.
23///
24/// The verifier is used to validate Steel commitments representing a historical blockchain state.
25///
26/// ### Usage
27/// - **Preflight verification on the Host:** To prepare verification on the host environment and
28///   build the necessary proof, use [SteelVerifier::preflight]. The environment can be initialized
29///   using the [EthEvmEnv::builder] or [EvmEnv::builder].
30/// - **Verification in the Guest:** To initialize the verifier in the guest environment, use
31///   [SteelVerifier::new]. The environment should be constructed using [EvmInput::into_env].
32///
33/// ### Examples
34/// ```rust,no_run
35/// # use risc0_steel::{ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, SteelVerifier, Commitment};
36/// # use url::Url;
37///
38/// # #[tokio::main(flavor = "current_thread")]
39/// # async fn main() -> anyhow::Result<()> {
40/// // Host:
41/// let rpc_url = Url::parse("https://ethereum-rpc.publicnode.com")?;
42/// let mut env = EthEvmEnv::builder().rpc(rpc_url).chain_spec(&ETH_MAINNET_CHAIN_SPEC).build().await?;
43///
44/// // Preflight the verification of a commitment
45/// let commitment = Commitment::default(); // Your commitment here
46/// SteelVerifier::preflight(&mut env).verify(&commitment).await?;
47///
48/// let evm_input = env.into_input().await?;
49///
50/// // Guest:
51/// let evm_env = evm_input.into_env(&ETH_MAINNET_CHAIN_SPEC);
52/// let verifier = SteelVerifier::new(&evm_env);
53/// verifier.verify(&commitment); // Panics if verification fails
54/// # Ok(())
55/// # }
56/// ```
57///
58/// [EthEvmEnv::builder]: crate::ethereum::EthEvmEnv
59/// [EvmEnv::builder]: crate::EvmEnv
60/// [EvmInput::into_env]: crate::EvmInput::into_env
61pub struct SteelVerifier<E> {
62    env: E,
63}
64
65impl<'a, F: EvmFactory> SteelVerifier<&'a GuestEvmEnv<F>> {
66    /// Constructor for verifying Steel commitments in the guest.
67    pub fn new(env: &'a GuestEvmEnv<F>) -> Self {
68        Self { env }
69    }
70
71    /// Verifies the commitment in the guest and panics on failure.
72    ///
73    /// This includes checking that the `commitment.configID` matches the
74    /// configuration ID associated with the current guest environment (`self.env.commit.configID`).
75    #[inline]
76    pub fn verify(&self, commitment: &Commitment) {
77        self.verify_with_config_id(commitment, self.env.commit.configID);
78    }
79
80    /// Verifies the commitment in the guest against an explicitly provided configuration ID,
81    /// and panics on failure.
82    pub fn verify_with_config_id(&self, commitment: &Commitment, config_id: B256) {
83        assert_eq!(commitment.configID, config_id, "Invalid config ID");
84        let (id, version) = commitment.decode_id();
85        match version {
86            0 => {
87                let block_number =
88                    validate_block_number(self.env.header().inner(), id).expect("Invalid ID");
89                let block_hash = self.env.db().block_hash(block_number);
90                assert_eq!(block_hash, commitment.digest, "Invalid digest");
91            }
92            1 => {
93                let db = WrapStateDb::new(self.env.db(), self.env.header());
94                let beacon_root = BeaconRootsContract::get_from_db(db, id)
95                    .expect("calling BeaconRootsContract failed");
96                assert_eq!(beacon_root, commitment.digest, "Invalid digest");
97            }
98            v => unimplemented!("Invalid commitment version {}", v),
99        }
100    }
101}
102
103#[cfg(feature = "host")]
104mod host {
105    use super::*;
106    use crate::{history::beacon_roots, host::HostEvmEnv};
107    use anyhow::Context;
108    use revm::Database;
109
110    impl<'a, D, F: EvmFactory, C> SteelVerifier<&'a mut HostEvmEnv<D, F, C>>
111    where
112        D: crate::EvmDatabase + Send + 'static,
113        beacon_roots::Error: From<<D as Database>::Error>,
114        anyhow::Error: From<<D as Database>::Error>,
115        <D as Database>::Error: Send + 'static,
116    {
117        /// Constructor for preflighting Steel commitment verifications on the host.
118        ///
119        /// Initializes the environment for verifying Steel commitments, fetching necessary data via
120        /// RPC, and generating a storage proof for any accessed elements using
121        /// [EvmEnv::into_input].
122        ///
123        /// [EvmEnv::into_input]: crate::EvmEnv::into_input
124        pub fn preflight(env: &'a mut HostEvmEnv<D, F, C>) -> Self {
125            Self { env }
126        }
127
128        /// Preflights the commitment verification on the host.
129        ///
130        /// This includes checking that the `commitment.configID` matches the
131        /// configuration ID associated with the current host environment.
132        #[inline]
133        pub async fn verify(self, commitment: &Commitment) -> anyhow::Result<()> {
134            let config_id = self.env.commit.config_id();
135            self.verify_with_config_id(commitment, config_id).await
136        }
137
138        /// Preflights the commitment verification on the host against an explicitly provided
139        /// configuration ID.
140        pub async fn verify_with_config_id(
141            self,
142            commitment: &Commitment,
143            config_id: B256,
144        ) -> anyhow::Result<()> {
145            log::info!("Executing preflight verifying {commitment:?}");
146
147            ensure!(commitment.configID == config_id, "invalid config ID");
148            let (id, version) = commitment.decode_id();
149            match version {
150                0 => {
151                    let block_number = validate_block_number(self.env.header().inner(), id)
152                        .context("invalid ID")?;
153                    let block_hash = self
154                        .env
155                        .spawn_with_db(move |db| db.block_hash(block_number))
156                        .await?;
157                    ensure!(block_hash == commitment.digest, "invalid digest");
158
159                    Ok(())
160                }
161                1 => {
162                    let beacon_root = self
163                        .env
164                        .spawn_with_db(move |db| BeaconRootsContract::get_from_db(db, id))
165                        .await
166                        .with_context(|| format!("calling BeaconRootsContract({id}) failed"))?;
167                    ensure!(beacon_root == commitment.digest, "invalid digest");
168
169                    Ok(())
170                }
171                v => unimplemented!("Invalid commitment version {}", v),
172            }
173        }
174    }
175}
176
177fn validate_block_number(header: &impl EvmBlockHeader, block_number: U256) -> anyhow::Result<u64> {
178    let block_number = block_number.try_into().context("invalid block number")?;
179    // if block_number > header.number(), this will also be caught in the following `ensure`
180    let diff = header.number().saturating_sub(block_number);
181    ensure!(
182        diff > 0 && diff <= 256,
183        "valid range is the last 256 blocks (not including the current one)"
184    );
185    Ok(block_number)
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::{
192        ethereum::{EthEvmEnv, ETH_MAINNET_CHAIN_SPEC},
193        test_utils::get_el_url,
194        CommitmentVersion,
195    };
196    use alloy::{
197        consensus::BlockHeader,
198        network::{primitives::HeaderResponse, BlockResponse},
199        providers::{Provider, ProviderBuilder},
200        rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag,
201    };
202    use test_log::test;
203
204    #[test(tokio::test)]
205    #[cfg_attr(
206        any(not(feature = "rpc-tests"), no_auth),
207        ignore = "RPC tests are disabled"
208    )]
209    async fn verify_block_commitment() {
210        let el = ProviderBuilder::new().connect_http(get_el_url());
211
212        // create block commitment to the previous block
213        let latest = el.get_block_number().await.unwrap();
214        let block = el
215            .get_block_by_number((latest - 1).into())
216            .await
217            .expect("eth_getBlockByNumber failed")
218            .unwrap();
219        let header = block.header();
220        let commit = Commitment::new(
221            CommitmentVersion::Block as u16,
222            header.number(),
223            header.hash(),
224            ETH_MAINNET_CHAIN_SPEC.digest(),
225        );
226
227        // preflight the verifier
228        let mut env = EthEvmEnv::builder()
229            .provider(el)
230            .chain_spec(&ETH_MAINNET_CHAIN_SPEC)
231            .build()
232            .await
233            .unwrap();
234        SteelVerifier::preflight(&mut env)
235            .verify(&commit)
236            .await
237            .unwrap();
238
239        // mock guest execution, by executing the verifier on the GuestEvmEnv
240        let env = env
241            .into_input()
242            .await
243            .unwrap()
244            .into_env(&ETH_MAINNET_CHAIN_SPEC);
245        SteelVerifier::new(&env).verify(&commit);
246    }
247
248    #[test(tokio::test)]
249    #[cfg_attr(
250        any(not(feature = "rpc-tests"), no_auth),
251        ignore = "RPC tests are disabled"
252    )]
253    async fn verify_beacon_commitment() {
254        let el = ProviderBuilder::new().connect_http(get_el_url());
255
256        // create Beacon commitment from latest block
257        let block = el
258            .get_block_by_number(AlloyBlockNumberOrTag::Latest)
259            .await
260            .expect("eth_getBlockByNumber failed")
261            .unwrap();
262        let header = block.header();
263        let commit = Commitment::new(
264            CommitmentVersion::Beacon as u16,
265            header.timestamp,
266            header.parent_beacon_block_root.unwrap(),
267            ETH_MAINNET_CHAIN_SPEC.digest(),
268        );
269
270        // preflight the verifier
271        let mut env = EthEvmEnv::builder()
272            .provider(el)
273            .chain_spec(&ETH_MAINNET_CHAIN_SPEC)
274            .build()
275            .await
276            .unwrap();
277        SteelVerifier::preflight(&mut env)
278            .verify(&commit)
279            .await
280            .unwrap();
281
282        // mock guest execution, by executing the verifier on the GuestEvmEnv
283        let env = env
284            .into_input()
285            .await
286            .unwrap()
287            .into_env(&ETH_MAINNET_CHAIN_SPEC);
288        SteelVerifier::new(&env).verify(&commit);
289    }
290}