diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd05ec8..6db86287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. - Add a new `precompiles` module with type-safe wrappers for the EIP-2935 `HistoryStorage` and EIP-4788 `BeaconRoots` contracts. - The `Contract` API now includes a `raw` method to allow for direct calls with raw calldata. This provides greater flexibility when interacting with non-standard interfaces, such as precompiles. - Add `Event` support for chains other than Ethereum. +- Added `MultiblockEvmEnv` and `MultiblockEvmInput to enable verifiable computation across multiple blocks within a single proof. The guest environment now securely validates the integrity of the block sequence by verifying commitments between each consecutive block. - Improve EVM error handling to support calls without return data. - Add `EvmEnvBuilder::consensus_commitment_slot` to specify a beacon chain slot directly as the commitment target. This is useful for light client verification scenarios where the verifier has direct access to beacon chain state. @@ -22,7 +23,8 @@ All notable changes to this project will be documented in this file. ### ⚙️ Miscellaneous - The `Steel.sol` library now uses the OpenZeppelin Blockhash library to provide safer access to historical block hashes up to 8,191 blocks. -- Adapt `SteelVerifier` to use the history storage contract when available, in line with `Steel.validateCommitment`. +- Adapt `SteelVerifier` to use the history storage contract when available, in line with `Steel.validateCommitment`. It now includes optimizations for directly verifying adjacent blocks via the parent hash field. +- The `EvmEnv::merge` function is now more flexible, allowing environments with different commitments to be merged. - Add a warning log when the provider's chain ID does not match the chain spec configuration. - Improved error messages when the execution block incorrectly matches the commitment block for historical commitments. diff --git a/crates/steel/src/block.rs b/crates/steel/src/block.rs index 67f7ab55..12f54d58 100644 --- a/crates/steel/src/block.rs +++ b/crates/steel/src/block.rs @@ -19,7 +19,6 @@ use crate::{ use ::serde::{Deserialize, Serialize}; use alloy_consensus::TxReceipt; use alloy_primitives::{B256, Bytes, Sealable, Sealed, map::HashMap}; -use std::marker::PhantomData; /// Input committing to the corresponding execution block hash. #[derive(Clone, Serialize, Deserialize)] @@ -30,8 +29,6 @@ pub struct BlockInput { contracts: Vec, ancestors: Vec, receipts: Option>, - #[serde(skip)] - phantom: PhantomData, } /// Implement [BlockHeaderCommit] for the unit type. @@ -50,6 +47,8 @@ impl BlockHeaderCommit for () { impl BlockInput { /// Converts the input into a [EvmEnv] for verifiable state access in the guest. + /// + /// This method verifies that the state matches the state root in the header and panics if not. pub fn into_env(self, chain_spec: &ChainSpec) -> GuestEvmEnv { // verify that the state root matches the state trie let state_root = self.state_trie.hash_slow(); @@ -119,7 +118,7 @@ pub mod host { use alloy_primitives::Sealed; use anyhow::{anyhow, ensure}; use log::debug; - use std::{fmt::Display, marker::PhantomData}; + use std::fmt::Display; impl BlockInput { /// Creates the `BlockInput` containing the necessary EVM state that can be verified against @@ -162,6 +161,7 @@ pub mod host { .transpose() .map_err(|err| anyhow!("invalid receipt: {err}"))?; + debug!("Preparing input for block {}:", header.number()); debug!("state size: {}", state_trie.size()); debug!("storage tries: {}", storage_tries.len()); debug!( @@ -179,7 +179,6 @@ pub mod host { contracts, ancestors, receipts, - phantom: PhantomData, }; Ok(input) diff --git a/crates/steel/src/contract.rs b/crates/steel/src/contract.rs index b852e764..83f3a396 100644 --- a/crates/steel/src/contract.rs +++ b/crates/steel/src/contract.rs @@ -286,7 +286,7 @@ mod host { impl CallBuilder, F, C>> where N: Network, - P: Provider + Send + Sync + 'static, + P: Provider + 'static, S: SolCall + Send + Sync + 'static, ::Return: Send, F: EvmFactory, diff --git a/crates/steel/src/ethereum.rs b/crates/steel/src/ethereum.rs index d4a2d323..73a330d4 100644 --- a/crates/steel/src/ethereum.rs +++ b/crates/steel/src/ethereum.rs @@ -14,7 +14,8 @@ //! Type aliases and specifications for Ethereum. use crate::{ - CallError, EvmBlockHeader, EvmEnv, EvmFactory, EvmInput, EvmSpecId, + CallError, EvmBlockHeader, EvmEnv, EvmFactory, EvmInput, EvmSpecId, MultiblockEvmEnv, + MultiblockEvmInput, config::{ChainSpec, ForkCondition}, serde::{Eip2718Wrapper, RlpHeader}, }; @@ -32,6 +33,9 @@ use revm::{ use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, error::Error as StdError, sync::LazyLock}; +/// [EvmBlockHeader] for Ethereum. +pub type EthBlockHeader = RlpHeader; + /// [ChainSpec] for Ethereum. pub type EthChainSpec = ChainSpec; @@ -44,8 +48,11 @@ pub type EthEvmEnv = EvmEnv; /// [EvmInput] for Ethereum. pub type EthEvmInput = EvmInput; -/// [EvmBlockHeader] for Ethereum. -pub type EthBlockHeader = RlpHeader; +/// [MultiblockEvmEnv] for Ethereum. +pub type EthMultiblockEvmEnv = MultiblockEvmEnv; + +/// [MultiblockEvmInput] for Ethereum. +pub type EthMultiblockEvmInput = MultiblockEvmInput; macro_rules! define_chain_specs { ($($(#[$meta:meta])* $name:ident { chain_id: $id:literal, forks: $forks:expr $(,)? })*) => { diff --git a/crates/steel/src/history/eip2935/history_storage.rs b/crates/steel/src/history/eip2935/history_storage.rs index 7f8865ee..e9d0f004 100644 --- a/crates/steel/src/history/eip2935/history_storage.rs +++ b/crates/steel/src/history/eip2935/history_storage.rs @@ -119,7 +119,7 @@ mod tests { ignore = "RPC tests are disabled" )] async fn contract() { - // TODO: Make this an Anvil provider, once Anvil has EIP-2935 support + // TODO(https://github.com/foundry-rs/foundry/issues/10357): Use Anvil provider let el = ProviderBuilder::new().connect_http(get_el_url()); // get the latest header diff --git a/crates/steel/src/history/eip2935/mod.rs b/crates/steel/src/history/eip2935/mod.rs index 1a97119d..d2340a1c 100644 --- a/crates/steel/src/history/eip2935/mod.rs +++ b/crates/steel/src/history/eip2935/mod.rs @@ -29,7 +29,7 @@ mod history_storage; #[derive(Clone, Serialize, Deserialize)] pub struct HistoryInput { input: BlockInput, - commit: HistoryCommit<::Header>, + commit: HistoryCommit, } /// Commitment that an execution block is an ancestor of a specific other execution block. diff --git a/crates/steel/src/host/builder.rs b/crates/steel/src/host/builder.rs index 1d8408ce..749cca5b 100644 --- a/crates/steel/src/host/builder.rs +++ b/crates/steel/src/host/builder.rs @@ -13,10 +13,10 @@ // limitations under the License. use crate::{ - CommitmentVersion, EvmBlockHeader, EvmEnv, EvmFactory, EvmSpecId, + CommitmentVersion, EvmBlockHeader, EvmEnv, EvmFactory, EvmInput, EvmSpecId, beacon::{BeaconCommit, host::client::BeaconClient}, config::ChainSpec, - ethereum::EthEvmFactory, + ethereum::{EthChainSpec, EthEvmFactory}, history::{Eip2935HistoryCommit, HistoryCommit}, host::{ BlockId, BlockNumberOrTag, EthHostEvmEnv, HostCommit, HostEvmEnv, @@ -29,7 +29,7 @@ use alloy::{ }; use alloy_primitives::{B256, BlockHash, BlockNumber, Sealable, Sealed}; use anyhow::{Context, Result, anyhow, ensure}; -use std::{fmt::Display, marker::PhantomData}; +use std::{fmt::Display, future::Future, marker::PhantomData}; use url::Url; impl EvmEnv<(), F, ()> { @@ -47,14 +47,7 @@ impl EvmEnv<(), F, ()> { /// # } /// ``` pub fn builder() -> EvmEnvBuilder<(), F, (), ()> { - EvmEnvBuilder { - provider: (), - provider_config: ProviderConfig::default(), - block: BlockId::default(), - chain_spec: (), - commitment_config: (), - phantom: PhantomData, - } + EvmEnvBuilder::new() } } @@ -75,7 +68,20 @@ pub struct EvmEnvBuilder { block: BlockId, chain_spec: S, commitment_config: C, - phantom: PhantomData, + phantom: PhantomData F>, +} + +impl EvmEnvBuilder<(), F, (), ()> { + pub(crate) fn new() -> Self { + EvmEnvBuilder { + provider: (), + provider_config: ProviderConfig::default(), + block: BlockId::default(), + chain_spec: (), + commitment_config: (), + phantom: PhantomData, + } + } } impl EvmEnvBuilder<(), EthEvmFactory, S, ()> { @@ -254,6 +260,23 @@ impl EvmEnvBuilder { self } + /// Creates a clone of this builder configured for the given EVM execution block with elided + /// commitment config. + pub(crate) fn clone_with_block(&self, block: impl Into) -> EvmEnvBuilder + where + P: Clone, + S: Clone, + { + EvmEnvBuilder { + provider: self.provider.clone(), + provider_config: self.provider_config.clone(), + block: block.into(), + chain_spec: self.chain_spec.clone(), + commitment_config: (), + phantom: PhantomData, + } + } + /// Returns the [EvmBlockHeader] of the specified block. /// /// If `block` is `None`, the block based on the current builder configuration is used instead. @@ -655,6 +678,81 @@ fn ensure_distinct( Ok(()) } +/// Extension trait used by [HostMultiblockEvmEnv] to build an [EvmInput] instance from an +/// [EvmEnvBuilder] given an existing [EvmEnv]. +/// +/// This trait abstracts the process of transforming a configured [EvmEnvBuilder] into a +/// corresponding [EvmInput]. Essentially, it applies the specified commitment type from the +/// [EvmEnvBuilder] to the EVM data from the given [EvmEnv]. +/// +/// [HostMultiblockEvmEnv]: crate::multiblock::host::HostMultiblockEvmEnv +pub trait InputBuilder, F: EvmFactory>: Send { + /// Consumes this builder and constructs an [EvmInput] from the given [EvmEnv]. + /// + /// The returned future performs any necessary commitment computation, or state verification + /// required by the builder’s configuration. + /// It returns an error, if this process fails or if the [ChainSpec] config of the + /// [EvmEnvBuilder] and the [EvmEnv] do not match. + fn build_input( + self, + env: HostEvmEnv, F, ()>, + ) -> impl Future>> + Send; +} + +macro_rules! build_input { + ($D:ty, $F:ty) => { + async fn build_input(self, env: HostEvmEnv<$D, $F, ()>) -> Result> { + // rebuild an empty environment for the same block + let builder = self.block_hash(env.header().seal()); + let empty_env = builder.build().await.context("builder failed")?; + // merge execution state and verify compatibility + let env = empty_env + .merge(env) + .context("environment not compatible with builder")?; + + env.into_input().await + } + }; +} + +impl InputBuilder for EvmEnvBuilder, ()> +where + N: Network, + P: Provider, + F: EvmFactory, + F::Header: TryFrom<::HeaderResponse>, + ::HeaderResponse>>::Error: Display, + F::Receipt: TryFrom<::ReceiptResponse>, + ::ReceiptResponse>>::Error: Display, +{ + build_input!(ProviderDb, F); +} + +impl InputBuilder for EvmEnvBuilder, Eip2935History> +where + N: Network, + P: Provider, + F: EvmFactory, + F::Header: TryFrom<::HeaderResponse>, + ::HeaderResponse>>::Error: Display, + F::Receipt: TryFrom<::ReceiptResponse>, + ::ReceiptResponse>>::Error: Display, +{ + build_input!(ProviderDb, F); +} + +impl> InputBuilder + for EvmEnvBuilder +{ + build_input!(ProviderDb, EthEvmFactory); +} + +impl> InputBuilder + for EvmEnvBuilder +{ + build_input!(ProviderDb, EthEvmFactory); +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/steel/src/host/db/proof.rs b/crates/steel/src/host/db/proof.rs index 5ae18444..e2550e97 100644 --- a/crates/steel/src/host/db/proof.rs +++ b/crates/steel/src/host/db/proof.rs @@ -202,13 +202,6 @@ impl> ProofDb> { /// Returns the merkle proofs (sparse [MerkleTrie]) for the state and all storage queries /// recorded by the [RevmDatabase]. pub(crate) async fn state_proof(&mut self) -> Result<(MerkleTrie, Vec)> { - ensure!( - !self.accounts.is_empty() - || !self.block_hash_numbers.is_empty() - || !self.log_filters.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(); diff --git a/crates/steel/src/host/db/provider.rs b/crates/steel/src/host/db/provider.rs index c84fec50..417b4628 100644 --- a/crates/steel/src/host/db/provider.rs +++ b/crates/steel/src/host/db/provider.rs @@ -49,7 +49,7 @@ impl DBErrorMarker for Error {} /// must *not* be executed inside an async runtime, or it will panic when trying to block. If the /// immediate context is only synchronous, but a transitive caller is async, use /// [tokio::task::spawn_blocking] around the calls that need to be blocked. -pub struct ProviderDb> { +pub struct ProviderDb { /// Provider to fetch the data from. provider: P, /// Configuration of the provider. @@ -61,7 +61,7 @@ pub struct ProviderDb> { /// Bytecode cache to allow querying bytecode by hash instead of address. contracts: B256HashMap, - phantom: PhantomData, + phantom: PhantomData N>, } /// Additional configuration for a [Provider]. diff --git a/crates/steel/src/host/mod.rs b/crates/steel/src/host/mod.rs index 4cde8ae3..9df7f066 100644 --- a/crates/steel/src/host/mod.rs +++ b/crates/steel/src/host/mod.rs @@ -32,7 +32,7 @@ use alloy::{ providers::Provider, rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag, }; -use alloy_primitives::{B256, BlockHash}; +use alloy_primitives::{B256, BlockHash, BlockNumber}; use anyhow::{Result, ensure}; use db::{ProofDb, ProviderDb}; use std::{ @@ -40,14 +40,15 @@ use std::{ str::FromStr, }; -pub use builder::{Beacon, EvmEnvBuilder, History}; - mod builder; pub mod db; +pub use crate::multiblock::host::HostMultiblockEvmEnv; +pub use builder::{Beacon, Eip2935History, EvmEnvBuilder, History, InputBuilder}; + /// A Block Identifier. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum BlockId { +pub(crate) enum BlockId { /// A block hash Hash(BlockHash), /// A block number or tag (e.g. latest) @@ -101,6 +102,12 @@ impl From for BlockId { } } +impl From for BlockId { + fn from(number: BlockNumber) -> Self { + Self::Number(number.into()) + } +} + /// A block number (or tag - "latest", "safe", "finalized"). /// This enum is used to specify which block to query when interacting with the blockchain. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] @@ -276,7 +283,7 @@ impl HostEvmEnv { /// # Ok(()) /// # } /// ``` - pub fn merge(self, mut other: Self) -> Result { + pub fn merge(self, mut other: HostEvmEnv) -> Result { let Self { mut db, chain_id, @@ -285,13 +292,14 @@ impl HostEvmEnv { commit, } = self; - ensure!(chain_id == other.chain_id, "configuration mismatch"); - ensure!(spec_id == other.spec_id, "configuration mismatch"); + ensure!(chain_id == other.chain_id, "chain ID mismatch"); + ensure!(spec_id == other.spec_id, "EVM specification mismatch"); + ensure!(header.seal() == other.header.seal(), "header mismatch"); + // the commitments do not need to match as long as the config_id is consistent ensure!( - header.seal() == other.header.seal(), - "execution header mismatch" + commit.config_id == other.commit.config_id, + "configuration mismatch" ); - // the commitments do not need to match as long as the cfg_env is consistent // safe unwrap: EvmEnv is never returned without a DB let db = db.take().unwrap(); @@ -317,7 +325,7 @@ where F::Receipt: TryFrom<::ReceiptResponse>, ::ReceiptResponse>>::Error: Display, { - /// Converts the environment into a [EvmInput] committing to an execution block hash. + /// Converts the environment into a [EvmInput] committing to the execution block hash. pub async fn into_input(self) -> Result> { let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?; diff --git a/crates/steel/src/lib.rs b/crates/steel/src/lib.rs index ad11d4fc..73af1d42 100644 --- a/crates/steel/src/lib.rs +++ b/crates/steel/src/lib.rs @@ -36,7 +36,12 @@ use revm::{ Database as RevmDatabase, context::{BlockEnv, result::HaltReasonTr}, }; -use std::{error::Error, fmt, fmt::Debug, hash::Hash}; +use std::{ + error::Error, + fmt, + fmt::{Debug, Display}, + hash::Hash, +}; pub mod account; pub mod beacon; @@ -50,6 +55,7 @@ pub mod history; pub mod host; mod merkle; mod mpt; +mod multiblock; pub mod precompiles; pub mod serde; mod state; @@ -64,8 +70,9 @@ pub use contract::{CallBuilder, CallError, Contract, RawCall}; pub use event::Event; pub use history::{Eip2935HistoryInput, HistoryInput}; pub use mpt::MerkleTrie; +pub use multiblock::{MultiblockEvmEnv, MultiblockEvmInput}; pub use state::{StateAccount, StateDb}; -pub use verifier::SteelVerifier; +pub use verifier::{EIP2935_HISTORY_LIMIT, HISTORY_LIMIT, SteelVerifier}; /// The serializable input to derive and validate an [EvmEnv] from. #[non_exhaustive] @@ -82,9 +89,7 @@ pub enum EvmInput { } impl EvmInput { - /// Converts the input into a [EvmEnv] for execution. - /// - /// This method verifies that the state matches the state root in the header and panics if not. + /// Converts the input into a [EvmEnv] for verifiable state access in the guest. #[inline] pub fn into_env(self, chain_spec: &ChainSpec) -> GuestEvmEnv { match self { @@ -435,16 +440,27 @@ impl Commitment { } } +impl Display for Commitment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (id, version_code) = self.decode_id(); + let version_str = match CommitmentVersion::n(version_code) { + Some(v) => format!("{v:?}"), + None => format!("Unknown({version_code:#x})"), + }; + write!(f, "{version_str}({id})={:#}", &self.digest) + } +} + impl Debug for Commitment { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (id, version_code) = self.decode_id(); - let version = match CommitmentVersion::n(version_code) { + let version_str = match CommitmentVersion::n(version_code) { Some(v) => format!("{v:?}"), - None => format!("Unknown({version_code:x})"), + None => format!("Unknown({version_code:#x})"), }; f.debug_struct("Commitment") - .field("version", &version) + .field("version", &version_str) .field("id", &id) .field("digest", &self.digest) .field("configID", &self.configID) diff --git a/crates/steel/src/multiblock.rs b/crates/steel/src/multiblock.rs new file mode 100644 index 00000000..e9a739fa --- /dev/null +++ b/crates/steel/src/multiblock.rs @@ -0,0 +1,532 @@ +// 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. +// 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. + +//! Types for verifiable computation across multiple blocks of the same chain. +//! +//! This module provides [MultiblockEvmEnv] and [MultiblockEvmInput] for executing and proving EVM +//! state queries spanning multiple historical blocks. The guest environment validates that all +//! blocks belong to the same chain by verifying commitments between consecutive blocks. +use crate::{ + Commitment, EvmBlockHeader, EvmEnv, EvmFactory, EvmInput, GuestEvmEnv, StateDb, SteelVerifier, + config::ChainSpec, +}; +use alloy_primitives::BlockNumber; +use delegate::delegate; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// A sequence of [EvmEnv] that form a subsequence in a single chain. +/// +/// ### Examples +/// +/// Query token balances across multiple blocks to compute a time-weighted average: +/// +/// ```rust,no_run +/// # use risc0_steel::{ +/// # ethereum::{EthEvmEnv, ETH_MAINNET_CHAIN_SPEC}, +/// # host::HostMultiblockEvmEnv, +/// # Contract, EvmBlockHeader +/// # }; +/// # use alloy::providers::{Provider,ProviderBuilder}; +/// # use alloy_primitives::address; +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() -> anyhow::Result<()> { +/// // === Host Setup === +/// let provider = +/// ProviderBuilder::new().connect_http("https://ethereum-rpc.publicnode.com".parse()?); +/// let latest = provider.get_block_number().await?; +/// +/// let builder = EthEvmEnv::builder().provider(provider).chain_spec(Ð_MAINNET_CHAIN_SPEC); +/// let mut envs = HostMultiblockEvmEnv::from_builder(builder); +/// +/// // Query state at multiple points in time (e.g., every ~24 hours) +/// for i in 0..3 { +/// let block = latest - i * 7200; // ~24h apart at 12s/block +/// let env = envs.get_or_build(block).await?; +/// // Preflight your contract calls here... +/// } +/// +/// // Generate input for the guest +/// let evm_input = envs.into_input().await?; +/// +/// // === Guest Execution === +/// let envs = evm_input.into_env(Ð_MAINNET_CHAIN_SPEC); +/// +/// // Process each block's state +/// for env in envs.iter() { +/// let block_number = env.header().number(); +/// // Execute contract calls... +/// } +/// +/// // Commit using the final block's commitment (covers all blocks) +/// let commitment = envs.into_commitment(); +/// # Ok(()) +/// # } +/// ``` +pub struct MultiblockEvmEnv(BTreeMap>); + +/// The serializable input to derive and validate an [MultiblockEvmEnv] from. +#[derive(Clone, Serialize, Deserialize)] +pub struct MultiblockEvmInput(Vec>); + +impl MultiblockEvmInput { + /// Converts the input into a [MultiblockEvmEnv] for verifiable state access in the guest. + /// + /// This method verifies that all the envs belong to the same chain. + pub fn into_env( + self, + chain_spec: &ChainSpec, + ) -> MultiblockEvmEnv { + assert!(!self.0.is_empty(), "Empty environment"); + + let mut envs = BTreeMap::new(); + for env_input in self.0 { + let env = env_input.into_env(chain_spec); + if let Some(collision) = envs.insert(env.header().number(), env) { + panic!( + "More than one env for block {}", + collision.header().number() + ); + }; + } + + for (env_prev, env) in envs.values().zip(envs.values().skip(1)) { + SteelVerifier::new(env).verify(env_prev.commitment()); + } + + MultiblockEvmEnv(envs) + } +} + +impl MultiblockEvmEnv { + delegate! { + to self.0 { + /// Returns the number of environments. + pub fn len(&self) -> usize; + /// Returns `true` if it contains no environments. + pub fn is_empty(&self) -> bool; + } + } + + /// Returns a reference to the environment corresponding to the block number. + pub fn get(&self, num: BlockNumber) -> Option<&GuestEvmEnv> { + self.0.get(&num) + } + + /// Returns the first environment, i.e. the environment with the smallest block number. + pub fn first(&self) -> &GuestEvmEnv { + // safe unwrap: MultiblockEvmEnv cannot be constructed empty + self.0.first_key_value().unwrap().1 + } + + /// Returns the final environment, i.e. the environment with the largest block number. + pub fn last(&self) -> &GuestEvmEnv { + // safe unwrap: MultiblockEvmEnv cannot be constructed empty + self.0.last_key_value().unwrap().1 + } + + /// Gets an iterator over the block numbers in ascending order. + pub fn block_numbers(&self) -> impl Iterator + '_ { + self.0.keys().copied() + } + + /// Gets an iterator over the environments, ordered by block number in ascending order. + pub fn iter(&self) -> impl Iterator> { + self.0.values() + } + + /// Returns the [Commitment] for the entire block sequence. + /// + /// The returned commitment is from the final (highest) block. Verifying this single commitment + /// on-chain validates the entire sequence because each block is cryptographically linked to its + /// predecessors. + pub fn commitment(&self) -> &Commitment { + self.last().commitment() + } + + /// Consumes and returns the [Commitment] for the entire block sequence. + /// + /// The returned commitment is from the final (highest) block. Verifying this single commitment + /// on-chain validates the entire sequence because each block is cryptographically linked to its + /// predecessors. + pub fn into_commitment(mut self) -> Commitment { + // safe unwrap: MultiblockEvmEnv cannot be constructed empty + let env = self.0.pop_last().unwrap().1; + env.into_commitment() + } +} + +#[cfg(feature = "host")] +pub(crate) mod host { + use super::*; + use crate::{ + EvmSpecId, + host::{ + EvmEnvBuilder, HostCommit, HostEvmEnv, InputBuilder, + db::{ProofDb, ProviderDb}, + }, + verifier, + }; + use alloy::providers::{Network, Provider}; + use anyhow::{Context, bail, ensure}; + use delegate::delegate; + use std::{collections::btree_map::Entry, fmt::Display}; + + /// A host-side collection of [EvmEnv] instances spanning multiple blocks of the same chain. + /// + /// This type uses an [EvmEnvBuilder] as a template to construct individual environments on + /// demand via [HostMultiblockEvmEnv::get_or_build()]. This design avoids duplicating + /// configuration methods while ensuring all environments share consistent settings (provider, + /// chain spec, commitment type). + /// + /// See [MultiblockEvmEnv] for usage examples. + pub struct HostMultiblockEvmEnv<'a, N, P, F: EvmFactory, C> { + template: EvmEnvBuilder, C>, + env: MultiblockEvmEnv>, F, HostCommit<()>>, + } + + impl<'a, N, P, F, C> HostMultiblockEvmEnv<'a, N, P, F, C> + where + N: Network, + P: Provider + Clone + 'static, + F: EvmFactory, + F::Header: TryFrom<::HeaderResponse>, + ::HeaderResponse>>::Error: Display, + F::Receipt: TryFrom<::ReceiptResponse>, + ::ReceiptResponse>>::Error: Display, + EvmEnvBuilder, C>: InputBuilder, + { + /// Creates a new [HostMultiblockEvmEnv] using the given [EvmEnvBuilder] as a template. + /// + /// Prefer using [EvmEnvBuilder::build_multi()] for a more fluent API. + pub fn from_builder(template: EvmEnvBuilder, C>) -> Self { + Self { + template, + env: MultiblockEvmEnv(BTreeMap::new()), + } + } + + delegate! { + to self.env.0 { + /// Returns the number of environments. + pub fn len(&self) -> usize; + /// Returns `true` if it contains no environments. + pub fn is_empty(&self) -> bool; + } + } + + /// Gets or creates an environment for the specified block number. + /// + /// If an environment for this block already exists, returns a mutable reference to it. + /// Otherwise, creates a new environment using the template builder and inserts it. + /// + /// Blocks can be added in any order, they will be properly ordered when + /// [HostMultiblockEvmEnv::into_input] is called. + pub async fn get_or_build( + &mut self, + num: BlockNumber, + ) -> anyhow::Result<&mut HostEvmEnv, F, ()>> { + match self.env.0.entry(num) { + Entry::Occupied(entry) => Ok(entry.into_mut()), + Entry::Vacant(entry) => { + Ok(entry.insert(self.template.clone_with_block(num).build().await?)) + } + } + } + + /// Returns a mutable reference to the environment corresponding to the block number. + pub fn get_mut( + &mut self, + num: BlockNumber, + ) -> Option<&mut HostEvmEnv, F, ()>> { + self.env.0.get_mut(&num) + } + + /// Returns a mutable reference to the first environment, i.e. the environment with the + /// smallest block number, or `None` if empty. + pub fn first_mut(&mut self) -> Option<&mut HostEvmEnv, F, ()>> { + self.env.0.first_entry().map(|entry| entry.into_mut()) + } + + /// Returns a mutable reference to the last environment, i.e. the environment with the + /// largest block number, or `None` if empty. + pub fn last_mut(&mut self) -> Option<&mut HostEvmEnv, F, ()>> { + self.env.0.last_entry().map(|entry| entry.into_mut()) + } + + /// Gets an iterator over the block numbers in ascending order. + pub fn block_numbers(&self) -> impl Iterator + '_ { + self.env.0.keys().copied() + } + + /// Gets a mutable iterator over the environments in order by their block number. + pub fn iter_mut( + &mut self, + ) -> impl Iterator, F, ()>> { + self.env.0.values_mut() + } + + /// Converts the environment into a [MultiblockEvmInput] using the commitment method + /// specified in the template builder. + /// + /// Each environment's commitment is verified against its successor using [SteelVerifier], + /// ensuring all blocks belong to the same chain. The verification strategy adapts based on + /// distance between blocks (direct block hash vs. EIP-2935 history commitment). + pub async fn into_input(self) -> anyhow::Result> { + ensure!( + !self.is_empty(), + "cannot build input: no blocks added via get_or_build()" + ); + + let mut inputs = Vec::with_capacity(self.env.0.len()); + let mut iter = self.env.0.into_values(); + // safe unwrap: empty checked above + let mut current_env = iter.next().unwrap(); + + for mut next_env in iter { + let current_block = current_env.header().number(); + let next_block = next_env.header().number(); + let dist = next_block - current_block; + + let input = if dist > verifier::HISTORY_LIMIT && !current_env.spec_id.has_eip2935() + { + bail!( + "EIP-2935 required: distance between blocks \ + {current_block} and {next_block} exceeds BLOCKHASH limit" + ) + } else if dist <= verifier::EIP2935_HISTORY_LIMIT { + // Short-range: direct block commitment + let commit = current_env.commitment(); + SteelVerifier::preflight(&mut next_env) + .verify(&commit) + .await + .with_context(|| { + format!("block {current_block}: failed to verify {commit}") + })?; + current_env.into_input().await + } else { + // Long-range: intermediate EIP-2935 history commitment + let target_block = next_block - verifier::EIP2935_HISTORY_LIMIT; + // create a new env that contains all the content of the current one but uses an EIP-2935 history commitment + let builder = self + .template + .clone_with_block(current_env.header().seal()) + .commitment_block_number(target_block); + let history_env = builder.build().await.with_context(|| { + format!("block {current_block}: failed to build EIP-2935 history commitment to {target_block}") + })?.merge(current_env)?; + + let commit = history_env.commitment(); + SteelVerifier::preflight(&mut next_env) + .verify(&commit) + .await + .with_context(|| { + format!("block {current_block}: failed to verify {commit}") + })?; + history_env.into_input().await + } + .with_context(|| format!("failed to build input for block {current_block}"))?; + + inputs.push(input); + current_env = next_env; + } + + // Final environment: use the template's commitment type + let final_block = current_env.header().number(); + let final_input = self + .template + .build_input(current_env) + .await + .with_context(|| format!("failed to build input for final block {final_block}"))?; + inputs.push(final_input); + + Ok(MultiblockEvmInput(inputs)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + Account, CommitmentVersion, + ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, + host::HostMultiblockEvmEnv, + test_utils::{get_cl_url, get_el_url}, + }; + use alloy::{ + network::TransactionBuilder, + node_bindings::Anvil, + providers::{Provider, ProviderBuilder}, + }; + use alloy_consensus::BlockHeader; + use alloy_primitives::{Address, U256, address}; + use alloy_rpc_types::{BlockId, TransactionRequest}; + use revm::primitives::hardfork::SpecId; + use test_log::test; + + #[test(tokio::test)] + async fn successive_blocks() -> anyhow::Result<()> { + const N: u64 = 5; + const ADDRESS: Address = address!("0x0000000000000000000000000000000000000042"); + + let chain_spec = ChainSpec::new_single(31337, SpecId::CANCUN); + let provider = ProviderBuilder::new().connect_anvil_with_config(Anvil::cancun); + + let sender = provider.get_accounts().await?[0]; + for _ in 0..N { + let tx = TransactionRequest::default() + .with_from(sender) + .with_to(ADDRESS) + .with_value(U256::from(1)); + provider.send_transaction(tx).await?.watch().await?; + } + + let block_hash = provider + .get_block(BlockId::default()) + .await? + .unwrap() + .hash(); + + let builder = EthEvmEnv::builder() + .provider(provider) + .chain_spec(&chain_spec); + let mut host_env = HostMultiblockEvmEnv::from_builder(builder); + + for i in 1..=N { + let env = host_env.get_or_build(i as BlockNumber).await?; + Account::preflight(ADDRESS, env).info().await?; + } + + let input = host_env.into_input().await?; + + let guest_env = input.into_env(&chain_spec); + for i in 1..=N { + let info = Account::new(ADDRESS, guest_env.get(i).unwrap()).info(); + assert_eq!(info.balance, U256::from(i)); + } + + let commitment = dbg!(guest_env.into_commitment()); + assert_eq!(commitment.decode_id().1, CommitmentVersion::Block as u16); + assert_eq!(commitment.digest, block_hash); + assert_eq!(commitment.configID, chain_spec.digest()); + + Ok(()) + } + + #[test(tokio::test)] + #[cfg_attr( + any(not(feature = "rpc-tests"), no_auth), + ignore = "RPC tests are disabled" + )] + async fn eip2935_history_commitment() -> anyhow::Result<()> { + const N: u64 = 3; + // TODO(https://github.com/foundry-rs/foundry/issues/10357): Use Anvil provider + let provider = ProviderBuilder::new().connect_http(get_el_url()); + + let block_number = provider.get_block_number().await?; + let block_hash = provider + .get_block_by_number(block_number.into()) + .await? + .unwrap() + .hash(); + + let builder = EthEvmEnv::builder() + .provider(provider) + .chain_spec(Ð_MAINNET_CHAIN_SPEC) + .commitment_block_hash(block_hash); + let mut host_env = HostMultiblockEvmEnv::from_builder(builder); + + for i in 1..=N { + let env = host_env.get_or_build(block_number - i * 8192).await?; + Account::preflight(Address::ZERO, env).info().await?; + } + + let input = host_env.into_input().await?; + let commitment = dbg!(input.into_env(Ð_MAINNET_CHAIN_SPEC).into_commitment()); + assert_eq!(commitment.decode_id().1, CommitmentVersion::Block as u16); + assert_eq!(commitment.digest, block_hash); + + Ok(()) + } + + #[test(tokio::test)] + #[cfg_attr( + any(not(feature = "rpc-tests"), no_auth), + ignore = "RPC tests are disabled" + )] + async fn beacon_commitment() -> anyhow::Result<()> { + let el = ProviderBuilder::new().connect_http(get_el_url()); + + let latest = el.get_block_number().await?; + let parent_beacon_block_root = el + .get_block_by_number(latest.into()) + .await? + .unwrap() + .header + .parent_beacon_block_root() + .unwrap(); + + let builder = EthEvmEnv::builder() + .provider(el) + .chain_spec(Ð_MAINNET_CHAIN_SPEC) + .beacon_api(get_cl_url()); + let mut host_env = HostMultiblockEvmEnv::from_builder(builder); + + let env = host_env.get_or_build(latest - 1).await?; + Account::preflight(Address::ZERO, env).info().await?; + + let input = host_env.into_input().await?; + let commitment = dbg!(input.into_env(Ð_MAINNET_CHAIN_SPEC).into_commitment()); + assert_eq!(commitment.decode_id().1, CommitmentVersion::Beacon as u16); + assert_eq!(commitment.digest, parent_beacon_block_root); + + Ok(()) + } + + #[test(tokio::test)] + #[cfg_attr( + any(not(feature = "rpc-tests"), no_auth), + ignore = "RPC tests are disabled" + )] + async fn history_commitment() -> anyhow::Result<()> { + let el = ProviderBuilder::new().connect_http(get_el_url()); + + let latest = el.get_block_number().await?; + let parent_beacon_block_root = el + .get_block_by_number(latest.into()) + .await? + .unwrap() + .header + .parent_beacon_block_root() + .unwrap(); + + let builder = EthEvmEnv::builder() + .provider(el) + .chain_spec(Ð_MAINNET_CHAIN_SPEC) + .beacon_api(get_cl_url()) + .commitment_block_number(latest - 1); + let mut host_env = HostMultiblockEvmEnv::from_builder(builder); + + let env = host_env.get_or_build(latest - 2).await?; + Account::preflight(Address::ZERO, env).info().await?; + + let input = host_env.into_input().await?; + let commitment = dbg!(input.into_env(Ð_MAINNET_CHAIN_SPEC).into_commitment()); + assert_eq!(commitment.decode_id().1, CommitmentVersion::Beacon as u16); + assert_eq!(commitment.digest, parent_beacon_block_root); + + Ok(()) + } +} diff --git a/crates/steel/src/precompiles.rs b/crates/steel/src/precompiles.rs index b6e22ba3..dfd62963 100644 --- a/crates/steel/src/precompiles.rs +++ b/crates/steel/src/precompiles.rs @@ -86,7 +86,7 @@ mod host { impl<'a, N, P, F, C> BeaconRootsContract<&'a mut HostEvmEnv, F, C>> where N: Network, - P: Provider + Send + Sync + 'static, + P: Provider + 'static, F: EvmFactory, { pub fn preflight(env: &'a mut HostEvmEnv, F, C>) -> Self { @@ -103,7 +103,7 @@ mod host { impl<'a, N, P, F, C> HistoryStorageContract<&'a mut HostEvmEnv, F, C>> where N: Network, - P: Provider + Send + Sync + 'static, + P: Provider + 'static, F: EvmFactory, { /// Creates a new `HistoryStorageContract` instance for use on the host for preflighting. diff --git a/crates/steel/src/verifier.rs b/crates/steel/src/verifier.rs index 11abdc74..b6d67a72 100644 --- a/crates/steel/src/verifier.rs +++ b/crates/steel/src/verifier.rs @@ -17,7 +17,12 @@ use crate::{ precompiles::{BeaconRootsContract, HistoryStorageContract}, }; use alloy_primitives::{B256, BlockNumber, U256}; -use anyhow::ensure; +use anyhow::{bail, ensure}; + +/// Number of block hashes the verifier can access via the BLOCKHASH opcode. +pub const HISTORY_LIMIT: u64 = revm::primitives::BLOCK_HASH_HISTORY; +/// Number of block hashes the verifier can access via the EIP2935 history storage contract. +pub const EIP2935_HISTORY_LIMIT: u64 = alloy_eips::eip2935::HISTORY_SERVE_WINDOW as u64; /// Represents a verifier for validating Steel commitments within Steel. /// @@ -84,12 +89,20 @@ impl<'a, F: EvmFactory> SteelVerifier<&'a GuestEvmEnv> { let (id, version_code) = commitment.decode_id(); match CommitmentVersion::n(version_code) { Some(CommitmentVersion::Block) => { + let header = self.env.header().inner(); + let block_number = validate_block_number(id, header).expect("Invalid block number"); + // use the header field for a direct parent + let block_hash = if block_number + 1 == header.number() { + *header.parent_hash() + } // use history storage contract when EIP-2935 was activated - let block_hash = if self.env.spec_id.has_eip2935() { + else if self.env.spec_id.has_eip2935() { // history storage contract reverts when `id` id not in allowed history window HistoryStorageContract::new(self.env).call(id) - } else { - let block_number = validate_history_window(self.env.header().inner(), id, 256) + } + // otherwise emulate the BLOCKHASH opcode + else { + validate_history_window(header, block_number, HISTORY_LIMIT) .expect("Invalid block number"); self.env.db().block_hash(block_number) }; @@ -113,7 +126,6 @@ mod host { use super::*; use crate::host::{HostEvmEnv, db::ProviderDb}; use alloy::providers::{Network, Provider}; - use alloy_eips::eip2935; use anyhow::Context; use revm::Database; @@ -121,7 +133,7 @@ mod host { where F: EvmFactory, N: Network, - P: Provider + Send + Sync + 'static, + P: Provider + 'static, { /// Constructor for preflighting Steel commitment verifications on the host. /// @@ -158,12 +170,16 @@ mod host { match CommitmentVersion::n(version_code) { Some(CommitmentVersion::Block) => { let header = self.env.header().inner(); - let block_hash = if self.env.spec_id.has_eip2935() { - validate_history_window(header, id, eip2935::HISTORY_SERVE_WINDOW as u64) + let block_number = + validate_block_number(id, header).context("invalid block number")?; + let block_hash = if block_number + 1 == header.number() { + *header.parent_hash() + } else if self.env.spec_id.has_eip2935() { + validate_history_window(header, block_number, EIP2935_HISTORY_LIMIT) .context("invalid block number")?; HistoryStorageContract::preflight(self.env).call(id).await? } else { - let block_number = validate_history_window(header, id, 256) + validate_history_window(header, block_number, HISTORY_LIMIT) .context("invalid block number")?; self.env .spawn_with_db(move |db| db.block_hash(block_number)) @@ -189,18 +205,23 @@ mod host { } } +fn validate_block_number(n: U256, header: &impl EvmBlockHeader) -> anyhow::Result { + match BlockNumber::try_from(n) { + Ok(n) if n < header.number() => Ok(n), + _ => bail!("not an ancestor"), + } +} + fn validate_history_window( header: &impl EvmBlockHeader, - block_number: U256, - windows: u64, -) -> anyhow::Result { - let block_number: BlockNumber = block_number.saturating_to(); + number: BlockNumber, + window: u64, +) -> anyhow::Result<()> { ensure!( - block_number < header.number() && header.number() - block_number <= windows, - "only valid for the {windows} most recent blocks, excluding the current one" + number < header.number() && header.number() - number <= window, + "only valid for the {window} most recent blocks" ); - - Ok(block_number) + Ok(()) } #[cfg(test)] @@ -264,21 +285,23 @@ mod tests { ignore = "RPC tests are disabled" )] async fn eip2935_verify_block_commitment() { - // TODO: Make this an Anvil provider, once Anvil has EIP-2935 support + // TODO(https://github.com/foundry-rs/foundry/issues/10357): Use Anvil provider let el = ProviderBuilder::new().connect_http(get_el_url()); verify_block_commitment(el.clone(), Ð_MAINNET_CHAIN_SPEC, 1).await; - verify_block_commitment(el.clone(), Ð_MAINNET_CHAIN_SPEC, 8191).await; + verify_block_commitment(el.clone(), Ð_MAINNET_CHAIN_SPEC, 2).await; + verify_block_commitment(el.clone(), Ð_MAINNET_CHAIN_SPEC, EIP2935_HISTORY_LIMIT).await; } #[test(tokio::test)] async fn pre_eip2935_verify_block_commitment() { let chain_spec = ChainSpec::new_single(31337, SpecId::CANCUN); let el = ProviderBuilder::new().connect_anvil_with_config(|conf| conf.cancun()); - el.anvil_mine(Some(256), None).await.unwrap(); + el.anvil_mine(Some(HISTORY_LIMIT), None).await.unwrap(); verify_block_commitment(el.clone(), &chain_spec, 1).await; - verify_block_commitment(el.clone(), &chain_spec, 256).await; + verify_block_commitment(el.clone(), &chain_spec, 2).await; + verify_block_commitment(el.clone(), &chain_spec, HISTORY_LIMIT).await; } #[test(tokio::test)] diff --git a/crates/steel/tests/steel.rs b/crates/steel/tests/steel.rs index 38534454..443ce11f 100644 --- a/crates/steel/tests/steel.rs +++ b/crates/steel/tests/steel.rs @@ -573,23 +573,6 @@ async fn out_of_gas() { ); } -#[test(tokio::test)] -async fn no_preflight() { - let env = EthEvmEnv::builder() - .provider(test_provider().await) - .chain_spec(&STEEL_TEST_PRAGUE_CHAIN_SPEC) - .build() - .await - .unwrap(); - match env.into_input().await { - Ok(_) => panic!("calling into_input without a preflight should fail"), - Err(err) => assert_eq!( - err.to_string(), - "no accounts accessed: use Contract::preflight" - ), - } -} - alloy::sol!( // docker run -i ethereum/solc:0.8.26 - --optimize --bin #[sol(rpc, bytecode="60a0604052348015600e575f80fd5b5060405161012a38038061012a833981016040819052602b91604b565b60808190525f5b6080518110156045576001808255016032565b50506061565b5f60208284031215605a575f80fd5b5051919050565b60805160b46100765f395f6047015260b45ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c8063380eb4e014602a575b5f80fd5b60306042565b60405190815260200160405180910390f35b5f805b7f0000000000000000000000000000000000000000000000000000000000000000811015607a57805491909101906001016045565b509056fea26469706673582212203687b75eefdd9cc7ceedb243aa360bd9e1b4cab1930149a371efef74ce18bdf164736f6c634300081a0033")] diff --git a/examples/token-stats/.vscode/settings.json b/examples/token-stats/.vscode/settings.json deleted file mode 100644 index c2224e6a..00000000 --- a/examples/token-stats/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rust-analyzer.linkedProjects": [ - "./methods/guest/Cargo.toml", - "./host/Cargo.toml", - "./core/Cargo.toml" - ] -} \ No newline at end of file diff --git a/examples/token-stats/README.md b/examples/token-stats/README.md index 8627791f..87157262 100644 --- a/examples/token-stats/README.md +++ b/examples/token-stats/README.md @@ -15,24 +15,30 @@ You'll also need access to an Ethereum Mainnet RPC endpoint. You can for example To run the example, which computes the current APR of the Compound USDC Token [`0xc3d688B66703497DAA19211EEdff47f25384cdc3`](https://etherscan.io/token/0xc3d688B66703497DAA19211EEdff47f25384cdc3) on Ethereum, execute the following command: ```bash -RPC_URL=https://ethereum-rpc.publicnode.com RUST_LOG=info cargo run --release +RPC_URL=https://ethereum-rpc.publicnode.com RUST_LOG=info,risc0_steel=debug cargo run --release ``` The output should resemble the following: ```text -2024-08-05T17:58:28.709271Z INFO risc0_steel::host: Environment initialized for block 20464007 -2024-08-05T17:58:28.709406Z INFO risc0_steel: Commitment to block 0xba19f4d5d1aabd1e4ddca7263f1307cfdec1252041395edfc1d8507eaf142cf8 -2024-08-05T17:58:28.709502Z INFO risc0_steel::contract: Executing preflight calling 'getUtilization()' on 0xc3d688B66703497DAA19211EEdff47f25384cdc3 -Call getUtilization() Function on 0xc3d6…cdc3 returns: 715303307067353898 -2024-08-05T17:58:29.974428Z INFO risc0_steel::contract: Executing preflight calling 'getSupplyRate(uint256)' on 0xc3d688B66703497DAA19211EEdff47f25384cdc3 -Call getSupplyRate(uint256) Function on 0xc3d6…cdc3 returns: 1179470191 +2025-10-07T11:35:38.730992Z DEBUG risc0_steel::host::builder: Environment initialized with block 23518416 (0xf3b179a5030338d6d3b6477843ac14027c7e6f25ab4c8d42e61c2fbec616d598) +2025-10-07T11:35:38.731380Z DEBUG risc0_steel::contract::host: Executing preflight calling 'getUtilization()' +Call getUtilization() Function on 0xc3d6…cdc3 returns: 901643987632970446 +2025-10-07T11:35:39.566357Z DEBUG risc0_steel::contract::host: Executing preflight calling 'getSupplyRate(uint256)' +Call getSupplyRate(uint256) Function on 0xc3d6…cdc3 returns: 1194006355 +2025-10-07T11:35:40.350225Z DEBUG risc0_steel::host::builder: Environment initialized with block 23525616 (0x3728e8f5dcf4d42b68d5cd424c9c7fcb0582d552f14129f94411aab8e7527746) +2025-10-07T11:35:40.350296Z DEBUG risc0_steel::contract::host: Executing preflight calling 'getUtilization()' +Call getUtilization() Function on 0xc3d6…cdc3 returns: 809451025252486797 +2025-10-07T11:35:41.266703Z DEBUG risc0_steel::contract::host: Executing preflight calling 'getSupplyRate(uint256)' +Call getSupplyRate(uint256) Function on 0xc3d6…cdc3 returns: 924030850 +2025-10-07T11:35:41.937053Z DEBUG risc0_steel::verifier::host: Executing preflight verifying Commitment { version: "Block", id: 23518416, digest: 0xf3b179a5030338d6d3b6477843ac14027c7e6f25ab4c8d42e61c2fbec616d598, configID: 0x9a223c7ca04c969f1cacbe5b8db44c308b2c53390505d3d48c834ed4469fc839 } +2025-10-07T11:35:41.937113Z DEBUG risc0_steel::contract::host: Executing preflight calling 'raw' +2025-10-07T11:35:43.367732Z DEBUG risc0_steel::block::host: Preparing input for block 23518416: +2025-10-07T11:35:44.295123Z DEBUG risc0_steel::block::host: Preparing input for block 23525616: Running the guest with the constructed input: -2024-08-05T17:58:31.587359Z INFO executor: risc0_zkvm::host::server::exec::executor: execution time: 196.721584ms -2024-08-05T17:58:31.587385Z INFO executor: risc0_zkvm::host::server::session: number of segments: 11 -2024-08-05T17:58:31.587388Z INFO executor: risc0_zkvm::host::server::session: total cycles: 11534336 -2024-08-05T17:58:31.587390Z INFO executor: risc0_zkvm::host::server::session: user cycles: 8885255 -Proven APR calculated is: 3.7195771943376% +2025-10-07T11:35:44.777284Z INFO risc0_zkvm::host::server::exec::executor: execution time: 79.718292ms +Commitment { version: "Block", id: 23525616, digest: 0x3728e8f5dcf4d42b68d5cd424c9c7fcb0582d552f14129f94411aab8e7527746, configID: 0x9a223c7ca04c969f1cacbe5b8db44c308b2c53390505d3d48c834ed4469fc839 } +Proven APR over 2 days is: 3.339721064844 ``` [install-rust]: https://doc.rust-lang.org/cargo/getting-started/installation.html diff --git a/examples/token-stats/core/src/lib.rs b/examples/token-stats/core/src/lib.rs index f81f965f..51692c06 100644 --- a/examples/token-stats/core/src/lib.rs +++ b/examples/token-stats/core/src/lib.rs @@ -30,6 +30,8 @@ sol! { sol! { struct APRCommitment { Commitment commitment; + uint64 days; + uint64 finalBlockNumber; uint64 annualSupplyRate; } } diff --git a/examples/token-stats/host/src/main.rs b/examples/token-stats/host/src/main.rs index 204f798d..f0b0199e 100644 --- a/examples/token-stats/host/src/main.rs +++ b/examples/token-stats/host/src/main.rs @@ -16,9 +16,10 @@ use alloy_sol_types::{SolCall, SolValue}; use anyhow::{Context, Result}; use clap::Parser; use risc0_steel::{ - Contract, SteelVerifier, + Contract, alloy::providers::{Provider, ProviderBuilder}, ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, + host::HostMultiblockEvmEnv, }; use risc0_zkvm::{ExecutorEnv, default_executor}; use token_stats_core::{APRCommitment, CONTRACT, CometMainInterface}; @@ -32,10 +33,6 @@ struct Args { /// URL of the RPC endpoint #[arg(long, env = "RPC_URL")] rpc_url: Url, - - /// Beacon API endpoint URL - #[arg(long, env = "BEACON_API_URL")] - beacon_api_url: Url, } #[tokio::main] @@ -48,90 +45,49 @@ async fn main() -> Result<()> { let args = Args::parse(); // Query the latest block number. - let provider = ProviderBuilder::default().connect_http(args.rpc_url); + let provider = ProviderBuilder::new().connect_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) - .chain_spec(Ð_MAINNET_CHAIN_SPEC) - .build() - .await?; - - // Preflight the call to prepare the input that is required to execute the function in - // the guest without RPC access. It also returns the result of the call. - let mut contract = Contract::preflight(CONTRACT, &mut env); - let utilization = contract - .call_builder(&CometMainInterface::getUtilizationCall {}) - .call() - .await?; - println!( - "Call {} Function on {:#} returns: {}", - CometMainInterface::getUtilizationCall::SIGNATURE, - CONTRACT, - utilization - ); - let rate = contract - .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) - .call() - .await?; - println!( - "Call {} Function on {:#} returns: {}", - CometMainInterface::getSupplyRateCall::SIGNATURE, - CONTRACT, - rate - ); - - // 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. - let mut env = EthEvmEnv::builder() + // Create a multiblock environment for the network. + let builder = EthEvmEnv::builder() .provider(provider) - .chain_spec(Ð_MAINNET_CHAIN_SPEC) - .build() - .await?; - - // Preflight the verification of the commitment of the previous input. - SteelVerifier::preflight(&mut env) - .verify(&commitment_input1) - .await?; - - // Preflight the actual contract calls. - let mut contract = Contract::preflight(CONTRACT, &mut env); - let utilization = contract - .call_builder(&CometMainInterface::getUtilizationCall {}) - .call() - .await?; - println!( - "Call {} Function on {:#} returns: {}", - CometMainInterface::getUtilizationCall::SIGNATURE, - CONTRACT, - utilization - ); - let rate = contract - .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) - .call() - .await?; - println!( - "Call {} Function on {:#} returns: {}", - CometMainInterface::getSupplyRateCall::SIGNATURE, - CONTRACT, - rate - ); + .chain_spec(Ð_MAINNET_CHAIN_SPEC); + let mut envs = HostMultiblockEvmEnv::from_builder(builder); + + // Execute the call for the latest block and about 24h (7200 blocks) ago. + for block in [latest - 7200, latest] { + // Preflight the call to prepare the input that is required to execute the function in + // the guest without RPC access. It also returns the result of the call. + let mut contract = Contract::preflight(CONTRACT, envs.get_or_build(block).await?); + let utilization = contract + .call_builder(&CometMainInterface::getUtilizationCall {}) + .call() + .await?; + println!( + "Call {} Function on {:#} returns: {}", + CometMainInterface::getUtilizationCall::SIGNATURE, + CONTRACT, + utilization + ); + let rate = contract + .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) + .call() + .await?; + println!( + "Call {} Function on {:#} returns: {}", + CometMainInterface::getSupplyRateCall::SIGNATURE, + CONTRACT, + rate + ); + } // Finally, construct the second input from the environment representing the latest state. - let input2 = env.into_input().await?; + let input = envs.into_input().await?; println!("Running the guest with the constructed input:"); let session_info = { let env = ExecutorEnv::builder() - .write(&input1) - .unwrap() - .write(&input2) + .write(&input) .unwrap() .build() .context("failed to build executor env")?; @@ -147,7 +103,7 @@ async fn main() -> Result<()> { // Calculation is handling `/ 10^18 * 100` to match precision for a percentage. let apr = apr_commit.annualSupplyRate as f64 / 10f64.powi(16); - println!("Proven APR calculated is: {apr}%"); + println!("Proven APR over {} days is: {apr}%", apr_commit.days); Ok(()) } diff --git a/examples/token-stats/methods/guest/src/main.rs b/examples/token-stats/methods/guest/src/main.rs index dbe06b99..5eac8f23 100644 --- a/examples/token-stats/methods/guest/src/main.rs +++ b/examples/token-stats/methods/guest/src/main.rs @@ -14,60 +14,57 @@ use alloy_sol_types::SolValue; use risc0_steel::{ - Contract, SteelVerifier, - ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmInput}, + Contract, + ethereum::{ETH_MAINNET_CHAIN_SPEC, EthMultiblockEvmInput}, }; use risc0_zkvm::guest::env; use token_stats_core::{APRCommitment, CONTRACT, CometMainInterface}; -const SECONDS_PER_YEAR: u64 = 60 * 60 * 24 * 365; +const SECONDS_PER_YEAR: u128 = 60 * 60 * 24 * 365; fn main() { // Read the first input from the guest environment. It corresponds to the older EVM state. - let input: EthEvmInput = env::read(); + let input: EthMultiblockEvmInput = env::read(); // Converts the input into a `EvmEnv` for execution. - let env_prev = input.into_env(Ð_MAINNET_CHAIN_SPEC); + let envs = input.into_env(Ð_MAINNET_CHAIN_SPEC); - // Execute the view calls on the older EVM state. - let contract = Contract::new(CONTRACT, &env_prev); - let utilization = contract - .call_builder(&CometMainInterface::getUtilizationCall {}) - .call(); - let supply_rate_prev = contract - .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) - .call(); + // Check that the EVM states are exactly 7200 blocks apart. + let numbers: Vec<_> = envs.block_numbers().collect(); + for window in numbers.windows(2) { + assert_eq!(window[1] - window[0], 7200); + } - // Prepare the second `EvmEnv` for execution. It corresponds to the recent EVM state. - let input: EthEvmInput = env::read(); - let env_cur = input.into_env(Ð_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(); - let supply_rate_cur = contract - .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) - .call(); + // Execute the view calls on each EVM state. + let rates = envs + .iter() + .map(|env| { + // Execute the view calls on the older EVM state. + let contract = Contract::new(CONTRACT, env); + let utilization = contract + .call_builder(&CometMainInterface::getUtilizationCall {}) + .call(); + contract + .call_builder(&CometMainInterface::getSupplyRateCall { utilization }) + .call() + }) + .collect::>(); // The formula for APR in percentage is the following: // Seconds Per Year = 60 * 60 * 24 * 365 // Utilization = getUtilization() // Supply Rate = getSupplyRate(Utilization) // Supply APR = Supply Rate / (10 ^ 18) * Seconds Per Year * 100 - // - // 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; + let annual_supply_rate_u128 = + rates.iter().map(|&r| r as u128).sum::() * SECONDS_PER_YEAR / rates.len() as u128; + let annual_supply_rate = u64::try_from(annual_supply_rate_u128).unwrap(); // This commits the APR at current utilization rate for this given block. let journal = APRCommitment { - commitment: env_cur.into_commitment(), + days: (rates.len() - 1) as u64, + finalBlockNumber: envs.last().header().number, annualSupplyRate: annual_supply_rate, + commitment: envs.into_commitment(), }; env::commit_slice(&journal.abi_encode()); }