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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

Wollac
Copy link
Contributor

@Wollac Wollac commented Jan 23, 2025

Description

This PR introduces the capability to query Ethereum events within Steel. It allows developers to efficiently retrieve specific event logs emitted by smart contracts and confidently use them in the guest.
Its usage is very similar to the existing Contract which is used to execute contract calls.

Key Features:

  • Event Querying: Enables querying for events based on their type (e.g., IERC20::Transfer).
  • ZK Verification: Guarantees the integrity and correctness of event query results within the guest environment. To achieve this, the input includes all relevant receipts, unless the header's Bloom filter indicates that no matching logs are present.
  • Guest-Side Verification: The guest can verify the query results against the header's Bloom filter field or the Receipts root field, ensuring trustworthiness.

Performance

Event queries on Ethereum Mainnet, involving approximately 100-300 receipts per block, are executed with an average of 4 million cycles utilizing the Keccak accelerator.

Usage

The following code snippet illustrates how to query for ERC20 Transfer events using the new feature:

Host-Side (Fetching EVM Input):

use alloy_primitives::address;
use alloy_sol_types::sol;
use steel_evm::{EthEvmEnv, Event};

let contract_address = address!("dAC17F958D2ee523a2206206994597C13D831ec7"); // USDT contract address
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-Side (Querying and Verifying Events):

use alloy_primitives::address;
use alloy_sol_types::sol;
use steel_evm::{EthEvmEnv, Event};

// Assuming `evm_input` is passed to the guest
let env = evm_input.into_env();
let event = Event::new::<IERC20::Transfer>(&env).address(contract_address); // USDT contract address
let logs = event.query();

// ... further processing of the retrieved `logs` within the guest ...

Example

A practical example is included, demonstrating how to calculate the total amount of USDT transferred in a specific block by querying and aggregating the Transfer events emitted by the USDT ERC20 contract. This showcases the real-world applicability of event querying with ZK verification.

@Wollac Wollac requested a review from a team as a code owner January 23, 2025 16:19
@github-actions github-actions bot changed the title feat: Add event support to Steel WEB3-315: feat: Add event support to Steel Jan 23, 2025
@@ -203,6 +229,12 @@ pub trait EvmBlockHeader: Sealable {
fn timestamp(&self) -> u64;
/// Returns the state root hash.
fn state_root(&self) -> &B256;
#[cfg(feature = "unstable-event")]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This adds new fields we require from a block. This is a breaking change of the EvmBlockHeader trait.
And, unfortunately, it is difficult to hide this change behind feature flags.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is probably ok. I would be a bit surprised if anyone is implementing this themselves right now, and if anyone screams we can roll back the release and bump to 2.0 when we release this.

Comment on lines +378 to +387
// Unfortunately ReceiptResponse does not implement ReceiptEnvelope, so we have to
// manually convert it. We convert to a TransactionReceipt which is the default and
// works for Ethereum-compatible networks.
// Use serde here for the conversion as it is much safer than mem::transmute.
// TODO(https://github.com/alloy-rs/alloy/issues/854): use ReceiptEnvelope directly
let json = serde_json::to_value(rpc_receipt).context("failed to serialize")?;
let tx_receipt: TransactionReceipt = serde_json::from_value(json)
.context("failed to parse as Ethereum transaction receipt")?;

Ok(tx_receipt_to_envelope(tx_receipt))
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 very unfortunate that in the current version of Alloy a ReceiptResponse does not implement the ReceiptEnvelop (as the TransactionResponse does for example).
So we have to convert it manually. I tried two approaches:

  • Switch from the general Network to Ethereum, where the trait boundaries are correct. However, this completely changes the bounds in Steel and breaks op-steel support.
  • Cast" the receipt response. The current code does this in a memorysafe way with serde-json. This is ugly, but at least it works for everything that is Ethereum compatible. (It will not otherwise cause any weird bugs in the guest, as the resulting receipt root is verified.)

Eventually, this can only be fixed when alloy-rs/alloy#854 is fixed upstream.

Comment on lines +73 to +82
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,
}
}
}
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.


/// 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");
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.

@@ -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.

@Wollac Wollac requested review from nategraf and capossele January 23, 2025 18:28
@Wollac Wollac changed the title WEB3-315: feat: Add event support to Steel WEB3-315: feat: Add EVM Event support to Steel Jan 23, 2025
Comment on lines +232 to +237
#[cfg(feature = "unstable-event")]
/// Returns the receipts root hash of the block.
fn receipts_root(&self) -> &B256;
#[cfg(feature = "unstable-event")]
/// Returns the logs bloom filter of the block
fn logs_bloom(&self) -> &Bloom;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we instead of defining our own proprietary EvmBlockHeader trait, use alloy's BlockHeader?
The latter one contains many more methods which are not needed by Steel, but should already be implemented for all networks that are supported in alloy. I however, would be a bigger (breaking) change...

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should do that along with a 2.0 version bump. I am not opposed to bumping to 2.0.

Copy link
Contributor

@nategraf nategraf left a comment

Choose a reason for hiding this comment

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

Very nice! I think this is going to be very useful. We should show this to some partners as soon as it's in a release to get feedback

#[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");

Comment on lines +73 to +82
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,
}
}
}
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.

Comment on lines +91 to +97
/// 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()
}
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)?))
}

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

Comment on lines +232 to +237
#[cfg(feature = "unstable-event")]
/// Returns the receipts root hash of the block.
fn receipts_root(&self) -> &B256;
#[cfg(feature = "unstable-event")]
/// Returns the logs bloom filter of the block
fn logs_bloom(&self) -> &Bloom;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should do that along with a 2.0 version bump. I am not opposed to bumping to 2.0.

@@ -203,6 +229,12 @@ pub trait EvmBlockHeader: Sealable {
fn timestamp(&self) -> u64;
/// Returns the state root hash.
fn state_root(&self) -> &B256;
#[cfg(feature = "unstable-event")]
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is probably ok. I would be a bit surprised if anyone is implementing this themselves right now, and if anyone screams we can roll back the release and bump to 2.0 when we release this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants