-
Notifications
You must be signed in to change notification settings - Fork 44
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
Changes from 15 commits
bc7997a
5bfd6d9
f1a9e7e
a098422
4ba0ddc
02b344e
87f5ded
cc965be
9fe8326
682a211
3bda990
369c8b3
38985d1
2c1e548
fb5c014
0226b31
03779d2
16f03c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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. | ||||||
|
@@ -27,6 +28,7 @@ pub struct BlockInput<H> { | |||||
storage_tries: Vec<MerkleTrie>, | ||||||
contracts: Vec<Bytes>, | ||||||
ancestors: Vec<H>, | ||||||
receipts: Option<Vec<Eip2718Wrapper<ReceiptEnvelope>>>, | ||||||
} | ||||||
|
||||||
/// Implement [BlockHeaderCommit] for the unit type. | ||||||
|
@@ -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"); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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, | ||||||
|
@@ -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>( | ||||||
|
@@ -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!( | ||||||
|
@@ -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) | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,193 @@ | ||||||||||||||||||||||||||||
// 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. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
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 Ethereum event query. | ||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||
/// ### Usage | ||||||||||||||||||||||||||||
/// - **Preflight calls on the Host:** To prepare calls on the host environment and build the | ||||||||||||||||||||||||||||
/// necessary proof, use [Event::preflight]. | ||||||||||||||||||||||||||||
/// - **Calls in the Guest:** To initialize the contract in the guest environment, use [Event::new]. | ||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||
/// See [Contract] for more details. | ||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||
/// ### 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! { | ||||||||||||||||||||||||||||
/// # #[derive(Debug)] | ||||||||||||||||||||||||||||
/// 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(); | ||||||||||||||||||||||||||||
/// # dbg!(logs); | ||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||
/// # 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implementing this for Event::new::<IERC20::Transfer>(&env) Instead of having to also specify In general I decided to assign a fixed SolEvent to a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems reasonable to me. Enabling One other option would be to define a trait that is either blanket implemented for |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
#[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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something I've done before is provide a return type of
Suggested change
|
||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is is possible to additionally provide a more type-safe option using |
||||||||||||||||||||||||||||
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::{anyhow, Result}; | ||||||||||||||||||||||||||||
use revm::Database as RevmDatabase; | ||||||||||||||||||||||||||||
use std::fmt::Display; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
impl<D, H: EvmBlockHeader, C> Event<(), &mut HostEvmEnv<D, H, C>> | ||||||||||||||||||||||||||||
where | ||||||||||||||||||||||||||||
D: EvmDatabase + Send + 'static, | ||||||||||||||||||||||||||||
<D as RevmDatabase>::Error: Display + Send + 'static, | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
/// Constructor for preflighting an event query for a specific Solidity 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: Display + Send + '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>>> { | ||||||||||||||||||||||||||||
let logs = self | ||||||||||||||||||||||||||||
.env | ||||||||||||||||||||||||||||
.spawn_with_db(move |db| db.logs(self.filter)) | ||||||||||||||||||||||||||||
.await | ||||||||||||||||||||||||||||
.map_err(|err| anyhow!("querying '{}' failed: {}", S::SIGNATURE, err))?; | ||||||||||||||||||||||||||||
logs.iter() | ||||||||||||||||||||||||||||
.map(|log| Ok(S::decode_log(log, false)?)) | ||||||||||||||||||||||||||||
.collect() | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
/// Creates an event filter for a specific Solidity event and block header. | ||||||||||||||||||||||||||||
fn event_filter<S: SolEvent, H: EvmBlockHeader>(header: &Sealed<H>) -> Filter { | ||||||||||||||||||||||||||||
assert!(!S::ANONYMOUS, "Anonymous events not supported"); | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) | ||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
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.