Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
749a1d6
add multiblock env
Wollac Oct 3, 2025
651c2a4
fix doc
Wollac Oct 3, 2025
734f7bc
explain reason for allow(private_bounds)
Wollac Oct 3, 2025
db651c4
update CHANGELOG.md
Wollac Oct 3, 2025
2a4e88b
minor improvements
Wollac Oct 3, 2025
50df33e
use MultiblockEvmEnv
Wollac Oct 3, 2025
a6a8717
shorten log
Wollac Oct 3, 2025
03587f0
fix clippy warning
Wollac Oct 3, 2025
5b6844e
export history consts
Wollac Oct 3, 2025
a145f69
Merge branch 'main' into feat/multiblock
Wollac Oct 3, 2025
018b819
add tests for all commitments
Wollac Oct 3, 2025
64c678f
add len
Wollac Oct 3, 2025
304dbbb
add is_empty
Wollac Oct 5, 2025
108e1e4
Update crates/steel/src/multiblock.rs
Wollac Oct 5, 2025
19cc995
nit
Wollac Oct 5, 2025
44bb11a
improve token-stats example
Wollac Oct 7, 2025
d72fd51
cleanups
Wollac Oct 7, 2025
6cc201f
fix license header
Wollac Oct 7, 2025
281d147
add block number
Wollac Oct 7, 2025
0930d42
simplify input builder
Wollac Oct 27, 2025
d169719
add STEEL_TEST_CANCUN_CHAIN_SPEC
Wollac Oct 27, 2025
dc22bcf
add STEEL_TEST_CANCUN_CHAIN_SPEC
Wollac Oct 27, 2025
c275041
cargo fmt
Wollac Nov 4, 2025
1a882b2
sort test chain configs
Wollac Nov 4, 2025
11fbe87
Merge branch 'main' into feat/multiblock
Wollac Nov 18, 2025
6212fa7
fix unused imports
Wollac Nov 19, 2025
afe26ed
Merge branch 'main' into feat/multiblock
Wollac Dec 9, 2025
0d37730
add eth aliases
Wollac Dec 9, 2025
a8e2a90
revert Steel.sol changes
Wollac Dec 9, 2025
d013121
fix clippy warnings
Wollac Dec 9, 2025
bb2b346
Revert "fix clippy warnings"
Wollac Dec 9, 2025
8c0a86c
cleanups
Wollac Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ 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.

### ⚙️ 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.

## [2.4.0](https://github.com/boundless-xyz/steel/releases/tag/v2.4.0)

Expand Down
3 changes: 3 additions & 0 deletions crates/steel/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ impl<H: EvmBlockHeader> BlockHeaderCommit<H> for () {

impl<F: EvmFactory> BlockInput<F> {
/// 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<F::SpecId>) -> GuestEvmEnv<F> {
// verify that the state root matches the state trie
let state_root = self.state_trie.hash_slow();
Expand Down Expand Up @@ -162,6 +164,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!(
Expand Down
8 changes: 7 additions & 1 deletion crates/steel/src/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
use crate::{
config::{ChainSpec, ForkCondition},
serde::{Eip2718Wrapper, RlpHeader},
EvmBlockHeader, EvmEnv, EvmFactory, EvmInput, EvmSpecId,
EvmBlockHeader, EvmEnv, EvmFactory, EvmInput, EvmSpecId, MultiblockEvmEnv, MultiblockEvmInput,
};
use alloy_consensus::{Eip658Value, TxReceipt};
use alloy_eips::{eip4844, eip7691, Encodable2718, Typed2718};
Expand Down Expand Up @@ -140,6 +140,12 @@ pub type EthEvmEnv<D, C> = EvmEnv<D, EthEvmFactory, C>;
/// [EvmInput] for Ethereum.
pub type EthEvmInput = EvmInput<EthEvmFactory>;

/// [MultiblockEvmEnv] for Ethereum.
pub type EthMultiblockEvmEnv<D, C> = MultiblockEvmEnv<D, EthEvmFactory, C>;

/// [MultiblockEvmInput] for Ethereum.
pub type EthMultiblockEvmInput = MultiblockEvmInput<EthEvmFactory>;

/// [EvmBlockHeader] for Ethereum.
pub type EthBlockHeader = RlpHeader<alloy_consensus::Header>;

Expand Down
84 changes: 60 additions & 24 deletions crates/steel/src/host/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,7 @@ impl<F: EvmFactory> EvmEnv<(), F, ()> {
/// # }
/// ```
pub fn builder() -> EvmEnvBuilder<(), F, (), ()> {
EvmEnvBuilder {
provider: (),
provider_config: ProviderConfig::default(),
block: BlockId::default(),
chain_spec: (),
beacon_config: (),
phantom: PhantomData,
}
EvmEnvBuilder::new()
}
}

Expand All @@ -69,16 +62,43 @@ impl<F: EvmFactory> EvmEnv<(), F, ()> {
/// # Usage
/// The builder can be created using [EvmEnv::builder()]. Various configurations can be chained to
/// customize the environment before calling the `build` function to create the final [EvmEnv].
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct EvmEnvBuilder<P, F, S, B> {
provider: P,
provider_config: ProviderConfig,
block: BlockId,
chain_spec: S,
beacon_config: B,
commitment_config: B,
phantom: PhantomData<F>,
}

// the derive macro would also require F to implement Clone which is not necessary.
impl<P: Clone, F, S: Clone, B: Clone> Clone for EvmEnvBuilder<P, F, S, B> {
fn clone(&self) -> Self {
Self {
provider: self.provider.clone(),
provider_config: self.provider_config.clone(),
block: self.block,
chain_spec: self.chain_spec.clone(),
commitment_config: self.commitment_config.clone(),
phantom: PhantomData,
}
}
}

impl<F: EvmFactory> EvmEnvBuilder<(), F, (), ()> {
pub(crate) fn new() -> Self {
EvmEnvBuilder {
provider: (),
provider_config: ProviderConfig::default(),
block: BlockId::default(),
chain_spec: (),
commitment_config: (),
phantom: PhantomData,
}
}
}

impl<S> EvmEnvBuilder<(), EthEvmFactory, S, ()> {
/// Sets the Ethereum HTTP RPC endpoint that will be used by the [EvmEnv].
pub fn rpc(self, url: Url) -> EvmEnvBuilder<RootProvider<Ethereum>, EthEvmFactory, S, ()> {
Expand All @@ -100,7 +120,7 @@ impl<F: EvmFactory, S> EvmEnvBuilder<(), F, S, ()> {
provider_config: self.provider_config,
block: self.block,
chain_spec: self.chain_spec,
beacon_config: self.beacon_config,
commitment_config: self.commitment_config,
phantom: self.phantom,
}
}
Expand All @@ -117,7 +137,7 @@ impl<P, F: EvmFactory, B> EvmEnvBuilder<P, F, (), B> {
provider_config: self.provider_config,
block: self.block,
chain_spec,
beacon_config: self.beacon_config,
commitment_config: self.commitment_config,
phantom: self.phantom,
}
}
Expand Down Expand Up @@ -186,7 +206,7 @@ impl<P, F, S> EvmEnvBuilder<P, F, S, ()> {
provider_config: self.provider_config,
block: self.block,
chain_spec: self.chain_spec,
beacon_config: Eip2935History {
commitment_config: Eip2935History {
commitment_block: block,
},
phantom: Default::default(),
Expand All @@ -212,7 +232,7 @@ impl<P, S> EvmEnvBuilder<P, EthEvmFactory, S, ()> {
provider_config: self.provider_config,
block: self.block,
chain_spec: self.chain_spec,
beacon_config: Beacon {
commitment_config: Beacon {
url,
commitment_version: CommitmentVersion::Beacon,
},
Expand Down Expand Up @@ -250,6 +270,22 @@ impl<P, F, S, B> EvmEnvBuilder<P, F, S, B> {
self
}

/// Returns a copy of the builder with elided commitment config and set EVM execution block.
pub(crate) fn to_block(&self, block: impl Into<BlockId>) -> EvmEnvBuilder<P, F, S, ()>
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,
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the callsite for this function in multiblock, it was not obvious to me that this clones rather than consuming self like many of the other methods do. I might suggest making this consume self, and adding an explicit clone in front of the calls.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the Rust naming conventions to_ is borrowed -> owned for non-Copy types, like Path::to_str or str::to_lowercase().
So the alternative would be to rename this to an into_ and combine it will an explicit clone, I'll check whether that might help readability, even though it will always be combined with a clone.


/// Returns the [EvmBlockHeader] of the specified block.
///
/// If `block` is `None`, the block based on the current builder configuration is used instead.
Expand Down Expand Up @@ -327,7 +363,7 @@ impl<P, F: EvmFactory> EvmEnvBuilder<P, F, &ChainSpec<F::SpecId>, Eip2935History
{
let evm_header = self.get_header(None).await?;
let commitment_header = self
.get_header(Some(self.beacon_config.commitment_block))
.get_header(Some(self.commitment_config.commitment_block))
.await?;

log::debug!(
Expand Down Expand Up @@ -396,7 +432,7 @@ impl<P, S> EvmEnvBuilder<P, EthEvmFactory, S, Beacon> {
/// # }
/// ```
pub fn consensus_commitment(mut self) -> Self {
self.beacon_config.commitment_version = CommitmentVersion::Consensus;
self.commitment_config.commitment_version = CommitmentVersion::Consensus;
self
}

Expand Down Expand Up @@ -461,8 +497,8 @@ impl<P, S> EvmEnvBuilder<P, EthEvmFactory, S, Beacon> {
provider_config: self.provider_config,
block: self.block,
chain_spec: self.chain_spec,
beacon_config: History {
beacon_config: self.beacon_config,
commitment_config: History {
beacon_config: self.commitment_config,
commitment_block: block,
},
phantom: Default::default(),
Expand All @@ -483,8 +519,8 @@ impl<P> EvmEnvBuilder<P, EthEvmFactory, &ChainSpec<<EthEvmFactory as EvmFactory>
header.seal()
);

let beacon_url = self.beacon_config.url;
let version = self.beacon_config.commitment_version;
let beacon_url = self.commitment_config.url;
let version = self.commitment_config.commitment_version;
let commit = HostCommit {
inner: BeaconCommit::from_header(&header, version, &self.provider, beacon_url).await?,
config_id: self.chain_spec.digest(),
Expand All @@ -507,7 +543,7 @@ impl<P>
///
/// See [EvmEnvBuilder<P, EthEvmFactory, S, Beacon>::consensus_commitment] for more info.
pub fn consensus_commitment(mut self) -> Self {
self.beacon_config.beacon_config.commitment_version = CommitmentVersion::Consensus;
self.commitment_config.beacon_config.commitment_version = CommitmentVersion::Consensus;
self
}

Expand All @@ -519,7 +555,7 @@ impl<P>
{
let evm_header = self.get_header(None).await?;
let commitment_header = self
.get_header(Some(self.beacon_config.commitment_block))
.get_header(Some(self.commitment_config.commitment_block))
.await?;

log::debug!(
Expand All @@ -528,8 +564,8 @@ impl<P>
evm_header.seal()
);

let beacon_url = self.beacon_config.beacon_config.url;
let commitment_version = self.beacon_config.beacon_config.commitment_version;
let beacon_url = self.commitment_config.beacon_config.url;
let commitment_version = self.commitment_config.beacon_config.commitment_version;
let history_commit = HistoryCommit::from_headers(
&evm_header,
&commitment_header,
Expand Down
7 changes: 0 additions & 7 deletions crates/steel/src/host/db/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,6 @@ impl<N: Network, P: Provider<N>> ProofDb<ProviderDb<N, P>> {
/// 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<MerkleTrie>)> {
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();
Expand Down
2 changes: 1 addition & 1 deletion crates/steel/src/host/db/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<N: Network, P: Provider<N>> {
pub struct ProviderDb<N, P> {
/// Provider to fetch the data from.
provider: P,
/// Configuration of the provider.
Expand Down
31 changes: 24 additions & 7 deletions crates/steel/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,23 @@ use alloy::{
providers::Provider,
rpc::types::BlockNumberOrTag as AlloyBlockNumberOrTag,
};
use alloy_primitives::{BlockHash, B256};
use alloy_primitives::{BlockHash, BlockNumber, B256};
use anyhow::{ensure, Result};
use db::{ProofDb, ProviderDb};
use std::{
fmt::{self, Debug, Display},
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};

/// 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)
Expand Down Expand Up @@ -95,6 +96,18 @@ impl Display for BlockId {
}
}

impl From<BlockHash> for BlockId {
fn from(hash: BlockHash) -> Self {
Self::Hash(hash)
}
}

impl From<BlockNumber> 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)]
Expand Down Expand Up @@ -270,7 +283,7 @@ impl<D, F: EvmFactory, C> HostEvmEnv<D, F, C> {
/// # Ok(())
/// # }
/// ```
pub fn merge(self, mut other: Self) -> Result<Self> {
pub fn merge<C2>(self, mut other: HostEvmEnv<D, F, C2>) -> Result<Self> {
let Self {
mut db,
chain_id,
Expand All @@ -285,7 +298,11 @@ impl<D, F: EvmFactory, C> HostEvmEnv<D, F, C> {
header.seal() == other.header.seal(),
"execution header mismatch"
);
// the commitments do not need to match as long as the cfg_env is consistent
// the commitments do not need to match as long as the config_id is consistent
ensure!(
commit.config_id == other.commit.config_id,
"configuration mismatch"
);

// safe unwrap: EvmEnv is never returned without a DB
let db = db.take().unwrap();
Expand All @@ -311,7 +328,7 @@ where
F::Receipt: TryFrom<<N as Network>::ReceiptResponse>,
<F::Receipt as TryFrom<<N as Network>::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<EvmInput<F>> {
let input = BlockInput::from_proof_db(self.db.unwrap(), self.header).await?;

Expand Down
8 changes: 4 additions & 4 deletions crates/steel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub mod history;
pub mod host;
mod merkle;
mod mpt;
mod multiblock;
pub mod precompiles;
pub mod serde;
mod state;
Expand All @@ -64,8 +65,9 @@ pub use contract::{CallBuilder, 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::{SteelVerifier, EIP2935_HISTORY_LIMIT, HISTORY_LIMIT};

/// The serializable input to derive and validate an [EvmEnv] from.
#[non_exhaustive]
Expand All @@ -82,9 +84,7 @@ pub enum EvmInput<F: EvmFactory> {
}

impl<F: EvmFactory> EvmInput<F> {
/// 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<F::SpecId>) -> GuestEvmEnv<F> {
match self {
Expand Down
Loading
Loading