diff --git a/Cargo.toml b/Cargo.toml index dfe3888d..1e75b3de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,17 +24,16 @@ risc0-zkvm = { git = "https://github.com/risc0/risc0", branch = "main", default- risc0-binfmt = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } # Alloy guest dependencies -alloy-consensus = { version = "0.8" } +alloy-consensus = { version = "0.9" } alloy-rlp = { version = "0.3.8" } -alloy-primitives = { version = "0.8.8" } -alloy-sol-types = { version = "0.8.8" } +alloy-primitives = { version = "0.8.16" } +alloy-sol-types = { version = "0.8.16" } # OP Steel -op-alloy-network = { version = "0.8" } +op-alloy-network = { version = "0.9" } # Alloy host dependencies -alloy = { version = "0.8" } -# Note: semver breaking change in 0.7.7, be careful if switching to lower patch version +alloy = { version = "0.9" } alloy-trie = { version = "0.7.7" } # Beacon chain support @@ -45,7 +44,7 @@ anyhow = { version = "1.0" } bincode = { version = "1.3" } clap = { version = "4.5", features = ["derive", "env"] } log = "0.4" -revm = { version = "18.0", default-features = false, features = ["std"] } +revm = { version = "19.2", default-features = false, features = ["std"] } reqwest = "0.12" serde = "1.0" serde_json = "1.0" diff --git a/build/src/lib.rs b/build/src/lib.rs index f9a2e204..7e2ea14a 100644 --- a/build/src/lib.rs +++ b/build/src/lib.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. @@ -21,7 +21,6 @@ use std::{ use anyhow::{anyhow, bail, Context, Result}; use risc0_build::GuestListEntry; -use risc0_zkp::core::digest::Digest; const SOL_HEADER: &str = r#"// Copyright 2024 RISC Zero, Inc. // @@ -109,7 +108,7 @@ pub fn generate_image_id_sol(guests: &[GuestListEntry]) -> Result> { .iter() .map(|guest| { let name = guest.name.to_uppercase().replace('-', "_"); - let image_id = hex::encode(Digest::from(guest.image_id)); + let image_id = hex::encode(guest.image_id); format!("bytes32 public constant {name}_ID = bytes32(0x{image_id});") }) .collect(); diff --git a/examples/erc20-counter/Cargo.toml b/examples/erc20-counter/Cargo.toml index e0913bd4..a65688b5 100644 --- a/examples/erc20-counter/Cargo.toml +++ b/examples/erc20-counter/Cargo.toml @@ -18,7 +18,7 @@ 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.8", features = ["full"] } +alloy = { version = "0.9", 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..b45fefa9 100644 --- a/examples/erc20-counter/apps/src/bin/publisher.rs +++ b/examples/erc20-counter/apps/src/bin/publisher.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. @@ -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/governance/apps/Cargo.toml b/examples/governance/apps/Cargo.toml index 5cc8dea7..15cb2a28 100644 --- a/examples/governance/apps/Cargo.toml +++ b/examples/governance/apps/Cargo.toml @@ -4,7 +4,7 @@ version = { workspace = true } edition = { workspace = true } [dependencies] -alloy = { version = "0.8", features = ["full"] } +alloy = { version = "0.9", features = ["full"] } alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } anyhow = { workspace = true } diff --git a/examples/op/Cargo.toml b/examples/op/Cargo.toml index 00f3999a..9df376a3 100644 --- a/examples/op/Cargo.toml +++ b/examples/op/Cargo.toml @@ -13,4 +13,4 @@ risc0-op-steel = { path = "../../op-steel" } risc0-steel = { path = "../../steel" } risc0-ethereum-contracts = { path = "../../contracts" } examples-common = { path = "common" } -alloy = { version = "0.8" } +alloy = "0.9" diff --git a/examples/token-stats/Cargo.toml b/examples/token-stats/Cargo.toml index 38379935..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" } @@ -21,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/src/main.rs b/examples/token-stats/host/src/main.rs index eed3f774..fe76dd68 100644 --- a/examples/token-stats/host/src/main.rs +++ b/examples/token-stats/host/src/main.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. @@ -16,8 +16,9 @@ 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, + Contract, SteelVerifier, }; use risc0_zkvm::{default_executor, ExecutorEnv}; use token_stats_core::{APRCommitment, CometMainInterface, CONTRACT}; @@ -25,13 +26,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 +47,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 +87,53 @@ async fn main() -> Result<()> { rate ); - // Finally, construct the input from the environment. - let input = env.into_input().await?; + // 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().provider(provider).build().await?; + env = env.with_chain_spec(Ð_MAINNET_CHAIN_SPEC); + + // 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? + ._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 representing the latest state. + 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/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/examples/token-stats/methods/guest/src/main.rs b/examples/token-stats/methods/guest/src/main.rs index 86403326..6808a7a5 100644 --- a/examples/token-stats/methods/guest/src/main.rs +++ b/examples/token-stats/methods/guest/src/main.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. @@ -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()); diff --git a/op-steel/src/optimism/mod.rs b/op-steel/src/optimism/mod.rs index 22e4a4ff..5550c1ed 100644 --- a/op-steel/src/optimism/mod.rs +++ b/op-steel/src/optimism/mod.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. @@ -112,7 +112,7 @@ impl EvmBlockHeader for OpBlockHeader { // technically, this is only valid after EIP-4399 but revm makes sure it is not used before blk_env.prevrandao = Some(header.mix_hash); if let Some(excess_blob_gas) = header.excess_blob_gas { - blk_env.set_blob_excess_gas_and_price(excess_blob_gas) + blk_env.set_blob_excess_gas_and_price(excess_blob_gas, false) }; } } 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 diff --git a/steel/Cargo.toml b/steel/Cargo.toml index 466e79c1..2bf3181c 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 } @@ -51,3 +51,4 @@ host = [ "dep:url", ] unstable-history = [] +unstable-verifier = [] diff --git a/steel/docs/create-steel-app/README.md b/steel/docs/create-steel-app/README.md index 78a01aa6..57233c8a 100644 --- a/steel/docs/create-steel-app/README.md +++ b/steel/docs/create-steel-app/README.md @@ -12,14 +12,14 @@ Make sure to have the following installed: 2. [Foundry] 3. [cargo-risczero] +N.B. Please make sure to run `foundryup` to update to the latest Foundry version before continuing. + ## Usage ```sh sh <(curl -fsSL https://raw.githubusercontent.com/risc0/risc0-ethereum/refs/heads/main/steel/docs/create-steel-app/create-steel-app) ``` -The script will automatically detect your current `cargo-risczero` version and use that for the corresponding version of the `erc20-counter` example. You also have the manual choice between two release versions: [1.0] and [1.1]. - Once the script is finished running, you should: ```console diff --git a/steel/docs/create-steel-app/create-steel-app b/steel/docs/create-steel-app/create-steel-app index 211b3332..8ef20c09 100755 --- a/steel/docs/create-steel-app/create-steel-app +++ b/steel/docs/create-steel-app/create-steel-app @@ -26,10 +26,10 @@ get_risc0_version() { echo "detected risc0 version: ${RISC0_VERSION:?}" # map version to branch - if [[ "${RISC0_VERSION:?}" =~ ^1\.1\. ]]; then - BRANCH="release-1.1" + if [[ "${RISC0_VERSION:?}" =~ ^1\.2\. ]]; then + BRANCH="release-1.2" else - echo "unsupported risc0 version. version 1.1 is supported" + echo "unsupported risc0 version. version 1.2 is supported" BRANCH="" fi } @@ -37,9 +37,9 @@ get_risc0_version() { select_branch() { get_risc0_version - # NOTE: Only 1.1 is currently supported. + # NOTE: Only 1.2 is currently supported. - BRANCH="release-1.1" + BRANCH="release-1.2" echo "selected branch: ${BRANCH:?}" # Logic below can be used when more than one version is supported. diff --git a/steel/src/block.rs b/steel/src/block.rs index ffde7fc7..3e9fe16d 100644 --- a/steel/src/block.rs +++ b/steel/src/block.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. @@ -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/contract.rs b/steel/src/contract.rs index 945e7599..72ec9ef1 100644 --- a/steel/src/contract.rs +++ b/steel/src/contract.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. @@ -273,7 +273,7 @@ mod host { } } -impl<'a, S, H> CallBuilder> +impl CallBuilder> where S: SolCall, H: EvmBlockHeader, diff --git a/steel/src/ethereum.rs b/steel/src/ethereum.rs index 496dde6a..7a408395 100644 --- a/steel/src/ethereum.rs +++ b/steel/src/ethereum.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. @@ -93,7 +93,7 @@ impl EvmBlockHeader for EthBlockHeader { // technically, this is only valid after EIP-4399 but revm makes sure it is not used before blk_env.prevrandao = Some(header.mix_hash); if let Some(excess_blob_gas) = header.excess_blob_gas { - blk_env.set_blob_excess_gas_and_price(excess_blob_gas) + blk_env.set_blob_excess_gas_and_price(excess_blob_gas, false); }; } } diff --git a/steel/src/history/beacon_roots.rs b/steel/src/history/beacon_roots.rs index 38452760..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. @@ -14,10 +14,28 @@ use crate::{MerkleTrie, StateAccount}; use alloy_primitives::{address, b256, keccak256, uint, Address, B256, U256}; +use revm::{ + primitives::{AccountInfo, Bytecode}, + Database, +}; use serde::{Deserialize, Serialize}; +use std::convert::Infallible; + +/// 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)] +#[non_exhaustive] pub enum Error { /// Error indicating that the contract is not deployed at the expected address. #[error("wrong or no contract deployed")] @@ -31,118 +49,52 @@ pub enum Error { /// Error indicating that the contract execution was reverted. #[error("execution reverted")] Reverted, + /// Unspecified error. + #[error(transparent)] + Other(#[from] anyhow::Error), } -/// The `State` struct represents the state of the contract. +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 { + anyhow::Error::new(value).into() + } +} + +/// A simplified MPT-based read-only EVM database implementation only containing the state of the +/// beacon roots contract. #[derive(Clone, Serialize, Deserialize)] -pub struct State { - /// EVM (global) state trie with path to the contract account. +pub struct BeaconRootsState { + /// 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, } -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). + /// Prepares the [BeaconRootsState] by retrieving the beacon root from an RPC provider and + /// constructing the necessary proofs. /// - /// 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. - /// - /// 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, 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 +103,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,16 +122,115 @@ 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)), - err => Err(err).context("API returned invalid state"), + err => Err(err).context("RPC error"), }, } } } +/// 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: 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 { + // should never be called. + unimplemented!() + } + + #[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 + .get_rlp(keccak256(index.to_be_bytes::<32>()))? + .unwrap_or_default()) + } + + fn block_hash(&mut self, _number: u64) -> Result { + // should never be called. + unimplemented!() + } +} + +/// 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` 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 + 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 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) + } +} + #[cfg(test)] mod tests { use super::*; @@ -207,8 +258,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 +268,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 2ac96c12..73a064ff 100644 --- a/steel/src/history/mod.rs +++ b/steel/src/history/mod.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. @@ -18,9 +18,10 @@ 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}; -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 +43,7 @@ pub struct HistoryCommit { #[derive(Clone, Serialize, Deserialize)] struct StateCommit { /// State for verifying `evm_commit`. - state: beacon_roots::State, + state: BeaconRootsState, /// Commitment for `state` to a Beacon Chain block root. state_commit: GeneralizedBeaconCommit, } @@ -60,11 +61,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"); @@ -89,6 +90,7 @@ mod host { use crate::{ beacon::host::{client::BeaconClient, create_beacon_commit}, ethereum::EthBlockHeader, + history::beacon_roots::{BeaconRootsState, HISTORY_BUFFER_LENGTH}, }; use alloy::{ network::{primitives::BlockTransactionsKind, Ethereum}, @@ -132,7 +134,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; @@ -158,7 +160,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(), @@ -220,7 +222,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(); @@ -228,22 +230,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..f7aae3fc 100644 --- a/steel/src/host/db/mod.rs +++ b/steel/src/host/db/mod.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. @@ -15,7 +15,7 @@ //! [Database] implementations. //! //! [Database]: revm::Database -mod alloy; +pub(crate) mod alloy; mod proof; mod provider; diff --git a/steel/src/host/db/proof.rs b/steel/src/host/db/proof.rs index 745a64f9..3b597ba2 100644 --- a/steel/src/host/db/proof.rs +++ b/steel/src/host/db/proof.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. @@ -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/host/mod.rs b/steel/src/host/mod.rs index b3761065..338976b5 100644 --- a/steel/src/host/mod.rs +++ b/steel/src/host/mod.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. @@ -13,6 +13,10 @@ // limitations under the License. //! Functionality that is only needed for the host and not the guest. +use std::{ + fmt::{self, Debug, Display}, + str::FromStr, +}; use crate::{ beacon::BeaconCommit, @@ -21,10 +25,10 @@ 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::{ + eips::eip1898::{HexStringMissingPrefixError, ParseBlockNumberError}, network::{Ethereum, Network}, providers::Provider, rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag, @@ -32,10 +36,7 @@ use alloy::{ }; use alloy_primitives::B256; use anyhow::{ensure, Result}; -use core::fmt; use db::{AlloyDb, ProofDb}; -use std::fmt::Display; -use std::str::FromStr; use url::Url; mod builder; @@ -130,6 +131,21 @@ pub struct HostCommit { config_id: B256, } +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, @@ -146,18 +162,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) } } diff --git a/steel/src/lib.rs b/steel/src/lib.rs index 9335ef25..196e553f 100644 --- a/steel/src/lib.rs +++ b/steel/src/lib.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. @@ -44,6 +44,8 @@ mod merkle; mod mpt; pub mod serde; mod state; +#[cfg(feature = "unstable-verifier")] +mod verifier; pub use beacon::BeaconInput; pub use block::BlockInput; @@ -55,6 +57,8 @@ pub use state::{StateAccount, StateDb}; pub use history::HistoryInput; #[cfg(not(feature = "unstable-history"))] pub(crate) use history::HistoryInput; +#[cfg(feature = "unstable-verifier")] +pub use verifier::SteelVerifier; /// The serializable input to derive and validate an [EvmEnv] from. #[non_exhaustive] @@ -150,13 +154,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() } @@ -285,7 +289,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/mpt.rs b/steel/src/mpt.rs index bc6e8ad4..cd80474e 100644 --- a/steel/src/mpt.rs +++ b/steel/src/mpt.rs @@ -16,8 +16,7 @@ use std::fmt::Debug; use alloy_primitives::{b256, keccak256, map::B256HashMap, B256}; use alloy_rlp::{BufMut, Decodable, Encodable, Header, PayloadView, EMPTY_STRING_CODE}; -use alloy_trie::nodes::encode_path_leaf; -use alloy_trie::Nibbles; +use alloy_trie::{nodes::encode_path_leaf, nybbles::Nibbles}; use serde::{Deserialize, Serialize}; /// Root hash of an empty Merkle Patricia trie, i.e. `keccak256(RLP(""))`. @@ -98,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/state.rs b/steel/src/state.rs index 9ca4d041..79853886 100644 --- a/steel/src/state.rs +++ b/steel/src/state.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. @@ -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..42bfb907 --- /dev/null +++ b/steel/src/verifier.rs @@ -0,0 +1,234 @@ +// 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. + +use crate::{ + history::beacon_roots::BeaconRootsContract, state::WrapStateDb, Commitment, EvmBlockHeader, + GuestEvmEnv, +}; +use alloy_primitives::U256; +use anyhow::{ensure, Context}; + +/// Represents a verifier for validating Steel commitments within Steel. +#[stability::unstable(feature = "verifier")] +pub struct SteelVerifier { + env: E, +} + +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 { + 0 => { + 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) + .expect("calling BeaconRootsContract failed"); + assert_eq!(beacon_root, commitment.digest, "Invalid digest"); + } + v => unimplemented!("Invalid commitment version {}", v), + } + } +} + +#[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; + + 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); + + let (id, version) = commitment.decode_id(); + match version { + 0 => { + 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 = 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(()) + } + v => unimplemented!("Invalid commitment version {}", v), + } + } + } + + 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 { + 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, + "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, 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"; + + #[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) + .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, + ); + + // preflight the verifier + let mut env = EthEvmEnv::builder().provider(el).build().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(); + SteelVerifier::new(&env).verify(&commit); + } + + #[test(tokio::test)] + #[ignore = "queries actual RPC nodes"] + 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, BlockTransactionsKind::Hashes) + .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(); + 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(); + SteelVerifier::new(&env).verify(&commit); + } +}