Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WEB3-315: feat: Add EVM Event support to Steel #409

Merged
merged 18 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ risc0-binfmt = { git = "https://github.com/risc0/risc0", branch = "main", defaul

# Alloy guest dependencies
alloy-consensus = { version = "0.9" }
alloy-eips = { version = "0.9" }
alloy-rlp = { version = "0.3.8" }
alloy-primitives = { version = "0.8.16" }
alloy-rpc-types = { version = "0.9" }
alloy-sol-types = { version = "0.8.16" }

# OP Steel
Expand Down
1 change: 1 addition & 0 deletions crates/op-steel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ risc0-op-steel = { workspace = true, features = ["host"] }
[features]
default = []
host = ["dep:alloy", "dep:tokio", "dep:url", "risc0-steel/host", "alloy-primitives/rand"]
unstable-event = []
10 changes: 10 additions & 0 deletions crates/op-steel/src/optimism/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ impl EvmBlockHeader for OpBlockHeader {
fn state_root(&self) -> &B256 {
&self.0.inner().state_root
}
#[cfg(feature = "unstable-event")]
#[inline]
fn receipts_root(&self) -> &B256 {
&self.0.inner().receipts_root
}
#[cfg(feature = "unstable-event")]
#[inline]
fn logs_bloom(&self) -> &alloy_primitives::Bloom {
&self.0.inner().logs_bloom
}

#[inline]
fn fill_block_env(&self, blk_env: &mut BlockEnv) {
Expand Down
7 changes: 5 additions & 2 deletions crates/steel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

<!-- TODO: Add link when release is finished -->
## [1.3.0]
### ⚡️ Features

- Introduce the capability to query Ethereum events. The new `Event` allows to query events of a specific type in Steel. Its usage is very similar to the existing `Contract`, during the preflight step and in the guest. This functionality is currently marked unstable and must be enabled using the `unstable-event` feature.

## [1.3.0](https://github.com/risc0/risc0-ethereum/releases/tag/v1.3.0)

### ⚡️ Features

Expand Down
3 changes: 3 additions & 0 deletions crates/steel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
alloy = { workspace = true, optional = true, features = ["full"] }
alloy-consensus = { workspace = true }
alloy-eips = { workspace = true }
alloy-primitives = { workspace = true, features = ["rlp", "serde"] }
alloy-rlp = { workspace = true }
alloy-rpc-types = { workspace = true }
alloy-sol-types = { workspace = true }
alloy-trie = { workspace = true, features = ["serde"] }
anyhow = { workspace = true }
Expand Down Expand Up @@ -50,5 +52,6 @@ host = [
"dep:tokio",
"dep:url",
]
unstable-event = []
unstable-history = []
unstable-verifier = []
46 changes: 41 additions & 5 deletions crates/steel/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
// limitations under the License.

use crate::{
config::ChainSpec, state::StateDb, BlockHeaderCommit, Commitment, CommitmentVersion,
EvmBlockHeader, EvmEnv, GuestEvmEnv, MerkleTrie,
config::ChainSpec, serde::Eip2718Wrapper, state::StateDb, BlockHeaderCommit, Commitment,
CommitmentVersion, EvmBlockHeader, EvmEnv, GuestEvmEnv, MerkleTrie,
};
use ::serde::{Deserialize, Serialize};
use alloy_consensus::ReceiptEnvelope;
use alloy_primitives::{map::HashMap, Bytes, Sealed, B256};

/// Input committing to the corresponding execution block hash.
Expand All @@ -27,6 +28,7 @@ pub struct BlockInput<H> {
storage_tries: Vec<MerkleTrie>,
contracts: Vec<Bytes>,
ancestors: Vec<H>,
receipts: Option<Vec<Eip2718Wrapper<ReceiptEnvelope>>>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ReceiptEnvelope cannot be serialized using bincode or similar, so it must be RLP encoded. However, since the hash calculation also relies on the RLP encoding, this may actually be a performance improvement.

}

/// Implement [BlockHeaderCommit] for the unit type.
Expand Down Expand Up @@ -72,11 +74,38 @@ impl<H: EvmBlockHeader> BlockInput<H> {
previous_header = ancestor;
}

#[cfg(not(feature = "unstable-event"))]
// there must not be any receipts, if events are not supported
let logs = {
assert!(self.receipts.is_none(), "Receipts not supported");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assert!(self.receipts.is_none(), "Receipts not supported");
assert!(self.receipts.is_none(), "Receipts not supported; unstable-event feature not enabled");

None
};
#[cfg(feature = "unstable-event")]
// verify the root hash of the included receipts and extract their logs
let logs = self.receipts.map(|receipts| {
let root = alloy_trie::root::ordered_trie_root_with_encoder(&receipts, |r, out| {
alloy_eips::eip2718::Encodable2718::encode_2718(r, out)
});
assert_eq!(header.receipts_root(), &root, "Receipts root mismatch");

receipts
.into_iter()
.flat_map(|wrapper| match wrapper.into_inner() {
ReceiptEnvelope::Legacy(t) => t.receipt.logs,
ReceiptEnvelope::Eip2930(t) => t.receipt.logs,
ReceiptEnvelope::Eip1559(t) => t.receipt.logs,
ReceiptEnvelope::Eip4844(t) => t.receipt.logs,
ReceiptEnvelope::Eip7702(t) => t.receipt.logs,
})
.collect()
});

let db = StateDb::new(
self.state_trie,
self.storage_tries,
self.contracts,
block_hashes,
logs,
);
let commit = Commitment::new(
CommitmentVersion::Block as u16,
Expand All @@ -91,19 +120,19 @@ impl<H: EvmBlockHeader> BlockInput<H> {

#[cfg(feature = "host")]
pub mod host {
use std::fmt::Display;

use super::BlockInput;
use crate::{
host::db::{AlloyDb, ProofDb, ProviderDb},
serde::Eip2718Wrapper,
EvmBlockHeader,
};
use alloy::{network::Network, providers::Provider, transports::Transport};
use alloy_primitives::Sealed;
use anyhow::{anyhow, ensure};
use log::debug;
use std::fmt::Display;

impl<H: EvmBlockHeader> BlockInput<H> {
impl<H> BlockInput<H> {
/// Creates the `BlockInput` containing the necessary EVM state that can be verified against
/// the block hash.
pub(crate) async fn from_proof_db<T, N, P>(
Expand Down Expand Up @@ -137,6 +166,11 @@ pub mod host {
ancestors.push(header);
}

let receipts = db.receipt_proof().await?;
// wrap the receipts so that they can be serialized
let receipts =
receipts.map(|receipts| receipts.into_iter().map(Eip2718Wrapper::new).collect());

debug!("state size: {}", state_trie.size());
debug!("storage tries: {}", storage_tries.len());
debug!(
Expand All @@ -145,13 +179,15 @@ pub mod host {
);
debug!("contracts: {}", contracts.len());
debug!("ancestor blocks: {}", ancestors.len());
debug!("receipts: {:?}", receipts.as_ref().map(Vec::len));

let input = BlockInput {
header: header.into_inner(),
state_trie,
storage_tries,
contracts,
ancestors,
receipts,
};

Ok(input)
Expand Down
2 changes: 1 addition & 1 deletion crates/steel/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ where
/// required.
pub fn try_call(self) -> Result<S::Return, String> {
let mut evm = new_evm::<_, H>(
WrapStateDb::new(self.env.db()),
WrapStateDb::new(self.env.db(), &self.env.header),
self.env.cfg_env.clone(),
self.env.header.inner(),
);
Expand Down
10 changes: 10 additions & 0 deletions crates/steel/src/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ impl EvmBlockHeader for EthBlockHeader {
fn state_root(&self) -> &B256 {
&self.inner().state_root
}
#[cfg(feature = "unstable-event")]
#[inline]
fn receipts_root(&self) -> &B256 {
&self.inner().receipts_root
}
#[cfg(feature = "unstable-event")]
#[inline]
fn logs_bloom(&self) -> &alloy_primitives::Bloom {
&self.inner().logs_bloom
}

#[inline]
fn fill_block_env(&self, blk_env: &mut BlockEnv) {
Expand Down
192 changes: 192 additions & 0 deletions crates/steel/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// 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 related to event queries.
pub use alloy_rpc_types::{Topic, ValueOrArray};

use crate::{state::WrapStateDb, EvmBlockHeader, EvmDatabase, GuestEvmEnv};
use alloy_primitives::{Address, Log, Sealed};
use alloy_rpc_types::Filter;
use alloy_sol_types::SolEvent;
use std::marker::PhantomData;

/// Represents an EVM event query.
///
/// ### Usage
/// - **Preflight calls on the Host:** To prepare the event query on the host environment and build
/// the necessary proof, use [Event::preflight].
/// - **Calls in the Guest:** To initialize the event query in the guest, use [Event::new].
///
/// ### Examples
/// ```rust,no_run
/// # use risc0_steel::{ethereum::EthEvmEnv, Event};
/// # use alloy_primitives::address;
/// # use alloy_sol_types::sol;
///
/// # #[tokio::main(flavor = "current_thread")]
/// # async fn main() -> anyhow::Result<()> {
/// let contract_address = address!("dAC17F958D2ee523a2206206994597C13D831ec7");
/// sol! {
/// interface IERC20 {
/// event Transfer(address indexed from, address indexed to, uint256 value);
/// }
/// }
///
/// // Host:
/// let url = "https://ethereum-rpc.publicnode.com".parse()?;
/// let mut env = EthEvmEnv::builder().rpc(url).build().await?;
/// let event = Event::preflight::<IERC20::Transfer>(&mut env).address(contract_address);
/// event.query().await?;
///
/// let evm_input = env.into_input().await?;
///
/// // Guest:
/// let env = evm_input.into_env();
/// let event = Event::new::<IERC20::Transfer>(&env).address(contract_address);
/// let logs = event.query();
///
/// # Ok(())
/// # }
/// ```
///
/// [Contract]: crate::Contract
pub struct Event<S, E> {
filter: Filter,
env: E,
phantom: PhantomData<S>,
}

impl<H: EvmBlockHeader> Event<(), &GuestEvmEnv<H>> {
/// Constructor for executing an event query for a specific Solidity event.
pub fn new<S: SolEvent>(env: &GuestEvmEnv<H>) -> Event<S, &GuestEvmEnv<H>> {
Event {
filter: event_filter::<S, H>(env.header()),
env,
phantom: PhantomData,
}
}
}
Comment on lines +70 to +79
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implementing this for Event<(), E> makes it possible to initialize with

Event::new::<IERC20::Transfer>(&env)

Instead of having to also specify E.

In general I decided to assign a fixed SolEvent to a Steel. This allows the corresponding Event::query() to return the actual events of the correct type.
An alternative would be to specify an arbitrary filter in the query step and leave all casting and conversion to the user.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems reasonable to me. Enabling Event::new::<IERC20::Transfer>(&env) as the API is nice.

One other option would be to define a trait that is either blanket implemented for SolEvent to provide e.g. IERC20::Transfer::steel_event or something. I don't really think this is better though.


#[cfg(feature = "unstable-event")]
impl<S: SolEvent, H: EvmBlockHeader> Event<S, &GuestEvmEnv<H>> {
/// Executes the query and returns the matching logs and panics on failure.
///
/// A convenience wrapper for [Event::try_query], panicking if the call fails. Useful when
/// success is expected.
pub fn query(self) -> Vec<Log<S>> {
self.try_query().unwrap()
}

/// Attempts to execute the query and returns the matching logs or an error.
pub fn try_query(self) -> anyhow::Result<Vec<Log<S>>> {
let logs = WrapStateDb::new(self.env.db(), &self.env.header).logs(self.filter)?;
logs.iter()
.map(|log| Ok(S::decode_log(log, false)?))
.collect()
}
Comment on lines +91 to +97
Copy link
Contributor

Choose a reason for hiding this comment

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

Something I've done before is provide a return type of impl Iterator, which is nice when the caller just wants to go through the list once. Up to which one you think works better.

Suggested change
/// Attempts to execute the query and returns the matching logs or an error.
pub fn try_query(self) -> anyhow::Result<Vec<Log<S>>> {
let logs = WrapStateDb::new(self.env.db(), &self.env.header).logs(self.filter)?;
logs.iter()
.map(|log| Ok(S::decode_log(log, false)?))
.collect()
}
/// Attempts to execute the query and returns the matching logs or an error.
pub fn try_query(self) -> anyhow::Result<impl Iterator<Item = Log<S>>> {
let logs = WrapStateDb::new(self.env.db(), &self.env.header).logs(self.filter)?;
logs.iter()
.map(|log| Ok(S::decode_log(log, false)?))
}

}

impl<S, E> Event<S, E> {
/// Sets the address to query with this filter.
///
/// See [`Filter::address`].
pub fn address<A: Into<ValueOrArray<Address>>>(mut self, address: A) -> Self {
self.filter.address = address.into().into();
self
}

/// Sets the 1st indexed topic.
pub fn topic1<TO: Into<Topic>>(mut self, topic: TO) -> Self {
Comment on lines +109 to +110
Copy link
Contributor

Choose a reason for hiding this comment

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

Is is possible to additionally provide a more type-safe option using SolEvent::TopicList? It would be nice, but I can't tell from their docs how to make this work myself.

self.filter.topics[1] = topic.into();
self
}

/// Sets the 2nd indexed topic.
pub fn topic2<TO: Into<Topic>>(mut self, topic: TO) -> Self {
self.filter.topics[2] = topic.into();
self
}

/// Sets the 3rd indexed topic.
pub fn topic3<TO: Into<Topic>>(mut self, topic: TO) -> Self {
self.filter.topics[3] = topic.into();
self
}
}

#[cfg(feature = "host")]
mod host {
use super::*;
use crate::host::HostEvmEnv;
use anyhow::{Context, Result};
use revm::Database as RevmDatabase;
use std::error::Error as StdError;

impl<D, H: EvmBlockHeader, C> Event<(), &mut HostEvmEnv<D, H, C>>
where
D: EvmDatabase + Send + 'static,
<D as RevmDatabase>::Error: StdError + Send + Sync + 'static,
{
/// Constructor for preflighting an event query for a specific EVM event.
///
/// Initializes the environment for event queries, fetching necessary data via the
/// [Provider], and generating a storage proof for any accessed elements using
/// [EvmEnv::into_input].
///
/// [EvmEnv::into_input]: crate::EvmEnv::into_input
/// [EvmEnv]: crate::EvmEnv
/// [Provider]: alloy::providers::Provider
pub fn preflight<S: SolEvent>(
env: &mut HostEvmEnv<D, H, C>,
) -> Event<S, &mut HostEvmEnv<D, H, C>> {
Event {
filter: event_filter::<S, H>(env.header()),
env,
phantom: PhantomData,
}
}
}

impl<S: SolEvent, D, H: EvmBlockHeader, C> Event<S, &mut HostEvmEnv<D, H, C>>
where
D: EvmDatabase + Send + 'static,
<D as RevmDatabase>::Error: StdError + Send + Sync + 'static,
{
/// Executes the event query using an [EvmEnv] constructed with [Event::preflight].
///
/// This uses [tokio::task::spawn_blocking] to run the blocking revm execution.
///
/// [EvmEnv]: crate::EvmEnv
pub async fn query(self) -> Result<Vec<Log<S>>> {
log::info!("Executing preflight querying event '{}'", S::SIGNATURE);

let logs = self
.env
.spawn_with_db(move |db| db.logs(self.filter))
.await
.with_context(|| format!("querying logs for '{}' failed", S::SIGNATURE))?;
logs.iter()
.map(|log| Ok(S::decode_log(log, false)?))
.collect()
}
}
}

/// Creates an event filter for a specific event and block header.
fn event_filter<S: SolEvent, H: EvmBlockHeader>(header: &Sealed<H>) -> Filter {
assert!(!S::ANONYMOUS, "Anonymous events not supported");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not possible to cleanly handle anonymous events with this approach. Since do not have a signature success of the decode_log cannot be guaranteed.

Filter::new()
.event_signature(S::SIGNATURE_HASH)
.at_block_hash(header.seal())
}
Loading
Loading