-
Notifications
You must be signed in to change notification settings - Fork 36
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
base: main
Are you sure you want to change the base?
Conversation
@@ -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")] |
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.
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.
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.
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.
// 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)) |
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.
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
toEthereum
, 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.
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, | ||
} | ||
} | ||
} |
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.
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.
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.
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"); |
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.
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>>>, |
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.
#[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; |
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.
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...
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.
Maybe we should do that along with a 2.0 version bump. I am not opposed to bumping to 2.0.
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.
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"); |
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.
assert!(self.receipts.is_none(), "Receipts not supported"); | |
assert!(self.receipts.is_none(), "Receipts not supported; unstable-event feature not enabled"); |
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, | ||
} | ||
} | ||
} |
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.
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.
/// 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() | ||
} |
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.
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.
/// 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)?)) | |
} |
/// Sets the 1st indexed topic. | ||
pub fn topic1<TO: Into<Topic>>(mut self, topic: TO) -> Self { |
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.
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.
#[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; |
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.
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")] |
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.
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.
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:
IERC20::Transfer
).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):
Guest-Side (Querying and Verifying Events):
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.