From 9c1c0a0479e8dfc5a137ceb9b9b99714bee6400f Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 4 Nov 2024 17:21:35 +0100 Subject: [PATCH 01/21] add commitment verifier --- steel/src/history/beacon_roots.rs | 232 ++++++++++++++++++------------ steel/src/history/mod.rs | 26 ++-- steel/src/host/db/mod.rs | 2 +- steel/src/lib.rs | 6 +- steel/src/state.rs | 6 +- steel/src/verifier.rs | 141 ++++++++++++++++++ 6 files changed, 301 insertions(+), 112 deletions(-) create mode 100644 steel/src/verifier.rs diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index d7c21792..4d53d1f3 100644 --- a/steel/src/history/beacon_roots.rs +++ b/steel/src/history/beacon_roots.rs @@ -14,7 +14,23 @@ use crate::{MerkleTrie, StateAccount}; use alloy_primitives::{address, b256, keccak256, uint, Address, B256, U256}; +use revm::primitives::{AccountInfo, Bytecode}; +use revm::Database; use serde::{Deserialize, Serialize}; +use std::convert::Infallible; +use anyhow::anyhow; + +/// The length of the buffer that stores historical entries, i.e., the number of stored +/// timestamps and roots. +pub const HISTORY_BUFFER_LENGTH: U256 = uint!(8191_U256); +/// Address where the contract is deployed. +pub const ADDRESS: Address = address!("000F3df6D732807Ef1319fB7B8bB8522d0Beac02"); + +/// Hash of the contract's address, where the contract is deployed. +const ADDRESS_HASH: B256 = + b256!("37d65eaa92c6bc4c13a5ec45527f0c18ea8932588728769ec7aecfe6d9f32e42"); +/// Hash of the deployed EVM bytecode. +const CODE_HASH: B256 = b256!("f57acd40259872606d76197ef052f3d35588dadf919ee1f0e3cb9b62d3f4b02c"); /// Enum representing possible errors that can occur within the `BeaconRootsContract`. #[derive(Debug, thiserror::Error)] @@ -31,104 +47,39 @@ pub enum Error { /// Error indicating that the contract execution was reverted. #[error("execution reverted")] Reverted, + /// Unspecified error. + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +#[cfg(feature = "host")] +impl From for Error { + fn from(value: crate::host::db::alloy::Error) -> Self { + anyhow::Error::new(value).into() + } +} + +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } } /// The `State` struct represents the state of the contract. #[derive(Clone, Serialize, Deserialize)] -pub struct State { +pub struct BeaconRootsState { /// EVM (global) state trie with path to the contract account. state_trie: MerkleTrie, /// Storage trie containing the state of the beacon root contract. storage_trie: MerkleTrie, } -impl State { +impl BeaconRootsState { /// Computes the state root. #[inline] pub fn root(&self) -> B256 { self.state_trie.hash_slow() } -} - -/// The `BeaconRootsContract` is responsible for storing and retrieving historical beacon roots. -/// -/// It is an exact reimplementation of the beacon roots contract as defined in [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788). -/// It is deployed at the address `000F3df6D732807Ef1319fB7B8bB8522d0Beac02` and has the -/// following storage layout: -/// - `timestamp_idx = timestamp % HISTORY_BUFFER_LENGTH`: Stores the timestamp at this index. -/// - `root_idx = timestamp_idx + HISTORY_BUFFER_LENGTH`: Stores the beacon root at this index. -pub struct BeaconRootsContract { - storage: MerkleTrie, -} - -impl BeaconRootsContract { - /// The length of the buffer that stores historical entries, i.e., the number of stored - /// timestamps and roots. - pub const HISTORY_BUFFER_LENGTH: U256 = uint!(8191_U256); - /// Address where the contract is deployed. - #[allow(dead_code)] - pub const ADDRESS: Address = address!("000F3df6D732807Ef1319fB7B8bB8522d0Beac02"); - - /// Hash of the contract's address, where the contract is deployed. - const ADDRESS_HASH: B256 = - b256!("37d65eaa92c6bc4c13a5ec45527f0c18ea8932588728769ec7aecfe6d9f32e42"); - /// Hash of the deployed EVM bytecode. - const CODE_HASH: B256 = - b256!("f57acd40259872606d76197ef052f3d35588dadf919ee1f0e3cb9b62d3f4b02c"); - - /// Creates a new instance of the `BeaconRootsContract` by verifying the provided state. - pub fn new(state: State) -> Result { - // retrieve the account data from the state trie using the contract's address hash - let account: StateAccount = state - .state_trie - .get_rlp(Self::ADDRESS_HASH)? - .unwrap_or_default(); - // validate the account's code hash and storage root - if account.code_hash != Self::CODE_HASH { - return Err(Error::NoContract); - } - let storage = state.storage_trie; - if storage.hash_slow() != account.storage_root { - return Err(Error::InvalidState); - } - - Ok(Self { storage }) - } - - /// Retrieves the root associated with the provided `calldata` (timestamp). - /// - /// This behaves exactly like the EVM bytecode defined in EIP-4788. - pub fn get(&self, calldata: U256) -> Result { - if calldata.is_zero() { - return Err(Error::Reverted); - } - - let timestamp_idx = calldata % Self::HISTORY_BUFFER_LENGTH; - let timestamp = self.storage_get(timestamp_idx)?; - - if timestamp != calldata { - return Err(Error::Reverted); - } - - let root_idx = timestamp_idx + Self::HISTORY_BUFFER_LENGTH; - let root = self.storage_get(root_idx)?; - - Ok(root.into()) - } - - /// Retrieves the root from a given `State` based on the provided `calldata` (timestamp). - #[inline] - pub fn get_from_state(state: State, calldata: U256) -> Result { - Self::new(state)?.get(calldata) - } - - /// Retrieves a value from the contract's storage at the given index. - fn storage_get(&self, index: U256) -> Result { - Ok(self - .storage - .get_rlp(keccak256(index.to_be_bytes::<32>()))? - .unwrap_or_default()) - } /// Prepares and retrieves the beacon root from an RPC provider by constructing the /// necessary proof. @@ -142,7 +93,7 @@ impl BeaconRootsContract { calldata: U256, provider: P, block_id: alloy::eips::BlockId, - ) -> anyhow::Result<(B256, State)> + ) -> anyhow::Result<(B256, BeaconRootsState)> where T: alloy::transports::Transport + Clone, N: alloy::network::Network, @@ -151,16 +102,16 @@ impl BeaconRootsContract { use anyhow::{anyhow, Context}; // compute the keys of the two storage slots that will be accessed - let timestamp_idx = calldata % Self::HISTORY_BUFFER_LENGTH; - let root_idx = timestamp_idx + Self::HISTORY_BUFFER_LENGTH; + let timestamp_idx = calldata % HISTORY_BUFFER_LENGTH; + let root_idx = timestamp_idx + HISTORY_BUFFER_LENGTH; // derive the minimal state needed to query and validate let proof = provider - .get_proof(Self::ADDRESS, vec![timestamp_idx.into(), root_idx.into()]) + .get_proof(ADDRESS, vec![timestamp_idx.into(), root_idx.into()]) .block_id(block_id) .await .context("eth_getProof failed")?; - let state = State { + let mut state = BeaconRootsState { state_trie: MerkleTrie::from_rlp_nodes(proof.account_proof) .context("accountProof invalid")?, storage_trie: MerkleTrie::from_rlp_nodes( @@ -170,7 +121,7 @@ impl BeaconRootsContract { }; // validate the returned state and compute the return value - match Self::get_from_state(state.clone(), calldata) { + match BeaconRootsContract::get_from_db(&mut state, calldata) { Ok(returns) => Ok((returns, state)), Err(err) => match err { Error::Reverted => Err(anyhow!("BeaconRootsContract({}) reverted", calldata)), @@ -180,6 +131,103 @@ impl BeaconRootsContract { } } +impl Database for BeaconRootsState { + type Error = Error; + + #[inline(always)] + fn basic(&mut self, address: Address) -> Result, Self::Error> { + assert_eq!(address, ADDRESS); + let account: Option = self.state_trie.get_rlp(ADDRESS_HASH)?; + match account { + Some(account) => { + if self.storage_trie.hash_slow() != account.storage_root { + return Err(Error::InvalidState); + } + Ok(Some(AccountInfo { + balance: account.balance, + nonce: account.nonce, + code_hash: account.code_hash, + code: None, + })) + } + None => Ok(None), + } + } + + fn code_by_hash(&mut self, _code_hash: B256) -> Result { + unreachable!() + } + + #[inline(always)] + fn storage(&mut self, address: Address, index: U256) -> Result { + assert_eq!(address, ADDRESS); + Ok(self + .storage_trie + .get_rlp(keccak256(index.to_be_bytes::<32>()))? + .unwrap_or_default()) + } + + fn block_hash(&mut self, _number: u64) -> Result { + unreachable!() + } +} + +/// The `BeaconRootsContract` is responsible for storing and retrieving historical beacon roots. +/// +/// It is an exact reimplementation of the beacon roots contract as defined in [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788). +/// It is deployed at the address `000F3df6D732807Ef1319fB7B8bB8522d0Beac02` and has the +/// following storage layout: +/// - `timestamp_idx = timestamp % HISTORY_BUFFER_LENGTH`: Stores the timestamp at this index. +/// - `root_idx = timestamp_idx + HISTORY_BUFFER_LENGTH`: Stores the beacon root at this index. +pub struct BeaconRootsContract { + db: D, +} + +impl BeaconRootsContract +where + D: Database, + Error: From<::Error>, +{ + /// Creates a new instance of the `BeaconRootsContract` by verifying the provided state. + pub fn new(mut db: D) -> Result { + // retrieve the account data from the state trie using the contract's address hash + let account = db.basic(ADDRESS)?.unwrap_or_default(); + // validate the account's code hash and storage root + if account.code_hash != CODE_HASH { + return Err(Error::NoContract); + } + + Ok(Self { db }) + } + + /// Retrieves the root associated with the provided `calldata` (timestamp). + /// + /// This behaves exactly like the EVM bytecode defined in EIP-4788. + pub fn get(&mut self, calldata: U256) -> Result { + if calldata.is_zero() { + return Err(Error::Reverted); + } + + let timestamp_idx = calldata % HISTORY_BUFFER_LENGTH; + let timestamp = self.db.storage(ADDRESS, timestamp_idx)?; + + if timestamp != calldata { + return Err(Error::Reverted); + } + + let root_idx = timestamp_idx + HISTORY_BUFFER_LENGTH; + let root = self.db.storage(ADDRESS, root_idx)?; + + Ok(root.into()) + } + + /// Retrieves the root from a given `State` based on the provided `calldata` (timestamp). + #[inline] + pub fn get_from_db(db: D, calldata: U256) -> Result { + Self::new(db)?.get(calldata) + } +} + #[cfg(test)] mod tests { use super::*; @@ -207,8 +255,8 @@ mod tests { // query the contract for the latest timestamp, this should return parent_beacon_block_root let calldata = U256::from(header.timestamp); - let (preflight, state) = - BeaconRootsContract::preflight_get(calldata, el, header.hash.into()) + let (preflight, mut state) = + BeaconRootsState::preflight_get(calldata, el, header.hash.into()) .await .expect("preflighting BeaconRootsContract failed"); assert_eq!(state.root(), header.state_root); @@ -217,7 +265,7 @@ mod tests { // executing the contract from the exact state should return the same value assert_eq!( preflight, - dbg!(BeaconRootsContract::get_from_state(state, calldata)).unwrap() + dbg!(BeaconRootsContract::get_from_db(&mut state, calldata)).unwrap() ); } } diff --git a/steel/src/history/mod.rs b/steel/src/history/mod.rs index 02bf672f..5eb9ebdd 100644 --- a/steel/src/history/mod.rs +++ b/steel/src/history/mod.rs @@ -20,7 +20,7 @@ use alloy_primitives::{Sealed, B256, U256}; use beacon::{BeaconCommit, GeneralizedBeaconCommit, STATE_ROOT_LEAF_INDEX}; use beacon_roots::BeaconRootsContract; use serde::{Deserialize, Serialize}; -mod beacon_roots; +pub(crate) mod beacon_roots; /// Input committing a previous block hash to the corresponding Beacon Chain block root. pub type HistoryInput = ComposeInput; @@ -42,7 +42,7 @@ pub struct HistoryCommit { #[derive(Clone, Serialize, Deserialize)] struct StateCommit { /// State for verifying `evm_commit`. - state: beacon_roots::State, + state: beacon_roots::BeaconRootsState, /// Commitment for `state` to a Beacon Chain block root. state_commit: GeneralizedBeaconCommit, } @@ -60,11 +60,11 @@ impl BlockHeaderCommit for HistoryCommit { // starting from evm_commit, "walk forward" along state_commits to reach a later beacon root let mut beacon_root = initial_commitment.digest; - for state_commit in self.state_commits { + for mut state_commit in self.state_commits { // verify that the previous commitment is valid wrt the current state let state_root = state_commit.state.root(); let commitment_root = - BeaconRootsContract::get_from_state(state_commit.state, timestamp) + BeaconRootsContract::get_from_db(&mut state_commit.state, timestamp) .expect("Beacon roots contract failed"); assert_eq!(commitment_root, beacon_root, "Beacon root does not match"); @@ -86,6 +86,7 @@ impl BlockHeaderCommit for HistoryCommit { #[cfg(feature = "host")] mod host { use super::*; + use crate::history::beacon_roots::{BeaconRootsState, HISTORY_BUFFER_LENGTH}; use crate::{ beacon::host::{client::BeaconClient, create_beacon_commit}, ethereum::EthBlockHeader, @@ -128,7 +129,7 @@ mod host { // we assume that not more than 25% of the blocks have been skipped // TODO(#309): implement a more sophisticated way to determine the step size - let step = BeaconRootsContract::HISTORY_BUFFER_LENGTH.to::() * 75 / 100; + let step = HISTORY_BUFFER_LENGTH.to::() * 75 / 100; let target = commitment_header.number(); let mut state_block = evm_header.number; @@ -154,7 +155,7 @@ mod host { ); // derive the historic state needed to verify the previous beacon commitment - let (beacon_root, state) = BeaconRootsContract::preflight_get( + let (beacon_root, state) = BeaconRootsState::preflight_get( U256::from(commit_ts), &rpc_provider, header.seal().into(), @@ -213,7 +214,7 @@ mod tests { let headers = get_headers(4).await.unwrap(); // create a history commitment executing on header[0] and committing to header[2] - let commit = + let mut commit = HistoryCommit::from_headers(&headers[0], &headers[2], &el, CL_URL.parse().unwrap()) .await .unwrap(); @@ -221,22 +222,19 @@ mod tests { let [StateCommit { state, state_commit, - }] = &commit.state_commits[..] + }] = &mut commit.state_commits[..] else { panic!("invalid state_commits") }; - // the state commit should verify against the beacon block root of headers[2] + // the state commit should verify against the beacon block root of headers[2]< state_commit .verify(state.root(), headers[3].parent_beacon_block_root.unwrap()) .unwrap(); // the beacon roots contract should return the beacon block root of headers[0] assert_eq!( - BeaconRootsContract::get_from_state( - state.clone(), - U256::from(commit.evm_commit.timestamp()) - ) - .unwrap(), + BeaconRootsContract::get_from_db(state, U256::from(commit.evm_commit.timestamp())) + .unwrap(), headers[1].parent_beacon_block_root.unwrap(), ); // the resulting commitment should correspond to the beacon block root of headers[2] diff --git a/steel/src/host/db/mod.rs b/steel/src/host/db/mod.rs index 32489ada..f41cad6b 100644 --- a/steel/src/host/db/mod.rs +++ b/steel/src/host/db/mod.rs @@ -15,7 +15,7 @@ //! [Database] implementations. //! //! [Database]: revm::Database -mod alloy; +pub(crate) mod alloy; mod proof; mod provider; diff --git a/steel/src/lib.rs b/steel/src/lib.rs index 79cbc4a1..79aaa8ab 100644 --- a/steel/src/lib.rs +++ b/steel/src/lib.rs @@ -36,12 +36,14 @@ mod merkle; mod mpt; pub mod serde; mod state; +mod verifier; pub use beacon::BeaconInput; pub use block::BlockInput; pub use contract::{CallBuilder, Contract}; pub use mpt::MerkleTrie; pub use state::{StateAccount, StateDb}; +pub use verifier::Verifier; #[cfg(feature = "unstable-history")] pub use history::HistoryInput; @@ -142,13 +144,13 @@ impl EvmEnv { &self.header } - fn db(&self) -> &D { + pub(crate) fn db(&self) -> &D { // safe unwrap: self cannot be borrowed without a DB self.db.as_ref().unwrap() } #[allow(dead_code)] - fn db_mut(&mut self) -> &mut D { + pub(crate) fn db_mut(&mut self) -> &mut D { // safe unwrap: self cannot be borrowed without a DB self.db.as_mut().unwrap() } diff --git a/steel/src/state.rs b/steel/src/state.rs index 9ca4d041..d35cf00f 100644 --- a/steel/src/state.rs +++ b/steel/src/state.rs @@ -74,7 +74,7 @@ impl StateDb { } #[inline] - fn account(&self, address: Address) -> Option { + pub(crate) fn account(&self, address: Address) -> Option { self.state_trie .get_rlp(keccak256(address)) .expect("Invalid encoded state trie value") @@ -88,7 +88,7 @@ impl StateDb { } #[inline] - fn block_hash(&self, number: u64) -> B256 { + pub(crate) fn block_hash(&self, number: u64) -> B256 { let hash = self .block_hashes .get(&number) @@ -97,7 +97,7 @@ impl StateDb { } #[inline] - fn storage_trie(&self, root: &B256) -> Option<&Rc> { + pub(crate) fn storage_trie(&self, root: &B256) -> Option<&Rc> { self.storage_tries.get(root) } } diff --git a/steel/src/verifier.rs b/steel/src/verifier.rs new file mode 100644 index 00000000..ed48bda0 --- /dev/null +++ b/steel/src/verifier.rs @@ -0,0 +1,141 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::history::beacon_roots::BeaconRootsContract; +use crate::state::WrapStateDb; +use crate::{Commitment, EvmBlockHeader, GuestEvmEnv}; +use alloy_primitives::U256; + +/// ### Examples +/// ```rust,no_run +/// # use risc0_steel::{ethereum::EthEvmEnv, Contract, host::BlockNumberOrTag}; +/// # use alloy_primitives::address; +/// # use alloy_sol_types::sol; +/// +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() -> anyhow::Result<()> { +/// use url::Url; +/// use risc0_steel::Verifier; +/// let contract_address = address!("dAC17F958D2ee523a2206206994597C13D831ec7"); +/// sol! { +/// interface IERC20 { +/// function balanceOf(address account) external view returns (uint); +/// } +/// } +/// let account = address!("F977814e90dA44bFA03b6295A0616a897441aceC"); +/// let get_balance = IERC20::balanceOfCall { account }; +/// +/// // Host: +/// let url: Url = "https://ethereum-rpc.publicnode.com".parse()?; +/// let mut env = EthEvmEnv::builder().rpc(url.clone()).block_number_or_tag(BlockNumberOrTag::Parent).build().await?; +/// let mut contract = Contract::preflight(contract_address, &mut env); +/// contract.call_builder(&get_balance).call().await?; +/// +/// let evm_input = env.into_input().await?; +/// +/// +/// // Guest: +/// let evm_env = evm_input.into_env(); +/// let contract = Contract::new(contract_address, &evm_env); +/// contract.call_builder(&get_balance).call(); +/// +/// let mut env2 = EthEvmEnv::builder().rpc(url).build().await?; +/// Verifier::preflight(&mut env2).verify(evm_env.commitment())?; +/// +/// # Ok(()) +/// # } +/// ``` +/// +/// [EthEvmEnv::builder]: crate::ethereum::EthEvmEnv::builder +/// [EvmEnv::builder]: crate::EvmEnv::builder +/// [EvmInput::into_env]: crate::EvmInput::into_env +pub struct Verifier { + env: E, +} + +impl<'a, H: EvmBlockHeader> Verifier<&'a GuestEvmEnv> { + pub fn new(env: &'a GuestEvmEnv) -> Self { + Self { env } + } + + pub fn verify(&self, commitment: &Commitment) { + let (id, version) = commitment.decode_id(); + match version { + 0 => { + let Some(block_number) = block_hash(self.env.header().inner(), id) else { + panic!() + }; + let db = self.env.db(); + assert_eq!(db.block_hash(block_number), commitment.digest); + } + 1 => { + let db = WrapStateDb::new(self.env.db()); + let beacon_root = BeaconRootsContract::get_from_db(db, id).unwrap(); + assert_eq!(beacon_root, commitment.digest); + } + _ => { + unimplemented!() + } + } + } +} + +fn block_hash(header: &impl EvmBlockHeader, calldata: U256) -> Option { + let requested_number: u64 = calldata.saturating_to(); + let diff = header.number().saturating_sub(requested_number); + (diff > 0 && diff <= 256).then_some(requested_number) +} + +#[cfg(feature = "host")] +mod host { + use super::*; + use crate::history::beacon_roots; + use crate::host::HostEvmEnv; + use anyhow::ensure; + use revm::Database; + + impl<'a, D: Database, H: EvmBlockHeader, C> Verifier<&'a mut HostEvmEnv> + where + beacon_roots::Error: From<::Error>, + anyhow::Error: From<::Error>, + { + pub fn preflight(env: &'a mut HostEvmEnv) -> Self { + Self { env } + } + + pub fn verify(&mut self, commitment: &Commitment) -> anyhow::Result<()> { + let (id, version) = commitment.decode_id(); + match version { + 0 => { + let Some(block_number) = block_hash(self.env.header().inner(), id) else { + panic!() + }; + let db = self.env.db_mut(); + ensure!(db.block_hash(block_number)? == commitment.digest); + + Ok(()) + } + 1 => { + let beacon_root = BeaconRootsContract::get_from_db(self.env.db_mut(), id)?; + ensure!(beacon_root == commitment.digest); + + Ok(()) + } + _ => { + unimplemented!() + } + } + } + } +} From f5fbc38c25e44320a5e5538b149549a6a0de95f8 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Wed, 6 Nov 2024 17:07:29 +0100 Subject: [PATCH 02/21] remove unused imports --- steel/src/history/beacon_roots.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index 4d53d1f3..e409054c 100644 --- a/steel/src/history/beacon_roots.rs +++ b/steel/src/history/beacon_roots.rs @@ -18,7 +18,6 @@ use revm::primitives::{AccountInfo, Bytecode}; use revm::Database; use serde::{Deserialize, Serialize}; use std::convert::Infallible; -use anyhow::anyhow; /// The length of the buffer that stores historical entries, i.e., the number of stored /// timestamps and roots. From 44f6a4896a53289613957838f86c3e85e4a085a5 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Thu, 7 Nov 2024 12:09:00 +0100 Subject: [PATCH 03/21] cleanups --- steel/src/contract.rs | 1 + steel/src/history/beacon_roots.rs | 2 +- steel/src/host/mod.rs | 28 +++++ steel/src/lib.rs | 2 +- steel/src/verifier.rs | 185 ++++++++++++++++++------------ 5 files changed, 144 insertions(+), 74 deletions(-) diff --git a/steel/src/contract.rs b/steel/src/contract.rs index 945e7599..0cbac297 100644 --- a/steel/src/contract.rs +++ b/steel/src/contract.rs @@ -233,6 +233,7 @@ mod host { // restore the DB before handling errors, so that we never return an env without a DB self.env.db = Some(db); + result.map_err(|err| anyhow!("call '{}' failed: {}", S::SIGNATURE, err)) } diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index e409054c..c3d5d182 100644 --- a/steel/src/history/beacon_roots.rs +++ b/steel/src/history/beacon_roots.rs @@ -124,7 +124,7 @@ impl BeaconRootsState { Ok(returns) => Ok((returns, state)), Err(err) => match err { Error::Reverted => Err(anyhow!("BeaconRootsContract({}) reverted", calldata)), - err => Err(err).context("API returned invalid state"), + err => Err(err).context("RPC error"), }, } } diff --git a/steel/src/host/mod.rs b/steel/src/host/mod.rs index 121ebdb3..e817a766 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.rs @@ -36,6 +36,7 @@ use alloy::{ use alloy_primitives::B256; use anyhow::{ensure, Result}; use db::{AlloyDb, ProofDb}; +use revm::Database; use url::Url; mod builder; @@ -96,6 +97,33 @@ pub struct HostCommit { config_id: B256, } +impl HostEvmEnv +where + D: Database + Send + 'static, +{ + /// Runs the provided closure that requires mutable access to the database on a thread where blocking is acceptable. + /// + /// It panics if the closure panics. + /// This function is necessary because mutable references to the database cannot be passed directly to `tokio::task::spawn_blocking`. Instead, the database is temporarily taken out of the `HostEvmEnv`, moved into the blocking task, and then restored after the task completes. + pub(crate) async fn spawn_with_db(&mut self, f: F) -> R + where + F: FnOnce(&mut ProofDb) -> R + Send + 'static, + R: Send + 'static, + { + // as mutable references are not possible, the DB must be moved in and out of the task + let mut db = self.db.take().unwrap(); + + let (result, db) = tokio::task::spawn_blocking(|| (f(&mut db), db)) + .await + .expect("DB execution panicked"); + + // restore the DB, so that we never return an env without a DB + self.db = Some(db); + + result + } +} + impl EthHostEvmEnv, Ethereum, RootProvider>>, ()> { /// Creates a new provable [EvmEnv] for Ethereum from an HTTP RPC endpoint. #[deprecated(since = "0.12.0", note = "use `EthEvmEnv::builder().rpc()` instead")] diff --git a/steel/src/lib.rs b/steel/src/lib.rs index 79aaa8ab..287d7bd8 100644 --- a/steel/src/lib.rs +++ b/steel/src/lib.rs @@ -279,7 +279,7 @@ impl std::fmt::Debug for Commitment { f.debug_struct("Commitment") .field("version", &version) .field("id", &id) - .field("claim", &self.digest) + .field("digest", &self.digest) .field("configID", &self.configID) .finish() } diff --git a/steel/src/verifier.rs b/steel/src/verifier.rs index ed48bda0..17c5843c 100644 --- a/steel/src/verifier.rs +++ b/steel/src/verifier.rs @@ -16,50 +16,8 @@ use crate::history::beacon_roots::BeaconRootsContract; use crate::state::WrapStateDb; use crate::{Commitment, EvmBlockHeader, GuestEvmEnv}; use alloy_primitives::U256; +use anyhow::ensure; -/// ### Examples -/// ```rust,no_run -/// # use risc0_steel::{ethereum::EthEvmEnv, Contract, host::BlockNumberOrTag}; -/// # use alloy_primitives::address; -/// # use alloy_sol_types::sol; -/// -/// # #[tokio::main(flavor = "current_thread")] -/// # async fn main() -> anyhow::Result<()> { -/// use url::Url; -/// use risc0_steel::Verifier; -/// let contract_address = address!("dAC17F958D2ee523a2206206994597C13D831ec7"); -/// sol! { -/// interface IERC20 { -/// function balanceOf(address account) external view returns (uint); -/// } -/// } -/// let account = address!("F977814e90dA44bFA03b6295A0616a897441aceC"); -/// let get_balance = IERC20::balanceOfCall { account }; -/// -/// // Host: -/// let url: Url = "https://ethereum-rpc.publicnode.com".parse()?; -/// let mut env = EthEvmEnv::builder().rpc(url.clone()).block_number_or_tag(BlockNumberOrTag::Parent).build().await?; -/// let mut contract = Contract::preflight(contract_address, &mut env); -/// contract.call_builder(&get_balance).call().await?; -/// -/// let evm_input = env.into_input().await?; -/// -/// -/// // Guest: -/// let evm_env = evm_input.into_env(); -/// let contract = Contract::new(contract_address, &evm_env); -/// contract.call_builder(&get_balance).call(); -/// -/// let mut env2 = EthEvmEnv::builder().rpc(url).build().await?; -/// Verifier::preflight(&mut env2).verify(evm_env.commitment())?; -/// -/// # Ok(()) -/// # } -/// ``` -/// -/// [EthEvmEnv::builder]: crate::ethereum::EthEvmEnv::builder -/// [EvmEnv::builder]: crate::EvmEnv::builder -/// [EvmInput::into_env]: crate::EvmInput::into_env pub struct Verifier { env: E, } @@ -73,69 +31,152 @@ impl<'a, H: EvmBlockHeader> Verifier<&'a GuestEvmEnv> { let (id, version) = commitment.decode_id(); match version { 0 => { - let Some(block_number) = block_hash(self.env.header().inner(), id) else { - panic!() - }; - let db = self.env.db(); - assert_eq!(db.block_hash(block_number), commitment.digest); + let block_number = + validate_block_number(self.env.header().inner(), id).expect("Invalid id"); + let block_hash = self.env.db().block_hash(block_number); + assert_eq!(block_hash, commitment.digest, "Invalid digest"); } 1 => { let db = WrapStateDb::new(self.env.db()); - let beacon_root = BeaconRootsContract::get_from_db(db, id).unwrap(); - assert_eq!(beacon_root, commitment.digest); - } - _ => { - unimplemented!() + let beacon_root = BeaconRootsContract::get_from_db(db, id) + .expect("calling BeaconRootsContract failed"); + assert_eq!(beacon_root, commitment.digest, "Invalid digest"); } + v => unimplemented!("Invalid commitment version {}", v), } } } -fn block_hash(header: &impl EvmBlockHeader, calldata: U256) -> Option { - let requested_number: u64 = calldata.saturating_to(); - let diff = header.number().saturating_sub(requested_number); - (diff > 0 && diff <= 256).then_some(requested_number) -} - #[cfg(feature = "host")] mod host { use super::*; use crate::history::beacon_roots; use crate::host::HostEvmEnv; - use anyhow::ensure; + use anyhow::Context; use revm::Database; - impl<'a, D: Database, H: EvmBlockHeader, C> Verifier<&'a mut HostEvmEnv> + impl<'a, D, H: EvmBlockHeader, C> Verifier<&'a mut HostEvmEnv> where + D: Database + Send + 'static, beacon_roots::Error: From<::Error>, anyhow::Error: From<::Error>, + ::Error: Send + 'static, { pub fn preflight(env: &'a mut HostEvmEnv) -> Self { Self { env } } - pub fn verify(&mut self, commitment: &Commitment) -> anyhow::Result<()> { + pub async fn verify(self, commitment: &Commitment) -> anyhow::Result<()> { + log::info!("Executing preflight verifying {:?}", commitment); + let (id, version) = commitment.decode_id(); match version { 0 => { - let Some(block_number) = block_hash(self.env.header().inner(), id) else { - panic!() - }; - let db = self.env.db_mut(); - ensure!(db.block_hash(block_number)? == commitment.digest); + let block_number = validate_block_number(self.env.header().inner(), id) + .context("invalid id")?; + let block_hash = self + .env + .spawn_with_db(move |db| db.block_hash(block_number)) + .await?; + ensure!(block_hash == commitment.digest, "invalid digest"); Ok(()) } 1 => { - let beacon_root = BeaconRootsContract::get_from_db(self.env.db_mut(), id)?; - ensure!(beacon_root == commitment.digest); + let beacon_root = self + .env + .spawn_with_db(move |db| BeaconRootsContract::get_from_db(db, id)) + .await + .with_context(|| format!("calling BeaconRootsContract({}) failed", id))?; + ensure!(beacon_root == commitment.digest, "invalid digest"); Ok(()) } - _ => { - unimplemented!() - } + v => unimplemented!("Invalid commitment version {}", v), } } } } + +fn validate_block_number(header: &impl EvmBlockHeader, block_number: U256) -> anyhow::Result { + let block_number: u64 = block_number.saturating_to(); + let diff = header.number().saturating_sub(block_number); + ensure!( + diff > 0 && diff <= 256, + "valid range is the last 256 blocks (not including the current one)" + ); + Ok(block_number) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::ChainSpec; + use crate::ethereum::EthEvmEnv; + use crate::host::BlockNumberOrTag; + use crate::CommitmentVersion; + use alloy::network::{BlockResponse, HeaderResponse}; + use alloy::providers::Provider; + use alloy::providers::ProviderBuilder; + use alloy::rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag; + + const EL_URL: &str = "https://ethereum-rpc.publicnode.com"; + + #[tokio::test] + #[ignore] // This queries actual RPC nodes, running only on demand. + async fn verify_block_commitment() { + let el = ProviderBuilder::new().on_builtin(EL_URL).await.unwrap(); + + let latest = el.get_block_number().await.unwrap(); + let block = el + .get_block_by_number((latest - 1).into(), false) + .await + .expect("eth_getBlockByNumber failed") + .unwrap(); + let header = block.header(); + let commit = Commitment::new( + CommitmentVersion::Block as u16, + header.number(), + header.hash(), + ChainSpec::DEFAULT_DIGEST, + ); + + let mut env = EthEvmEnv::builder() + .provider(el) + .block_number_or_tag(BlockNumberOrTag::Latest) + .build() + .await + .unwrap(); + + Verifier::preflight(&mut env).verify(&commit).await.unwrap(); + // env.into_input().await.unwrap(); + } + + #[tokio::test] + #[ignore] // This queries actual RPC nodes, running only on demand. + async fn verify_beacon_commitment() { + let el = ProviderBuilder::new().on_builtin(EL_URL).await.unwrap(); + + // create Beacon commitment from latest block + let block = el + .get_block_by_number(AlloyBlockNumberOrTag::Latest, false) + .await + .expect("eth_getBlockByNumber failed") + .unwrap(); + let header = block.header(); + let commit = Commitment::new( + CommitmentVersion::Beacon as u16, + header.timestamp, + header.parent_beacon_block_root.unwrap(), + ChainSpec::DEFAULT_DIGEST, + ); + + // preflight the verifier + let mut env = EthEvmEnv::builder().provider(el).build().await.unwrap(); + Verifier::preflight(&mut env).verify(&commit).await.unwrap(); + + // mock guest execution, by executing the verifier on the GuestEvmEnv + let env = env.into_input().await.unwrap().into_env(); + Verifier::new(&env).verify(&commit); + } +} From d0f94c536f682808c61e427d02f082155dd55d07 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Thu, 7 Nov 2024 12:42:29 +0100 Subject: [PATCH 04/21] support envs without accessed accounts --- steel/src/host/db/proof.rs | 20 +++++++++++++++++++- steel/src/mpt.rs | 6 ++++++ steel/src/verifier.rs | 25 ++++++++++++------------- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/steel/src/host/db/proof.rs b/steel/src/host/db/proof.rs index 745a64f9..23c804cf 100644 --- a/steel/src/host/db/proof.rs +++ b/steel/src/host/db/proof.rs @@ -15,6 +15,7 @@ use super::{provider::ProviderDb, AlloyDb}; use crate::MerkleTrie; use alloy::{ + consensus::BlockHeader, eips::eip2930::{AccessList, AccessListItem}, network::{primitives::BlockTransactionsKind, BlockResponse, Network}, providers::Provider, @@ -139,10 +140,27 @@ impl> ProofDb Result<(MerkleTrie, Vec)> { ensure!( - !self.accounts.is_empty(), + !self.accounts.is_empty() || !self.block_hash_numbers.is_empty(), "no accounts accessed: use Contract::preflight" ); + // if no accounts were accessed, use the state root of the corresponding block as is + if self.accounts.is_empty() { + let hash = self.inner.block_hash(); + let block = self + .inner + .provider() + .get_block_by_hash(hash, BlockTransactionsKind::Hashes) + .await + .context("eth_getBlockByNumber failed")? + .with_context(|| format!("block {} not found", hash))?; + + return Ok(( + MerkleTrie::from_digest(block.header().state_root()), + Vec::default(), + )); + } + let proofs = &mut self.proofs; for (address, storage_keys) in &self.accounts { let account_proof = proofs.get(address); diff --git a/steel/src/mpt.rs b/steel/src/mpt.rs index f7e60b57..5fedf3de 100644 --- a/steel/src/mpt.rs +++ b/steel/src/mpt.rs @@ -97,6 +97,12 @@ impl MerkleTrie { Ok(trie) } + + /// Creates a new trie corresponding to the given digest. + #[inline] + pub fn from_digest(digest: B256) -> Self { + MerkleTrie(Node::Digest(digest)) + } } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/steel/src/verifier.rs b/steel/src/verifier.rs index d3fe1780..bfa0a410 100644 --- a/steel/src/verifier.rs +++ b/steel/src/verifier.rs @@ -113,7 +113,6 @@ mod tests { use super::*; use crate::config::ChainSpec; use crate::ethereum::EthEvmEnv; - use crate::host::BlockNumberOrTag; use crate::CommitmentVersion; use alloy::consensus::BlockHeader; use alloy::network::primitives::BlockTransactionsKind; @@ -122,14 +121,16 @@ mod tests { use alloy::providers::Provider; use alloy::providers::ProviderBuilder; use alloy::rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag; + use test_log::test; const EL_URL: &str = "https://ethereum-rpc.publicnode.com"; - #[tokio::test] - #[ignore] // This queries actual RPC nodes, running only on demand. + #[test(tokio::test)] + #[ignore = "queries actual RPC nodes"] async fn verify_block_commitment() { let el = ProviderBuilder::new().on_builtin(EL_URL).await.unwrap(); + // create block commitment to the previous block let latest = el.get_block_number().await.unwrap(); let block = el .get_block_by_number((latest - 1).into(), BlockTransactionsKind::Hashes) @@ -144,19 +145,17 @@ mod tests { ChainSpec::DEFAULT_DIGEST, ); - let mut env = EthEvmEnv::builder() - .provider(el) - .block_number_or_tag(BlockNumberOrTag::Latest) - .build() - .await - .unwrap(); - + // preflight the verifier + let mut env = EthEvmEnv::builder().provider(el).build().await.unwrap(); Verifier::preflight(&mut env).verify(&commit).await.unwrap(); - // env.into_input().await.unwrap(); + + // mock guest execution, by executing the verifier on the GuestEvmEnv + let env = env.into_input().await.unwrap().into_env(); + Verifier::new(&env).verify(&commit); } - #[tokio::test] - #[ignore] // This queries actual RPC nodes, running only on demand. + #[test(tokio::test)] + #[ignore = "queries actual RPC nodes"] async fn verify_beacon_commitment() { let el = ProviderBuilder::new().on_builtin(EL_URL).await.unwrap(); From 8c50d20cc094f52a2eec1a33c25589997f85ccd4 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Thu, 7 Nov 2024 12:55:45 +0100 Subject: [PATCH 05/21] documentation --- steel/src/contract.rs | 1 - steel/src/history/beacon_roots.rs | 6 ++- steel/src/history/mod.rs | 2 +- steel/src/host/mod.rs | 7 +++- steel/src/lib.rs | 2 +- steel/src/verifier.rs | 61 ++++++++++++++++++++----------- 6 files changed, 50 insertions(+), 29 deletions(-) diff --git a/steel/src/contract.rs b/steel/src/contract.rs index 0cbac297..945e7599 100644 --- a/steel/src/contract.rs +++ b/steel/src/contract.rs @@ -233,7 +233,6 @@ mod host { // restore the DB before handling errors, so that we never return an env without a DB self.env.db = Some(db); - result.map_err(|err| anyhow!("call '{}' failed: {}", S::SIGNATURE, err)) } diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index 7b1cefce..f55f71f5 100644 --- a/steel/src/history/beacon_roots.rs +++ b/steel/src/history/beacon_roots.rs @@ -14,8 +14,10 @@ use crate::{MerkleTrie, StateAccount}; use alloy_primitives::{address, b256, keccak256, uint, Address, B256, U256}; -use revm::primitives::{AccountInfo, Bytecode}; -use revm::Database; +use revm::{ + primitives::{AccountInfo, Bytecode}, + Database, +}; use serde::{Deserialize, Serialize}; use std::convert::Infallible; diff --git a/steel/src/history/mod.rs b/steel/src/history/mod.rs index 33894728..49263735 100644 --- a/steel/src/history/mod.rs +++ b/steel/src/history/mod.rs @@ -86,10 +86,10 @@ impl BlockHeaderCommit for HistoryCommit { #[cfg(feature = "host")] mod host { use super::*; - use crate::history::beacon_roots::{BeaconRootsState, HISTORY_BUFFER_LENGTH}; use crate::{ beacon::host::{client::BeaconClient, create_beacon_commit}, ethereum::EthBlockHeader, + history::beacon_roots::{BeaconRootsState, HISTORY_BUFFER_LENGTH}, }; use alloy::{ network::{primitives::BlockTransactionsKind, Ethereum}, diff --git a/steel/src/host/mod.rs b/steel/src/host/mod.rs index e817a766..84c6a9fa 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.rs @@ -101,10 +101,13 @@ impl HostEvmEnv where D: Database + Send + 'static, { - /// Runs the provided closure that requires mutable access to the database on a thread where blocking is acceptable. + /// Runs the provided closure that requires mutable access to the database on a thread where + /// blocking is acceptable. /// /// It panics if the closure panics. - /// This function is necessary because mutable references to the database cannot be passed directly to `tokio::task::spawn_blocking`. Instead, the database is temporarily taken out of the `HostEvmEnv`, moved into the blocking task, and then restored after the task completes. + /// This function is necessary because mutable references to the database cannot be passed + /// directly to `tokio::task::spawn_blocking`. Instead, the database is temporarily taken out of + /// the `HostEvmEnv`, moved into the blocking task, and then restored after the task completes. pub(crate) async fn spawn_with_db(&mut self, f: F) -> R where F: FnOnce(&mut ProofDb) -> R + Send + 'static, diff --git a/steel/src/lib.rs b/steel/src/lib.rs index 287d7bd8..47323b4d 100644 --- a/steel/src/lib.rs +++ b/steel/src/lib.rs @@ -43,7 +43,7 @@ pub use block::BlockInput; pub use contract::{CallBuilder, Contract}; pub use mpt::MerkleTrie; pub use state::{StateAccount, StateDb}; -pub use verifier::Verifier; +pub use verifier::SteelVerifier; #[cfg(feature = "unstable-history")] pub use history::HistoryInput; diff --git a/steel/src/verifier.rs b/steel/src/verifier.rs index bfa0a410..54fb5c40 100644 --- a/steel/src/verifier.rs +++ b/steel/src/verifier.rs @@ -12,21 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::history::beacon_roots::BeaconRootsContract; -use crate::state::WrapStateDb; -use crate::{Commitment, EvmBlockHeader, GuestEvmEnv}; +use crate::{ + history::beacon_roots::BeaconRootsContract, state::WrapStateDb, Commitment, EvmBlockHeader, + GuestEvmEnv, +}; use alloy_primitives::U256; use anyhow::ensure; -pub struct Verifier { +/// Represents a verifier for validating Steel commitments within Steel. +pub struct SteelVerifier { env: E, } -impl<'a, H: EvmBlockHeader> Verifier<&'a GuestEvmEnv> { +impl<'a, H: EvmBlockHeader> SteelVerifier<&'a GuestEvmEnv> { + /// Constructor for verifying Steel commitments in the guest. pub fn new(env: &'a GuestEvmEnv) -> Self { Self { env } } + /// Verifies the commitment in the guest and panics on failure. pub fn verify(&self, commitment: &Commitment) { let (id, version) = commitment.decode_id(); match version { @@ -50,22 +54,29 @@ impl<'a, H: EvmBlockHeader> Verifier<&'a GuestEvmEnv> { #[cfg(feature = "host")] mod host { use super::*; - use crate::history::beacon_roots; - use crate::host::HostEvmEnv; + use crate::{history::beacon_roots, host::HostEvmEnv}; use anyhow::Context; use revm::Database; - impl<'a, D, H: EvmBlockHeader, C> Verifier<&'a mut HostEvmEnv> + impl<'a, D, H: EvmBlockHeader, C> SteelVerifier<&'a mut HostEvmEnv> where D: Database + Send + 'static, beacon_roots::Error: From<::Error>, anyhow::Error: From<::Error>, ::Error: Send + 'static, { + /// Constructor for preflighting Steel commitment verifications on the host. + /// + /// Initializes the environment for verifying Steel commitments, fetching necessary data via + /// RPC, and generating a storage proof for any accessed elements using + /// [EvmEnv::into_input]. + /// + /// [EvmEnv::into_input]: crate::EvmEnv::into_input pub fn preflight(env: &'a mut HostEvmEnv) -> Self { Self { env } } + /// Preflights the commitment verification on the host. pub async fn verify(self, commitment: &Commitment) -> anyhow::Result<()> { log::info!("Executing preflight verifying {:?}", commitment); @@ -111,16 +122,16 @@ fn validate_block_number(header: &impl EvmBlockHeader, block_number: U256) -> an #[cfg(test)] mod tests { use super::*; - use crate::config::ChainSpec; - use crate::ethereum::EthEvmEnv; - use crate::CommitmentVersion; - use alloy::consensus::BlockHeader; - use alloy::network::primitives::BlockTransactionsKind; - use alloy::network::primitives::HeaderResponse; - use alloy::network::BlockResponse; - use alloy::providers::Provider; - use alloy::providers::ProviderBuilder; - use alloy::rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag; + use crate::{config::ChainSpec, ethereum::EthEvmEnv, CommitmentVersion}; + use alloy::{ + consensus::BlockHeader, + network::{ + primitives::{BlockTransactionsKind, HeaderResponse}, + BlockResponse, + }, + providers::{Provider, ProviderBuilder}, + rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag, + }; use test_log::test; const EL_URL: &str = "https://ethereum-rpc.publicnode.com"; @@ -147,11 +158,14 @@ mod tests { // preflight the verifier let mut env = EthEvmEnv::builder().provider(el).build().await.unwrap(); - Verifier::preflight(&mut env).verify(&commit).await.unwrap(); + SteelVerifier::preflight(&mut env) + .verify(&commit) + .await + .unwrap(); // mock guest execution, by executing the verifier on the GuestEvmEnv let env = env.into_input().await.unwrap().into_env(); - Verifier::new(&env).verify(&commit); + SteelVerifier::new(&env).verify(&commit); } #[test(tokio::test)] @@ -175,10 +189,13 @@ mod tests { // preflight the verifier let mut env = EthEvmEnv::builder().provider(el).build().await.unwrap(); - Verifier::preflight(&mut env).verify(&commit).await.unwrap(); + SteelVerifier::preflight(&mut env) + .verify(&commit) + .await + .unwrap(); // mock guest execution, by executing the verifier on the GuestEvmEnv let env = env.into_input().await.unwrap().into_env(); - Verifier::new(&env).verify(&commit); + SteelVerifier::new(&env).verify(&commit); } } From eb3d80332da8f9b335a8b3fc75763997a2e11a41 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Thu, 7 Nov 2024 13:17:27 +0100 Subject: [PATCH 06/21] cleanup beacon roots --- steel/src/history/beacon_roots.rs | 63 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index f55f71f5..03e9a204 100644 --- a/steel/src/history/beacon_roots.rs +++ b/steel/src/history/beacon_roots.rs @@ -52,7 +52,11 @@ pub enum Error { #[error(transparent)] Other(#[from] anyhow::Error), } - +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} #[cfg(feature = "host")] impl From for Error { fn from(value: crate::host::db::alloy::Error) -> Self { @@ -60,18 +64,13 @@ impl From for Error { } } -impl From for Error { - fn from(_: Infallible) -> Self { - unreachable!() - } -} - -/// The `State` struct represents the state of the contract. +/// A simplified MPT-based read-only EVM database implementation only containing the state of the +/// beacon roots contract. #[derive(Clone, Serialize, Deserialize)] pub struct BeaconRootsState { - /// EVM (global) state trie with path to the contract account. + /// EVM (global) state trie with path to the beacon roots contract. state_trie: MerkleTrie, - /// Storage trie containing the state of the beacon root contract. + /// Storage trie containing the state of the beacon roots contract. storage_trie: MerkleTrie, } @@ -82,13 +81,11 @@ impl BeaconRootsState { self.state_trie.hash_slow() } - /// Prepares and retrieves the beacon root from an RPC provider by constructing the - /// necessary proof. + /// Prepares the [BeaconRootsState] by retrieving the beacon root from an RPC provider and + /// constructing the necessary proofs. /// - /// It fetches the minimal set of Merkle proofs (for the contract's state and storage) - /// required to verify and retrieve the beacon root associated with the given `calldata` - /// (timestamp). It leverages the Ethereum `eth_getProof` RPC to get the account and - /// storage proofs needed to validate the contract's state and storage. + /// It fetches the minimal set of Merkle proofs (for the contract's state and storage) required + /// to verify and retrieve the beacon root associated with the given `calldata` (timestamp). #[cfg(feature = "host")] pub async fn preflight_get( calldata: U256, @@ -132,27 +129,26 @@ impl BeaconRootsState { } } +/// Implements the Database trait, but only for the account of the beacon roots contract. impl Database for BeaconRootsState { type Error = Error; #[inline(always)] fn basic(&mut self, address: Address) -> Result, Self::Error> { + // only allow accessing the beacon roots contract's address assert_eq!(address, ADDRESS); - let account: Option = self.state_trie.get_rlp(ADDRESS_HASH)?; - match account { - Some(account) => { - if self.storage_trie.hash_slow() != account.storage_root { - return Err(Error::InvalidState); - } - Ok(Some(AccountInfo { - balance: account.balance, - nonce: account.nonce, - code_hash: account.code_hash, - code: None, - })) - } - None => Ok(None), + let account: StateAccount = self.state_trie.get_rlp(ADDRESS_HASH)?.unwrap_or_default(); + // and the account storage must match the storage trie + if account.storage_root != self.storage_trie.hash_slow() { + return Err(Error::InvalidState); } + + Ok(Some(AccountInfo { + balance: account.balance, + nonce: account.nonce, + code_hash: account.code_hash, + code: None, + })) } fn code_by_hash(&mut self, _code_hash: B256) -> Result { @@ -161,6 +157,7 @@ impl Database for BeaconRootsState { #[inline(always)] fn storage(&mut self, address: Address, index: U256) -> Result { + // only allow accessing the beacon roots contract's address assert_eq!(address, ADDRESS); Ok(self .storage_trie @@ -189,11 +186,11 @@ where D: Database, Error: From<::Error>, { - /// Creates a new instance of the `BeaconRootsContract` by verifying the provided state. + /// Creates a new instance of the `BeaconRootsContract` from the given db. pub fn new(mut db: D) -> Result { // retrieve the account data from the state trie using the contract's address hash let account = db.basic(ADDRESS)?.unwrap_or_default(); - // validate the account's code hash and storage root + // validate the account's code hash if account.code_hash != CODE_HASH { return Err(Error::NoContract); } @@ -222,7 +219,7 @@ where Ok(root.into()) } - /// Retrieves the root from a given `State` based on the provided `calldata` (timestamp). + /// Retrieves the root associated with the provided `calldata` (timestamp) from the given `db`. #[inline] pub fn get_from_db(db: D, calldata: U256) -> Result { Self::new(db)?.get(calldata) From 7144a7dec2802eefdb0dda3d89affd2c1d8fae0c Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Thu, 7 Nov 2024 16:46:28 +0100 Subject: [PATCH 07/21] minor cleanups --- steel/src/history/beacon_roots.rs | 2 ++ steel/src/history/mod.rs | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index 03e9a204..2c7767ce 100644 --- a/steel/src/history/beacon_roots.rs +++ b/steel/src/history/beacon_roots.rs @@ -52,11 +52,13 @@ pub enum Error { #[error(transparent)] Other(#[from] anyhow::Error), } + impl From for Error { fn from(_: Infallible) -> Self { unreachable!() } } + #[cfg(feature = "host")] impl From for Error { fn from(value: crate::host::db::alloy::Error) -> Self { diff --git a/steel/src/history/mod.rs b/steel/src/history/mod.rs index 49263735..4bfb4aac 100644 --- a/steel/src/history/mod.rs +++ b/steel/src/history/mod.rs @@ -18,8 +18,9 @@ use crate::{ }; use alloy_primitives::{Sealed, B256, U256}; use beacon::{BeaconCommit, GeneralizedBeaconCommit, STATE_ROOT_LEAF_INDEX}; -use beacon_roots::BeaconRootsContract; +use beacon_roots::{BeaconRootsContract, BeaconRootsState}; use serde::{Deserialize, Serialize}; + pub(crate) mod beacon_roots; /// Input committing a previous block hash to the corresponding Beacon Chain block root. @@ -42,7 +43,7 @@ pub struct HistoryCommit { #[derive(Clone, Serialize, Deserialize)] struct StateCommit { /// State for verifying `evm_commit`. - state: beacon_roots::BeaconRootsState, + state: BeaconRootsState, /// Commitment for `state` to a Beacon Chain block root. state_commit: GeneralizedBeaconCommit, } From 58c1fb2fbb2b60c62e4e8af366679b2e9286be9a Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Wed, 20 Nov 2024 13:18:33 +0100 Subject: [PATCH 08/21] Add SteelVerifier example (#327) Extend the token-stats example to use two sample points with only a single commitment. --- examples/token-stats/Cargo.toml | 1 + examples/token-stats/host/Cargo.toml | 1 + examples/token-stats/host/src/main.rs | 78 ++++++++++++++++--- .../token-stats/methods/guest/src/main.rs | 42 ++++++---- 4 files changed, 100 insertions(+), 22 deletions(-) diff --git a/examples/token-stats/Cargo.toml b/examples/token-stats/Cargo.toml index 38379935..b6c9b435 100644 --- a/examples/token-stats/Cargo.toml +++ b/examples/token-stats/Cargo.toml @@ -11,6 +11,7 @@ risc0-build = { git = "https://github.com/risc0/risc0", branch = "main" } risc0-zkvm = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } risc0-zkp = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } +alloy = { version = "0.6", features = ["full"] } alloy-primitives = { version = "0.8", features = ["serde", "rlp", "std"] } alloy-rlp = { version = "0.3", default-features = false } alloy-rlp-derive = { version = "0.3", default-features = false } diff --git a/examples/token-stats/host/Cargo.toml b/examples/token-stats/host/Cargo.toml index 575c546f..bce1b6e0 100644 --- a/examples/token-stats/host/Cargo.toml +++ b/examples/token-stats/host/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +alloy = { workspace = true } alloy-sol-types = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } diff --git a/examples/token-stats/host/src/main.rs b/examples/token-stats/host/src/main.rs index eed3f774..dddbfd2e 100644 --- a/examples/token-stats/host/src/main.rs +++ b/examples/token-stats/host/src/main.rs @@ -12,12 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloy_sol_types::{SolCall, SolValue}; +use alloy::{ + providers::{Provider, ProviderBuilder}, + sol_types::{SolCall, SolValue}, +}; use anyhow::{Context, Result}; use clap::Parser; use risc0_steel::{ ethereum::{EthEvmEnv, ETH_MAINNET_CHAIN_SPEC}, - Contract, + Contract, SteelVerifier, }; use risc0_zkvm::{default_executor, ExecutorEnv}; use token_stats_core::{APRCommitment, CometMainInterface, CONTRACT}; @@ -25,13 +28,16 @@ use token_stats_methods::TOKEN_STATS_ELF; use tracing_subscriber::EnvFilter; use url::Url; -// Simple program to show the use of Ethereum contract data inside the guest. #[derive(Parser, Debug)] #[command(about, long_about = None)] struct Args { /// URL of the RPC endpoint - #[arg(short, long, env = "RPC_URL")] + #[arg(long, env)] rpc_url: Url, + + /// Beacon API endpoint URL + #[clap(long, env)] + beacon_api_url: Url, } #[tokio::main] @@ -43,8 +49,17 @@ async fn main() -> Result<()> { // Parse the command line arguments. let args = Args::parse(); - // Create an EVM environment from an RPC endpoint defaulting to the latest block. - let mut env = EthEvmEnv::builder().rpc(args.rpc_url).build().await?; + // Query the latest block number. + let provider = ProviderBuilder::new().on_http(args.rpc_url); + let latest = provider.get_block_number().await?; + + // Create an EVM environment for that provider and about 12h (3600 blocks) ago. + let mut env = EthEvmEnv::builder() + .provider(provider.clone()) + .block_number(latest - 3600) + .beacon_api(args.beacon_api_url) + .build() + .await?; // The `with_chain_spec` method is used to specify the chain configuration. env = env.with_chain_spec(Ð_MAINNET_CHAIN_SPEC); @@ -74,13 +89,58 @@ async fn main() -> Result<()> { rate ); - // Finally, construct the input from the environment. - let input = env.into_input().await?; + // Construct the input from the environment. + let input1 = env.into_input().await?; + + // Create another EVM environment for that provider defaulting to the latest block. + let mut env = EthEvmEnv::builder().provider(provider).build().await?; + env = env.with_chain_spec(Ð_MAINNET_CHAIN_SPEC); + + // TODO: Make it easier to get the commitment from a HostEvmEnv + let commitment = input1 + .clone() + .into_env() + .with_chain_spec(Ð_MAINNET_CHAIN_SPEC) + .into_commitment(); + // Preflight the verification of the commitment of the previous input. + SteelVerifier::preflight(&mut env) + .verify(&commitment) + .await?; + + // Preflight the actual contract calls. + let mut contract = Contract::preflight(CONTRACT, &mut env); + let utilization = contract + .call_builder(&CometMainInterface::getUtilizationCall {}) + .call() + .await? + ._0; + println!( + "Call {} Function on {:#} returns: {}", + CometMainInterface::getUtilizationCall::SIGNATURE, + CONTRACT, + utilization + ); + let rate = contract + .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) + .call() + .await? + ._0; + println!( + "Call {} Function on {:#} returns: {}", + CometMainInterface::getSupplyRateCall::SIGNATURE, + CONTRACT, + rate + ); + + // Finally, construct the second input from the environment. + let input2 = env.into_input().await?; println!("Running the guest with the constructed input:"); let session_info = { let env = ExecutorEnv::builder() - .write(&input) + .write(&input1) + .unwrap() + .write(&input2) .unwrap() .build() .context("failed to build executor env")?; diff --git a/examples/token-stats/methods/guest/src/main.rs b/examples/token-stats/methods/guest/src/main.rs index 86403326..f2d26fc7 100644 --- a/examples/token-stats/methods/guest/src/main.rs +++ b/examples/token-stats/methods/guest/src/main.rs @@ -15,7 +15,7 @@ use alloy_sol_types::SolValue; use risc0_steel::{ ethereum::{EthEvmInput, ETH_MAINNET_CHAIN_SPEC}, - Contract, + Contract, SteelVerifier, }; use risc0_zkvm::guest::env; use token_stats_core::{APRCommitment, CometMainInterface, CONTRACT}; @@ -23,21 +23,38 @@ use token_stats_core::{APRCommitment, CometMainInterface, CONTRACT}; const SECONDS_PER_YEAR: u64 = 60 * 60 * 24 * 365; fn main() { - // Read the input from the guest environment. + // Read the first input from the guest environment. It corresponds to the older EVM state. let input: EthEvmInput = env::read(); - // Converts the input into a `EvmEnv` for execution. The `with_chain_spec` method is used - // to specify the chain configuration. It checks that the state matches the state root in the - // header provided in the input. - let env = input.into_env().with_chain_spec(Ð_MAINNET_CHAIN_SPEC); + // Converts the input into a `EvmEnv` for execution. + let env_prev = input.into_env().with_chain_spec(Ð_MAINNET_CHAIN_SPEC); - // Execute the view calls; it returns the result in the type generated by the `sol!` macro. - let contract = Contract::new(CONTRACT, &env); + // Execute the view calls on the older EVM state. + let contract = Contract::new(CONTRACT, &env_prev); let utilization = contract .call_builder(&CometMainInterface::getUtilizationCall {}) .call() ._0; - let supply_rate = contract + let supply_rate_prev = contract + .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) + .call() + ._0; + + // Prepare the second `EvmEnv` for execution. It corresponds to the recent EVM state. + let input: EthEvmInput = env::read(); + let env_cur = input.into_env().with_chain_spec(Ð_MAINNET_CHAIN_SPEC); + + // Verify that the older EVM state is valid wrt the recent EVM state. + // We initialize the SteelVerifier with the recent state, to check the previous commitment. + SteelVerifier::new(&env_cur).verify(env_prev.commitment()); + + // Execute the view calls also on the recent EVM state. + let contract = Contract::new(CONTRACT, &env_cur); + let utilization = contract + .call_builder(&CometMainInterface::getUtilizationCall {}) + .call() + ._0; + let supply_rate_cur = contract .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) .call() ._0; @@ -48,13 +65,12 @@ fn main() { // Supply Rate = getSupplyRate(Utilization) // Supply APR = Supply Rate / (10 ^ 18) * Seconds Per Year * 100 // - // And this is calculating: Supply Rate * Seconds Per Year, to avoid float calculations for - // precision. - let annual_supply_rate = supply_rate * SECONDS_PER_YEAR; + // Compute the average APR, by computing the average over both states. + let annual_supply_rate = (supply_rate_prev + supply_rate_cur) * SECONDS_PER_YEAR / 2; // This commits the APR at current utilization rate for this given block. let journal = APRCommitment { - commitment: env.into_commitment(), + commitment: env_cur.into_commitment(), annualSupplyRate: annual_supply_rate, }; env::commit_slice(&journal.abi_encode()); From 5599b10dca170d4939c4736258625ef0c82b4a48 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 15:07:54 +0100 Subject: [PATCH 09/21] return commitment for Envs with BlockHeaderCommit --- examples/token-stats/host/src/main.rs | 8 ++---- steel/src/host/mod.rs | 36 +++++++++++++++++---------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/examples/token-stats/host/src/main.rs b/examples/token-stats/host/src/main.rs index dddbfd2e..a89e44b4 100644 --- a/examples/token-stats/host/src/main.rs +++ b/examples/token-stats/host/src/main.rs @@ -89,6 +89,8 @@ async fn main() -> Result<()> { rate ); + let commitment = env.commitment(); + // Construct the input from the environment. let input1 = env.into_input().await?; @@ -96,12 +98,6 @@ async fn main() -> Result<()> { let mut env = EthEvmEnv::builder().provider(provider).build().await?; env = env.with_chain_spec(Ð_MAINNET_CHAIN_SPEC); - // TODO: Make it easier to get the commitment from a HostEvmEnv - let commitment = input1 - .clone() - .into_env() - .with_chain_spec(Ð_MAINNET_CHAIN_SPEC) - .into_commitment(); // Preflight the verification of the commitment of the previous input. SteelVerifier::preflight(&mut env) .verify(&commitment) diff --git a/steel/src/host/mod.rs b/steel/src/host/mod.rs index a64df85b..26a29c98 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.rs @@ -25,7 +25,7 @@ use crate::{ ethereum::{EthBlockHeader, EthEvmEnv}, history::HistoryCommit, host::db::ProviderDb, - ComposeInput, EvmBlockHeader, EvmEnv, EvmInput, + BlockHeaderCommit, Commitment, ComposeInput, EvmBlockHeader, EvmEnv, EvmInput, }; use alloy::eips::eip1898::{HexStringMissingPrefixError, ParseBlockNumberError}; use alloy::{ @@ -162,6 +162,21 @@ where } } +impl HostEvmEnv { + /// Sets the chain ID and specification ID from the given chain spec. + /// + /// This will panic when there is no valid specification ID for the current block. + pub fn with_chain_spec(mut self, chain_spec: &ChainSpec) -> Self { + self.cfg_env.chain_id = chain_spec.chain_id(); + self.cfg_env.handler_cfg.spec_id = chain_spec + .active_fork(self.header.number(), self.header.timestamp()) + .unwrap(); + self.commit.config_id = chain_spec.digest(); + + self + } +} + impl HostEvmEnv, H, ()> where T: Transport + Clone, @@ -178,18 +193,13 @@ where } } -impl HostEvmEnv { - /// Sets the chain ID and specification ID from the given chain spec. - /// - /// This will panic when there is no valid specification ID for the current block. - pub fn with_chain_spec(mut self, chain_spec: &ChainSpec) -> Self { - self.cfg_env.chain_id = chain_spec.chain_id(); - self.cfg_env.handler_cfg.spec_id = chain_spec - .active_fork(self.header.number(), self.header.timestamp()) - .unwrap(); - self.commit.config_id = chain_spec.digest(); - - self +impl> HostEvmEnv { + /// Returns the [Commitment] used to validate the environment. + pub fn commitment(&self) -> Commitment { + self.commit + .inner + .clone() + .commit(&self.header, self.commit.config_id) } } From 157a96d333d90652522687fadd1ab9c9faa099da Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 15:34:39 +0100 Subject: [PATCH 10/21] generalize also the block input --- steel/src/block.rs | 45 +++++++++++++++++++++++---------------- steel/src/host/builder.rs | 5 +++-- steel/src/host/mod.rs | 37 +++++++++++++++++--------------- steel/src/lib.rs | 12 ++++++----- steel/tests/corruption.rs | 3 ++- 5 files changed, 59 insertions(+), 43 deletions(-) diff --git a/steel/src/block.rs b/steel/src/block.rs index ffde7fc7..2d781401 100644 --- a/steel/src/block.rs +++ b/steel/src/block.rs @@ -12,16 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - config::ChainSpec, state::StateDb, Commitment, CommitmentVersion, EvmBlockHeader, EvmEnv, - GuestEvmEnv, MerkleTrie, -}; +use crate::{state::StateDb, BlockHeaderCommit, Commitment, CommitmentVersion, ComposeInput, EvmBlockHeader, EvmEnv, GuestEvmEnv, MerkleTrie}; use ::serde::{Deserialize, Serialize}; -use alloy_primitives::{map::HashMap, Bytes}; +use alloy_primitives::{map::HashMap, Bytes, Sealed, B256}; -/// Input committing to the corresponding execution block hash. #[derive(Clone, Serialize, Deserialize)] -pub struct BlockInput { +pub struct Input { header: H, state_trie: MerkleTrie, storage_tries: Vec, @@ -29,7 +25,26 @@ pub struct BlockInput { ancestors: Vec, } -impl BlockInput { +/// Input committing to the corresponding execution block hash. +pub type BlockInput = ComposeInput; + +/// A commitment to an execution block hash, along with the corresponding block number. +#[derive(Clone, Serialize, Deserialize)] +pub struct BlockCommit; + +impl BlockHeaderCommit for BlockCommit { + #[inline] + fn commit(self, header: &Sealed, config_id: B256) -> Commitment { + Commitment::new( + CommitmentVersion::Block as u16, + header.number(), + header.seal(), + config_id, + ) + } +} + +impl Input { /// Converts the input into a [EvmEnv] for verifiable state access in the guest. pub fn into_env(self) -> GuestEvmEnv { // verify that the state root matches the state trie @@ -64,14 +79,8 @@ impl BlockInput { self.contracts, block_hashes, ); - let commit = Commitment::new( - CommitmentVersion::Block as u16, - header.number(), - header.seal(), - ChainSpec::DEFAULT_DIGEST, - ); - EvmEnv::new(db, header, commit) + EvmEnv::new(db, header, Commitment::default()) } } @@ -79,7 +88,7 @@ impl BlockInput { pub mod host { use std::fmt::Display; - use super::BlockInput; + use super::Input; use crate::{ host::db::{AlloyDb, ProofDb, ProviderDb}, EvmBlockHeader, @@ -89,7 +98,7 @@ pub mod host { use anyhow::{anyhow, ensure}; use log::debug; - impl BlockInput { + impl Input { /// Creates the `BlockInput` containing the necessary EVM state that can be verified against /// the block hash. pub(crate) async fn from_proof_db( @@ -132,7 +141,7 @@ pub mod host { debug!("contracts: {}", contracts.len()); debug!("ancestor blocks: {}", ancestors.len()); - let input = BlockInput { + let input = Input { header: header.into_inner(), state_trie, storage_tries, diff --git a/steel/src/host/builder.rs b/steel/src/host/builder.rs index 4d02712c..bf03773b 100644 --- a/steel/src/host/builder.rs +++ b/steel/src/host/builder.rs @@ -35,6 +35,7 @@ use alloy_primitives::Sealed; use anyhow::{anyhow, ensure, Context, Result}; use std::{fmt::Display, marker::PhantomData}; use url::Url; +use crate::block::BlockCommit; impl EvmEnv<(), H, ()> { /// Creates a builder for building an environment. @@ -187,7 +188,7 @@ impl EvmEnvBuilder { impl EvmEnvBuilder { /// Builds and returns an [EvmEnv] with the configured settings that commits to a block hash. - pub async fn build(self) -> Result, H, ()>> + pub async fn build(self) -> Result, H, BlockCommit>> where T: Transport + Clone, N: Network, @@ -208,7 +209,7 @@ impl EvmEnvBuilder { header.seal(), )); let commit = HostCommit { - inner: (), + inner: BlockCommit, config_id: ChainSpec::DEFAULT_DIGEST, }; diff --git a/steel/src/host/mod.rs b/steel/src/host/mod.rs index 26a29c98..1ee6b10c 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.rs @@ -20,7 +20,7 @@ use std::{ use crate::{ beacon::BeaconCommit, - block::BlockInput, + block::Input, config::ChainSpec, ethereum::{EthBlockHeader, EthEvmEnv}, history::HistoryCommit, @@ -44,6 +44,7 @@ mod builder; pub mod db; pub use builder::EvmEnvBuilder; +use crate::block::BlockCommit; /// A block number (or tag - "latest", "safe", "finalized"). /// This enum is used to specify which block to query when interacting with the blockchain. @@ -177,7 +178,17 @@ impl HostEvmEnv { } } -impl HostEvmEnv, H, ()> +impl> HostEvmEnv { + /// Returns the [Commitment] used to validate the environment. + pub fn commitment(&self) -> Commitment { + self.commit + .inner + .clone() + .commit(&self.header, self.commit.config_id) + } +} + +impl HostEvmEnv, H, BlockCommit> where T: Transport + Clone, N: Network, @@ -187,19 +198,11 @@ where { /// Converts the environment into a [EvmInput] committing to an execution block hash. pub async fn into_input(self) -> Result> { - let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; + let input = Input::from_proof_db(self.db.unwrap(), self.header).await?; - Ok(EvmInput::Block(input)) - } -} - -impl> HostEvmEnv { - /// Returns the [Commitment] used to validate the environment. - pub fn commitment(&self) -> Commitment { - self.commit - .inner - .clone() - .commit(&self.header, self.commit.config_id) + Ok(EvmInput::Block(ComposeInput::new( + input, self.commit.inner, + ))) } } @@ -210,7 +213,7 @@ where { /// Converts the environment into a [EvmInput] committing to a Beacon Chain block root. pub async fn into_input(self) -> Result> { - let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; + let input = Input::from_proof_db(self.db.unwrap(), self.header).await?; Ok(EvmInput::Beacon(ComposeInput::new( input, @@ -228,7 +231,7 @@ where /// block roots. #[stability::unstable(feature = "history")] pub async fn into_input(self) -> Result> { - let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; + let input = Input::from_proof_db(self.db.unwrap(), self.header).await?; Ok(EvmInput::History(ComposeInput::new( input, @@ -250,7 +253,7 @@ where pub async fn into_beacon_input(self, url: Url) -> Result> { let commit = BeaconCommit::from_header(self.header(), self.db().inner().provider(), url).await?; - let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; + let input = Input::from_proof_db(self.db.unwrap(), self.header).await?; Ok(EvmInput::Beacon(ComposeInput::new(input, commit))) } diff --git a/steel/src/lib.rs b/steel/src/lib.rs index 47323b4d..770facc5 100644 --- a/steel/src/lib.rs +++ b/steel/src/lib.rs @@ -39,7 +39,6 @@ mod state; mod verifier; pub use beacon::BeaconInput; -pub use block::BlockInput; pub use contract::{CallBuilder, Contract}; pub use mpt::MerkleTrie; pub use state::{StateAccount, StateDb}; @@ -76,6 +75,8 @@ impl EvmInput { } } + + /// A trait linking the block header to a commitment. pub trait BlockHeaderCommit { /// Creates a verifiable [Commitment] of the `header`. @@ -88,18 +89,18 @@ pub trait BlockHeaderCommit { /// contained within the `input` field. #[derive(Clone, Serialize, Deserialize)] pub struct ComposeInput { - input: BlockInput, + input: Input, commit: C, } impl> ComposeInput { - /// Creates a new composed input from a [BlockInput] and a [BlockHeaderCommit]. - pub const fn new(input: BlockInput, commit: C) -> Self { + /// Creates a new composed input from a [Input] and a [BlockHeaderCommit]. + pub const fn new(input: Input, commit: C) -> Self { Self { input, commit } } /// Disassembles this `ComposeInput`, returning the underlying input and commitment creator. - pub fn into_parts(self) -> (BlockInput, C) { + pub fn into_parts(self) -> (Input, C) { (self.input, self.commit) } @@ -227,6 +228,7 @@ mod private { } pub use private::Commitment; +use crate::block::{BlockInput, Input}; /// Version of a [`Commitment`]. #[repr(u16)] diff --git a/steel/tests/corruption.rs b/steel/tests/corruption.rs index 33309f7d..50691272 100644 --- a/steel/tests/corruption.rs +++ b/steel/tests/corruption.rs @@ -162,8 +162,9 @@ async fn load_or_create<'a, T: serde::ser::Serialize + serde::de::DeserializeOwn fn get_block_input_mut(input: &mut Value) -> &mut Value { let (key, value) = input.as_object_mut().unwrap().into_iter().next().unwrap(); match key.as_str() { - "Block" => value, + "Block" => &mut value["input"], "Beacon" => &mut value["input"], + "History" => &mut value["input"], _ => unreachable!(), } } From e79e83bb1cd9ca7a6b701ab53927dc9b14344d0d Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 16:08:39 +0100 Subject: [PATCH 11/21] Revert "generalize also the block input" This reverts commit 157a96d333d90652522687fadd1ab9c9faa099da. --- steel/src/block.rs | 45 ++++++++++++++++----------------------- steel/src/host/builder.rs | 5 ++--- steel/src/host/mod.rs | 37 +++++++++++++++----------------- steel/src/lib.rs | 12 +++++------ steel/tests/corruption.rs | 3 +-- 5 files changed, 43 insertions(+), 59 deletions(-) diff --git a/steel/src/block.rs b/steel/src/block.rs index 2d781401..ffde7fc7 100644 --- a/steel/src/block.rs +++ b/steel/src/block.rs @@ -12,12 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{state::StateDb, BlockHeaderCommit, Commitment, CommitmentVersion, ComposeInput, EvmBlockHeader, EvmEnv, GuestEvmEnv, MerkleTrie}; +use crate::{ + config::ChainSpec, state::StateDb, Commitment, CommitmentVersion, EvmBlockHeader, EvmEnv, + GuestEvmEnv, MerkleTrie, +}; use ::serde::{Deserialize, Serialize}; -use alloy_primitives::{map::HashMap, Bytes, Sealed, B256}; +use alloy_primitives::{map::HashMap, Bytes}; +/// Input committing to the corresponding execution block hash. #[derive(Clone, Serialize, Deserialize)] -pub struct Input { +pub struct BlockInput { header: H, state_trie: MerkleTrie, storage_tries: Vec, @@ -25,26 +29,7 @@ pub struct Input { ancestors: Vec, } -/// Input committing to the corresponding execution block hash. -pub type BlockInput = ComposeInput; - -/// A commitment to an execution block hash, along with the corresponding block number. -#[derive(Clone, Serialize, Deserialize)] -pub struct BlockCommit; - -impl BlockHeaderCommit for BlockCommit { - #[inline] - fn commit(self, header: &Sealed, config_id: B256) -> Commitment { - Commitment::new( - CommitmentVersion::Block as u16, - header.number(), - header.seal(), - config_id, - ) - } -} - -impl Input { +impl BlockInput { /// Converts the input into a [EvmEnv] for verifiable state access in the guest. pub fn into_env(self) -> GuestEvmEnv { // verify that the state root matches the state trie @@ -79,8 +64,14 @@ impl Input { self.contracts, block_hashes, ); + let commit = Commitment::new( + CommitmentVersion::Block as u16, + header.number(), + header.seal(), + ChainSpec::DEFAULT_DIGEST, + ); - EvmEnv::new(db, header, Commitment::default()) + EvmEnv::new(db, header, commit) } } @@ -88,7 +79,7 @@ impl Input { pub mod host { use std::fmt::Display; - use super::Input; + use super::BlockInput; use crate::{ host::db::{AlloyDb, ProofDb, ProviderDb}, EvmBlockHeader, @@ -98,7 +89,7 @@ pub mod host { use anyhow::{anyhow, ensure}; use log::debug; - impl Input { + impl BlockInput { /// Creates the `BlockInput` containing the necessary EVM state that can be verified against /// the block hash. pub(crate) async fn from_proof_db( @@ -141,7 +132,7 @@ pub mod host { debug!("contracts: {}", contracts.len()); debug!("ancestor blocks: {}", ancestors.len()); - let input = Input { + let input = BlockInput { header: header.into_inner(), state_trie, storage_tries, diff --git a/steel/src/host/builder.rs b/steel/src/host/builder.rs index bf03773b..4d02712c 100644 --- a/steel/src/host/builder.rs +++ b/steel/src/host/builder.rs @@ -35,7 +35,6 @@ use alloy_primitives::Sealed; use anyhow::{anyhow, ensure, Context, Result}; use std::{fmt::Display, marker::PhantomData}; use url::Url; -use crate::block::BlockCommit; impl EvmEnv<(), H, ()> { /// Creates a builder for building an environment. @@ -188,7 +187,7 @@ impl EvmEnvBuilder { impl EvmEnvBuilder { /// Builds and returns an [EvmEnv] with the configured settings that commits to a block hash. - pub async fn build(self) -> Result, H, BlockCommit>> + pub async fn build(self) -> Result, H, ()>> where T: Transport + Clone, N: Network, @@ -209,7 +208,7 @@ impl EvmEnvBuilder { header.seal(), )); let commit = HostCommit { - inner: BlockCommit, + inner: (), config_id: ChainSpec::DEFAULT_DIGEST, }; diff --git a/steel/src/host/mod.rs b/steel/src/host/mod.rs index 1ee6b10c..26a29c98 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.rs @@ -20,7 +20,7 @@ use std::{ use crate::{ beacon::BeaconCommit, - block::Input, + block::BlockInput, config::ChainSpec, ethereum::{EthBlockHeader, EthEvmEnv}, history::HistoryCommit, @@ -44,7 +44,6 @@ mod builder; pub mod db; pub use builder::EvmEnvBuilder; -use crate::block::BlockCommit; /// A block number (or tag - "latest", "safe", "finalized"). /// This enum is used to specify which block to query when interacting with the blockchain. @@ -178,17 +177,7 @@ impl HostEvmEnv { } } -impl> HostEvmEnv { - /// Returns the [Commitment] used to validate the environment. - pub fn commitment(&self) -> Commitment { - self.commit - .inner - .clone() - .commit(&self.header, self.commit.config_id) - } -} - -impl HostEvmEnv, H, BlockCommit> +impl HostEvmEnv, H, ()> where T: Transport + Clone, N: Network, @@ -198,11 +187,19 @@ where { /// Converts the environment into a [EvmInput] committing to an execution block hash. pub async fn into_input(self) -> Result> { - let input = Input::from_proof_db(self.db.unwrap(), self.header).await?; + let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; - Ok(EvmInput::Block(ComposeInput::new( - input, self.commit.inner, - ))) + Ok(EvmInput::Block(input)) + } +} + +impl> HostEvmEnv { + /// Returns the [Commitment] used to validate the environment. + pub fn commitment(&self) -> Commitment { + self.commit + .inner + .clone() + .commit(&self.header, self.commit.config_id) } } @@ -213,7 +210,7 @@ where { /// Converts the environment into a [EvmInput] committing to a Beacon Chain block root. pub async fn into_input(self) -> Result> { - let input = Input::from_proof_db(self.db.unwrap(), self.header).await?; + let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; Ok(EvmInput::Beacon(ComposeInput::new( input, @@ -231,7 +228,7 @@ where /// block roots. #[stability::unstable(feature = "history")] pub async fn into_input(self) -> Result> { - let input = Input::from_proof_db(self.db.unwrap(), self.header).await?; + let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; Ok(EvmInput::History(ComposeInput::new( input, @@ -253,7 +250,7 @@ where pub async fn into_beacon_input(self, url: Url) -> Result> { let commit = BeaconCommit::from_header(self.header(), self.db().inner().provider(), url).await?; - let input = Input::from_proof_db(self.db.unwrap(), self.header).await?; + let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; Ok(EvmInput::Beacon(ComposeInput::new(input, commit))) } diff --git a/steel/src/lib.rs b/steel/src/lib.rs index 1497c4b7..32127636 100644 --- a/steel/src/lib.rs +++ b/steel/src/lib.rs @@ -47,6 +47,7 @@ mod state; mod verifier; pub use beacon::BeaconInput; +pub use block::BlockInput; pub use contract::{CallBuilder, Contract}; pub use mpt::MerkleTrie; pub use state::{StateAccount, StateDb}; @@ -83,8 +84,6 @@ impl EvmInput { } } - - /// A trait linking the block header to a commitment. pub trait BlockHeaderCommit { /// Creates a verifiable [Commitment] of the `header`. @@ -97,18 +96,18 @@ pub trait BlockHeaderCommit { /// contained within the `input` field. #[derive(Clone, Serialize, Deserialize)] pub struct ComposeInput { - input: Input, + input: BlockInput, commit: C, } impl> ComposeInput { - /// Creates a new composed input from a [Input] and a [BlockHeaderCommit]. - pub const fn new(input: Input, commit: C) -> Self { + /// Creates a new composed input from a [BlockInput] and a [BlockHeaderCommit]. + pub const fn new(input: BlockInput, commit: C) -> Self { Self { input, commit } } /// Disassembles this `ComposeInput`, returning the underlying input and commitment creator. - pub fn into_parts(self) -> (Input, C) { + pub fn into_parts(self) -> (BlockInput, C) { (self.input, self.commit) } @@ -236,7 +235,6 @@ mod private { } pub use private::Commitment; -use crate::block::{BlockInput, Input}; /// Version of a [`Commitment`]. #[repr(u16)] diff --git a/steel/tests/corruption.rs b/steel/tests/corruption.rs index ab95f397..f0b4b34b 100644 --- a/steel/tests/corruption.rs +++ b/steel/tests/corruption.rs @@ -159,9 +159,8 @@ async fn load_or_create<'a, T: serde::ser::Serialize + serde::de::DeserializeOwn fn get_block_input_mut(input: &mut Value) -> &mut Value { let (key, value) = input.as_object_mut().unwrap().into_iter().next().unwrap(); match key.as_str() { - "Block" => &mut value["input"], + "Block" => value, "Beacon" => &mut value["input"], - "History" => &mut value["input"], _ => unreachable!(), } } From 85709fc54f45fa4b43c27f237a1c0dac3422abf1 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 16:14:18 +0100 Subject: [PATCH 12/21] return commitment for Envs without commitment --- steel/src/host/mod.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/steel/src/host/mod.rs b/steel/src/host/mod.rs index 26a29c98..5e058425 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.rs @@ -25,7 +25,8 @@ use crate::{ ethereum::{EthBlockHeader, EthEvmEnv}, history::HistoryCommit, host::db::ProviderDb, - BlockHeaderCommit, Commitment, ComposeInput, EvmBlockHeader, EvmEnv, EvmInput, + BlockHeaderCommit, Commitment, CommitmentVersion, ComposeInput, EvmBlockHeader, EvmEnv, + EvmInput, }; use alloy::eips::eip1898::{HexStringMissingPrefixError, ParseBlockNumberError}; use alloy::{ @@ -191,6 +192,16 @@ where Ok(EvmInput::Block(input)) } + + /// Returns the [Commitment] used to validate the environment. + pub fn commitment(&self) -> Commitment { + Commitment::new( + CommitmentVersion::Block as u16, + self.header.number(), + self.header.seal(), + self.commit.config_id, + ) + } } impl> HostEvmEnv { From faa1a54bc02cef06dfe31575e165d0ed1aeb8309 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 16:53:41 +0100 Subject: [PATCH 13/21] various fixes --- examples/erc20-counter/Cargo.toml | 1 - examples/erc20-counter/apps/Cargo.toml | 1 - .../erc20-counter/apps/src/bin/publisher.rs | 17 ++++++++-------- examples/op/Cargo.toml | 1 - examples/token-stats/Cargo.toml | 2 -- examples/token-stats/host/Cargo.toml | 1 - examples/token-stats/host/src/main.rs | 15 ++++++-------- steel/Cargo.toml | 4 ++-- steel/src/block.rs | 20 ++++++++++++++++--- steel/src/host/mod.rs | 13 +----------- 10 files changed, 35 insertions(+), 40 deletions(-) diff --git a/examples/erc20-counter/Cargo.toml b/examples/erc20-counter/Cargo.toml index e1da573c..6c1f3f87 100644 --- a/examples/erc20-counter/Cargo.toml +++ b/examples/erc20-counter/Cargo.toml @@ -18,7 +18,6 @@ risc0-build = { git = "https://github.com/risc0/risc0", branch = "main", feature risc0-zkvm = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } risc0-zkp = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } -alloy = { version = "0.7", features = ["full"] } alloy-primitives = { version = "0.8", features = ["rlp", "serde", "std"] } alloy-sol-types = { version = "0.8" } anyhow = { version = "1.0.75" } diff --git a/examples/erc20-counter/apps/Cargo.toml b/examples/erc20-counter/apps/Cargo.toml index 379a886f..8f2c0dbb 100644 --- a/examples/erc20-counter/apps/Cargo.toml +++ b/examples/erc20-counter/apps/Cargo.toml @@ -4,7 +4,6 @@ version = { workspace = true } edition = { workspace = true } [dependencies] -alloy = { workspace = true } alloy-primitives = { workspace = true } anyhow = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } diff --git a/examples/erc20-counter/apps/src/bin/publisher.rs b/examples/erc20-counter/apps/src/bin/publisher.rs index 394f749a..c6c9bab9 100644 --- a/examples/erc20-counter/apps/src/bin/publisher.rs +++ b/examples/erc20-counter/apps/src/bin/publisher.rs @@ -16,17 +16,18 @@ // to the Bonsai proving service and publish the received proofs directly // to your deployed app contract. -use alloy::{ - network::EthereumWallet, - providers::ProviderBuilder, - signers::local::PrivateKeySigner, - sol_types::{SolCall, SolValue}, -}; use alloy_primitives::{Address, U256}; use anyhow::{ensure, Context, Result}; use clap::Parser; use erc20_counter_methods::{BALANCE_OF_ELF, BALANCE_OF_ID}; use risc0_ethereum_contracts::encode_seal; +use risc0_steel::alloy::{ + network::EthereumWallet, + providers::ProviderBuilder, + signers::local::PrivateKeySigner, + sol, + sol_types::{SolCall, SolValue}, +}; use risc0_steel::{ ethereum::{EthEvmEnv, ETH_SEPOLIA_CHAIN_SPEC}, host::BlockNumberOrTag, @@ -37,7 +38,7 @@ use tokio::task; use tracing_subscriber::EnvFilter; use url::Url; -alloy::sol! { +sol! { /// Interface to be called by the guest. interface IERC20 { function balanceOf(address account) external view returns (uint); @@ -50,7 +51,7 @@ alloy::sol! { } } -alloy::sol!( +sol!( #[sol(rpc, all_derives)] "../contracts/src/ICounter.sol" ); diff --git a/examples/op/Cargo.toml b/examples/op/Cargo.toml index dc9c4d17..44571487 100644 --- a/examples/op/Cargo.toml +++ b/examples/op/Cargo.toml @@ -13,4 +13,3 @@ risc0-op-steel = { path = "../../op-steel" } risc0-steel = { path = "../../steel" } risc0-ethereum-contracts = { path = "../../contracts" } examples-common = { path = "common" } -alloy = { version = "0.7" } diff --git a/examples/token-stats/Cargo.toml b/examples/token-stats/Cargo.toml index b6c9b435..90fdc58f 100644 --- a/examples/token-stats/Cargo.toml +++ b/examples/token-stats/Cargo.toml @@ -11,7 +11,6 @@ risc0-build = { git = "https://github.com/risc0/risc0", branch = "main" } risc0-zkvm = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } risc0-zkp = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } -alloy = { version = "0.6", features = ["full"] } alloy-primitives = { version = "0.8", features = ["serde", "rlp", "std"] } alloy-rlp = { version = "0.3", default-features = false } alloy-rlp-derive = { version = "0.3", default-features = false } @@ -22,7 +21,6 @@ log = "0.4" token-stats-core = { path = "core" } token-stats-methods = { path = "methods" } once_cell = "1.19" -rlp = "0.5.2" serde = "1.0" thiserror = "2.0" tokio = { version = "1.35", features = ["full"] } diff --git a/examples/token-stats/host/Cargo.toml b/examples/token-stats/host/Cargo.toml index bce1b6e0..575c546f 100644 --- a/examples/token-stats/host/Cargo.toml +++ b/examples/token-stats/host/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -alloy = { workspace = true } alloy-sol-types = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } diff --git a/examples/token-stats/host/src/main.rs b/examples/token-stats/host/src/main.rs index a89e44b4..fa9e3037 100644 --- a/examples/token-stats/host/src/main.rs +++ b/examples/token-stats/host/src/main.rs @@ -12,13 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloy::{ - providers::{Provider, ProviderBuilder}, - sol_types::{SolCall, SolValue}, -}; +use alloy_sol_types::{SolCall, SolValue}; use anyhow::{Context, Result}; use clap::Parser; use risc0_steel::{ + alloy::providers::{Provider, ProviderBuilder}, ethereum::{EthEvmEnv, ETH_MAINNET_CHAIN_SPEC}, Contract, SteelVerifier, }; @@ -89,9 +87,8 @@ async fn main() -> Result<()> { rate ); - let commitment = env.commitment(); - - // Construct the input from the environment. + // Construct the commitment and input from the environment representing the state 12h ago. + let commitment_input1 = env.commitment(); let input1 = env.into_input().await?; // Create another EVM environment for that provider defaulting to the latest block. @@ -100,7 +97,7 @@ async fn main() -> Result<()> { // Preflight the verification of the commitment of the previous input. SteelVerifier::preflight(&mut env) - .verify(&commitment) + .verify(&commitment_input1) .await?; // Preflight the actual contract calls. @@ -128,7 +125,7 @@ async fn main() -> Result<()> { rate ); - // Finally, construct the second input from the environment. + // Finally, construct the second input from the environment representing the latest state. let input2 = env.into_input().await?; println!("Running the guest with the constructed input:"); diff --git a/steel/Cargo.toml b/steel/Cargo.toml index a5f4a5a6..d73445fb 100644 --- a/steel/Cargo.toml +++ b/steel/Cargo.toml @@ -13,7 +13,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -alloy = { workspace = true, optional = true, features = ["eips", "network", "provider-http", "rpc-types"] } +alloy = { workspace = true, optional = true, features = ["full"] } alloy-consensus = { workspace = true } alloy-primitives = { workspace = true, features = ["rlp", "serde"] } alloy-rlp = { workspace = true } @@ -33,7 +33,7 @@ tokio = { workspace = true, optional = true } url = { workspace = true, optional = true } [dev-dependencies] -alloy = { workspace = true, features = ["contract", "node-bindings"] } +alloy = { workspace = true, features = ["node-bindings"] } alloy-trie = { workspace = true } bincode = { workspace = true } risc0-steel = { path = ".", features = ["host"] } diff --git a/steel/src/block.rs b/steel/src/block.rs index ffde7fc7..1615321d 100644 --- a/steel/src/block.rs +++ b/steel/src/block.rs @@ -13,11 +13,11 @@ // limitations under the License. use crate::{ - config::ChainSpec, state::StateDb, Commitment, CommitmentVersion, EvmBlockHeader, EvmEnv, - GuestEvmEnv, MerkleTrie, + config::ChainSpec, state::StateDb, BlockHeaderCommit, Commitment, CommitmentVersion, + EvmBlockHeader, EvmEnv, GuestEvmEnv, MerkleTrie, }; use ::serde::{Deserialize, Serialize}; -use alloy_primitives::{map::HashMap, Bytes}; +use alloy_primitives::{map::HashMap, Bytes, Sealed, B256}; /// Input committing to the corresponding execution block hash. #[derive(Clone, Serialize, Deserialize)] @@ -29,6 +29,20 @@ pub struct BlockInput { ancestors: Vec, } +/// Implement [BlockHeaderCommit] for the unit type. +/// This makes it possible to treat an `HostEvmEnv`, which is used for the [BlockInput] +/// in the same way as any other `HostEvmEnv`. +impl BlockHeaderCommit for () { + fn commit(self, header: &Sealed, config_id: B256) -> Commitment { + Commitment::new( + CommitmentVersion::Block as u16, + header.number(), + header.seal(), + config_id, + ) + } +} + impl BlockInput { /// Converts the input into a [EvmEnv] for verifiable state access in the guest. pub fn into_env(self) -> GuestEvmEnv { diff --git a/steel/src/host/mod.rs b/steel/src/host/mod.rs index 5e058425..26a29c98 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.rs @@ -25,8 +25,7 @@ use crate::{ ethereum::{EthBlockHeader, EthEvmEnv}, history::HistoryCommit, host::db::ProviderDb, - BlockHeaderCommit, Commitment, CommitmentVersion, ComposeInput, EvmBlockHeader, EvmEnv, - EvmInput, + BlockHeaderCommit, Commitment, ComposeInput, EvmBlockHeader, EvmEnv, EvmInput, }; use alloy::eips::eip1898::{HexStringMissingPrefixError, ParseBlockNumberError}; use alloy::{ @@ -192,16 +191,6 @@ where Ok(EvmInput::Block(input)) } - - /// Returns the [Commitment] used to validate the environment. - pub fn commitment(&self) -> Commitment { - Commitment::new( - CommitmentVersion::Block as u16, - self.header.number(), - self.header.seal(), - self.commit.config_id, - ) - } } impl> HostEvmEnv { From 984ecea70972a65189e5c0847556a856d54ca32f Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 17:12:16 +0100 Subject: [PATCH 14/21] mark verifier as unstable --- examples/token-stats/Cargo.toml | 2 +- examples/token-stats/methods/guest/Cargo.toml | 2 +- steel/Cargo.toml | 1 + steel/src/verifier.rs | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/token-stats/Cargo.toml b/examples/token-stats/Cargo.toml index 90fdc58f..63fef825 100644 --- a/examples/token-stats/Cargo.toml +++ b/examples/token-stats/Cargo.toml @@ -4,7 +4,7 @@ members = ["core", "host", "methods"] [workspace.dependencies] # Intra-workspace dependencies -risc0-steel = { path = "../../steel" } +risc0-steel = { path = "../../steel", features = ["unstable-verifier"] } # risc0 monorepo dependencies. risc0-build = { git = "https://github.com/risc0/risc0", branch = "main" } diff --git a/examples/token-stats/methods/guest/Cargo.toml b/examples/token-stats/methods/guest/Cargo.toml index 3923d474..3de4ece9 100644 --- a/examples/token-stats/methods/guest/Cargo.toml +++ b/examples/token-stats/methods/guest/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] alloy-sol-types = { version = "0.8" } -risc0-steel = { path = "../../../../steel" } +risc0-steel = { path = "../../../../steel", features = ["unstable-verifier"] } risc0-zkvm = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false, features = ["std"] } token-stats-core = { path = "../../core" } diff --git a/steel/Cargo.toml b/steel/Cargo.toml index d73445fb..13dc7617 100644 --- a/steel/Cargo.toml +++ b/steel/Cargo.toml @@ -52,3 +52,4 @@ host = [ "dep:url", ] unstable-history = [] +unstable-verifier = [] diff --git a/steel/src/verifier.rs b/steel/src/verifier.rs index 54fb5c40..6f24b175 100644 --- a/steel/src/verifier.rs +++ b/steel/src/verifier.rs @@ -20,6 +20,7 @@ use alloy_primitives::U256; use anyhow::ensure; /// Represents a verifier for validating Steel commitments within Steel. +#[stability::unstable(feature = "verifier")] pub struct SteelVerifier { env: E, } From f7aad343461a29045a8bb93f863da458afcceaba Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 17:14:18 +0100 Subject: [PATCH 15/21] readd alloy --- examples/op/Cargo.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/op/Cargo.toml b/examples/op/Cargo.toml index 44571487..eb15ca98 100644 --- a/examples/op/Cargo.toml +++ b/examples/op/Cargo.toml @@ -7,9 +7,12 @@ debug = 1 lto = true [workspace.dependencies] -risc0-zkvm = { git = "https://github.com/risc0/risc0", branch = "main" } -risc0-build = { git = "https://github.com/risc0/risc0", branch = "main" } risc0-op-steel = { path = "../../op-steel" } risc0-steel = { path = "../../steel" } risc0-ethereum-contracts = { path = "../../contracts" } examples-common = { path = "common" } + +risc0-zkvm = { git = "https://github.com/risc0/risc0", branch = "main" } +risc0-build = { git = "https://github.com/risc0/risc0", branch = "main" } + +alloy = { version = "0.7" } From 93222146cd9166bc98038db8bf5c8ae46b8c55e1 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 17:20:15 +0100 Subject: [PATCH 16/21] fix warnings --- steel/src/host/mod.rs | 33 +-------------------------------- steel/src/lib.rs | 4 +++- steel/src/verifier.rs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/steel/src/host/mod.rs b/steel/src/host/mod.rs index 26a29c98..058a0a84 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.rs @@ -27,8 +27,8 @@ use crate::{ host::db::ProviderDb, BlockHeaderCommit, Commitment, ComposeInput, EvmBlockHeader, EvmEnv, EvmInput, }; -use alloy::eips::eip1898::{HexStringMissingPrefixError, ParseBlockNumberError}; use alloy::{ + eips::eip1898::{HexStringMissingPrefixError, ParseBlockNumberError}, network::{Ethereum, Network}, providers::Provider, rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag, @@ -37,7 +37,6 @@ use alloy::{ use alloy_primitives::B256; use anyhow::{ensure, Result}; use db::{AlloyDb, ProofDb}; -use revm::Database; use url::Url; mod builder; @@ -132,36 +131,6 @@ pub struct HostCommit { config_id: B256, } -impl HostEvmEnv -where - D: Database + Send + 'static, -{ - /// Runs the provided closure that requires mutable access to the database on a thread where - /// blocking is acceptable. - /// - /// It panics if the closure panics. - /// This function is necessary because mutable references to the database cannot be passed - /// directly to `tokio::task::spawn_blocking`. Instead, the database is temporarily taken out of - /// the `HostEvmEnv`, moved into the blocking task, and then restored after the task completes. - pub(crate) async fn spawn_with_db(&mut self, f: F) -> R - where - F: FnOnce(&mut ProofDb) -> R + Send + 'static, - R: Send + 'static, - { - // as mutable references are not possible, the DB must be moved in and out of the task - let mut db = self.db.take().unwrap(); - - let (result, db) = tokio::task::spawn_blocking(|| (f(&mut db), db)) - .await - .expect("DB execution panicked"); - - // restore the DB, so that we never return an env without a DB - self.db = Some(db); - - result - } -} - impl HostEvmEnv { /// Sets the chain ID and specification ID from the given chain spec. /// diff --git a/steel/src/lib.rs b/steel/src/lib.rs index 32127636..9c7ed3e5 100644 --- a/steel/src/lib.rs +++ b/steel/src/lib.rs @@ -44,6 +44,7 @@ mod merkle; mod mpt; pub mod serde; mod state; +#[cfg(feature = "unstable-history")] mod verifier; pub use beacon::BeaconInput; @@ -51,12 +52,13 @@ pub use block::BlockInput; pub use contract::{CallBuilder, Contract}; pub use mpt::MerkleTrie; pub use state::{StateAccount, StateDb}; -pub use verifier::SteelVerifier; #[cfg(feature = "unstable-history")] pub use history::HistoryInput; #[cfg(not(feature = "unstable-history"))] pub(crate) use history::HistoryInput; +#[cfg(feature = "unstable-history")] +pub use verifier::SteelVerifier; /// The serializable input to derive and validate an [EvmEnv] from. #[non_exhaustive] diff --git a/steel/src/verifier.rs b/steel/src/verifier.rs index 6f24b175..03e222c6 100644 --- a/steel/src/verifier.rs +++ b/steel/src/verifier.rs @@ -55,6 +55,7 @@ impl<'a, H: EvmBlockHeader> SteelVerifier<&'a GuestEvmEnv> { #[cfg(feature = "host")] mod host { use super::*; + use crate::host::db::ProofDb; use crate::{history::beacon_roots, host::HostEvmEnv}; use anyhow::Context; use revm::Database; @@ -108,6 +109,36 @@ mod host { } } } + + impl HostEvmEnv + where + D: Database + Send + 'static, + { + /// Runs the provided closure that requires mutable access to the database on a thread where + /// blocking is acceptable. + /// + /// It panics if the closure panics. + /// This function is necessary because mutable references to the database cannot be passed + /// directly to `tokio::task::spawn_blocking`. Instead, the database is temporarily taken out of + /// the `HostEvmEnv`, moved into the blocking task, and then restored after the task completes. + async fn spawn_with_db(&mut self, f: F) -> R + where + F: FnOnce(&mut ProofDb) -> R + Send + 'static, + R: Send + 'static, + { + // as mutable references are not possible, the DB must be moved in and out of the task + let mut db = self.db.take().unwrap(); + + let (result, db) = tokio::task::spawn_blocking(|| (f(&mut db), db)) + .await + .expect("DB execution panicked"); + + // restore the DB, so that we never return an env without a DB + self.db = Some(db); + + result + } + } } fn validate_block_number(header: &impl EvmBlockHeader, block_number: U256) -> anyhow::Result { From b048895c487e1c53dcc5548479bb3a0ed504eafa Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 17:32:26 +0100 Subject: [PATCH 17/21] fix feature name --- steel/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/steel/src/lib.rs b/steel/src/lib.rs index 9c7ed3e5..7bc54ae1 100644 --- a/steel/src/lib.rs +++ b/steel/src/lib.rs @@ -44,7 +44,7 @@ mod merkle; mod mpt; pub mod serde; mod state; -#[cfg(feature = "unstable-history")] +#[cfg(feature = "unstable-verifier")] mod verifier; pub use beacon::BeaconInput; @@ -57,7 +57,7 @@ pub use state::{StateAccount, StateDb}; pub use history::HistoryInput; #[cfg(not(feature = "unstable-history"))] pub(crate) use history::HistoryInput; -#[cfg(feature = "unstable-history")] +#[cfg(feature = "unstable-verifier")] pub use verifier::SteelVerifier; /// The serializable input to derive and validate an [EvmEnv] from. From 813a0978be7cc50dc25257e3ddd64085484a4c3c Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 16 Dec 2024 17:37:09 +0100 Subject: [PATCH 18/21] update CHANGELOG.md --- steel/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/steel/CHANGELOG.md b/steel/CHANGELOG.md index 06917021..3e0533dc 100644 --- a/steel/CHANGELOG.md +++ b/steel/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### ⚡️ Features + +- Introduce the `SteelVerifier`, which acts as a built-in Steel `Contract` to verify Steel commitments. It is used like any other `Contract`, during the preflight step and in the guest. This functionality is currently marked unstable and must be enabled using the `unstable-verifier` feature. + ## [1.2.0](https://github.com/risc0/risc0-ethereum/releases/tag/v1.2.0) ### ⚡️ Features From 7e74a7bd5e48f37d275661b4b8f581d403811f36 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Mon, 6 Jan 2025 18:10:10 +0100 Subject: [PATCH 19/21] Apply suggestions from code review Co-authored-by: Victor Snyder-Graf --- steel/src/history/beacon_roots.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index 2c7767ce..b314c9be 100644 --- a/steel/src/history/beacon_roots.rs +++ b/steel/src/history/beacon_roots.rs @@ -35,6 +35,7 @@ const CODE_HASH: B256 = b256!("f57acd40259872606d76197ef052f3d35588dadf919ee1f0e /// Enum representing possible errors that can occur within the `BeaconRootsContract`. #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum Error { /// Error indicating that the contract is not deployed at the expected address. #[error("wrong or no contract deployed")] @@ -154,7 +155,8 @@ impl Database for BeaconRootsState { } fn code_by_hash(&mut self, _code_hash: B256) -> Result { - unreachable!() + // should never be called. + unimplemented!() } #[inline(always)] @@ -168,7 +170,8 @@ impl Database for BeaconRootsState { } fn block_hash(&mut self, _number: u64) -> Result { - unreachable!() + // should never be called. + unimplemented!() } } From 18070b4b707a57a93ed2c1529161fbc9defaa4fc Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Wed, 8 Jan 2025 18:59:17 +0100 Subject: [PATCH 20/21] address review comment --- steel/src/verifier.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/steel/src/verifier.rs b/steel/src/verifier.rs index 03e222c6..e8f45587 100644 --- a/steel/src/verifier.rs +++ b/steel/src/verifier.rs @@ -17,7 +17,7 @@ use crate::{ GuestEvmEnv, }; use alloy_primitives::U256; -use anyhow::ensure; +use anyhow::{ensure, Context}; /// Represents a verifier for validating Steel commitments within Steel. #[stability::unstable(feature = "verifier")] @@ -142,7 +142,8 @@ mod host { } fn validate_block_number(header: &impl EvmBlockHeader, block_number: U256) -> anyhow::Result { - let block_number: u64 = block_number.saturating_to(); + let block_number = block_number.try_into().context("invalid block number")?; + // if block_number > header.number(), this will also be caught in the following `ensure` let diff = header.number().saturating_sub(block_number); ensure!( diff > 0 && diff <= 256, From fa53c376a7ed4a8232951c0cc167237895352941 Mon Sep 17 00:00:00 2001 From: Wolfgang Welz Date: Wed, 8 Jan 2025 19:03:23 +0100 Subject: [PATCH 21/21] update license header --- steel/src/history/beacon_roots.rs | 2 +- steel/src/mpt.rs | 2 +- steel/src/verifier.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index b314c9be..ce0e75b7 100644 --- a/steel/src/history/beacon_roots.rs +++ b/steel/src/history/beacon_roots.rs @@ -1,4 +1,4 @@ -// Copyright 2024 RISC Zero, Inc. +// Copyright 2025 RISC Zero, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/steel/src/mpt.rs b/steel/src/mpt.rs index 80e197d6..cd80474e 100644 --- a/steel/src/mpt.rs +++ b/steel/src/mpt.rs @@ -1,4 +1,4 @@ -// Copyright 2024 RISC Zero, Inc. +// Copyright 2025 RISC Zero, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/steel/src/verifier.rs b/steel/src/verifier.rs index e8f45587..42bfb907 100644 --- a/steel/src/verifier.rs +++ b/steel/src/verifier.rs @@ -1,4 +1,4 @@ -// Copyright 2024 RISC Zero, Inc. +// Copyright 2025 RISC Zero, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.