risc0_steel/event.rs
1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Types related to event queries.
16pub use alloy_rpc_types::{Topic, ValueOrArray};
17
18use crate::{state::WrapStateDb, EvmBlockHeader, EvmDatabase, EvmFactory, GuestEvmEnv};
19use alloy_primitives::{Address, Bloom, Log, Sealed};
20use alloy_rpc_types::{Filter, FilteredParams};
21use alloy_sol_types::SolEvent;
22use std::marker::PhantomData;
23
24/// Represents an EVM event query.
25///
26/// This query builder is designed for fetching specific Solidity events that occurred within the
27/// block associated with the provided `EvmEnv`.
28///
29/// ### Filtering Capabilities
30/// This `Event` query builder is intentionally designed to mirror the structure and capabilities of
31/// the [alloy_rpc_types::Filter] type used in standard Ethereum RPC calls, adapted for the
32/// constraints of the RISC Zero zkVM environment.
33///
34/// You can filter events based on:
35/// - **Contract Addresses:** Use the [`.address()`](Event::address) method to specify the event
36/// source:
37/// - A single address matches only events from this address.
38/// - Multiple addresses (pass a `Vec<Address>`) matches events from *any* contract address.
39/// - Wildcard (default): If `.address()` is not called, or if an empty `Vec` is provided, it
40/// matches events from *any* contract address.
41/// - **Indexed Topics:** Use the [`.topic1()`](Event::topic1), [`.topic2()`](Event::topic2) and
42/// [`.topic3()`](Event::topic3) to filter by the indexed arguments of the event:
43/// - A single value matches only events where the topic has this exact value.
44/// - Multiple values (pass a `Vec<B256>`) matches events where the topic matches *any* value in
45/// the list.
46/// - Wildcard (default): If `.topicX()` is not called or if an empty `Vec` is provided, it
47/// matches *any* value for that topic position.
48///
49/// Certain filtering options available in [alloy_rpc_types::Filter] are not applicable or are
50/// fixed within the Steel environment:
51/// - **Block Specification:** The block context for the query is determined by the `EvmEnv`
52/// (retrieved via `env.header()`) used to create the `Event` query. You cannot specify a block
53/// range or a different block hash.
54/// - **Topic 0 (Event Signature):** This topic is automatically set based on the `SolEvent` type
55/// parameter (`S`) provided to [Event::new] or [Event::preflight] (using `S::SIGNATURE_HASH`). It
56/// cannot be altered or set to a wildcard/list. Anonymous events (where `S::ANONYMOUS` is true)
57/// are not supported.
58///
59/// ### Usage
60/// The usage pattern mirrors other Steel interactions like [Contract]:
61/// - **Preflight calls on the Host:** To prepare the event query on the host environment and build
62/// the necessary proof, use [Event::preflight].
63/// - **Calls in the Guest:** To initialize the event query in the guest, use [Event::new].
64///
65/// ### Examples
66/// Basic usage with a single contract address:
67/// ```rust,no_run
68/// # use risc0_steel::{ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, Event};
69/// # use alloy_primitives::address;
70/// # use alloy_sol_types::sol;
71/// # #[tokio::main(flavor = "current_thread")]
72/// # async fn main() -> anyhow::Result<()> {
73/// let contract_address = address!("dAC17F958D2ee523a2206206994597C13D831ec7");
74/// sol! {
75/// interface IERC20 {
76/// event Transfer(address indexed from, address indexed to, uint256 value);
77/// }
78/// }
79///
80/// // Host:
81/// let url = "https://ethereum-rpc.publicnode.com".parse()?;
82/// let mut env = EthEvmEnv::builder().rpc(url).chain_spec(Ð_MAINNET_CHAIN_SPEC).build().await?;
83/// let event = Event::preflight::<IERC20::Transfer>(&mut env).address(contract_address);
84/// event.query().await?;
85///
86/// let evm_input = env.into_input().await?;
87///
88/// // Guest:
89/// let env = evm_input.into_env(Ð_MAINNET_CHAIN_SPEC);
90/// let event = Event::new::<IERC20::Transfer>(&env).address(contract_address);
91/// let logs = event.query();
92///
93/// # Ok(())
94/// # }
95/// ```
96///
97/// Advanced filtering with multiple addresses and topics:
98/// ```rust,no_run
99/// # use risc0_steel::{ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}, Event};
100/// # use alloy_primitives::{address, b256, B256, Address};
101/// # use alloy_rpc_types::{Topic, ValueOrArray};
102/// # use alloy_sol_types::sol;
103///
104/// # #[tokio::main(flavor = "current_thread")]
105/// # async fn main() -> anyhow::Result<()> {
106/// // define multiple contract addresses and potential senders
107/// let usdt_address = address!("dAC17F958D2ee523a2206206994597C13D831ec7");
108/// let usdc_address = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
109///
110/// sol! {
111/// interface IERC20 {
112/// event Transfer(address indexed from, address indexed to, uint256 value);
113/// }
114/// }
115///
116/// let url = "https://ethereum-rpc.publicnode.com".parse()?;
117/// let mut env = EthEvmEnv::builder().rpc(url).chain_spec(Ð_MAINNET_CHAIN_SPEC).build().await?;
118///
119/// // Create an event query for Transfer events from *either* USDT or USDC contract,
120/// // originating from *either* sender1 or sender2.
121/// let event = Event::preflight::<IERC20::Transfer>(&mut env)
122/// // filter by contract address: Match USDT OR USDC
123/// .address(vec![usdt_address, usdc_address])
124/// // filter by topic 1 (`from`): Match sender1 OR sender2
125/// .topic1(vec![
126/// address!("0000000000000000000000000000000000000001").into_word(),
127/// address!("0000000000000000000000000000000000000002").into_word(),
128/// ]);
129/// // topic2 (`to`) and topic3 are left as wildcards
130///
131/// let logs = event.query().await?;
132/// # Ok(())
133/// # }
134/// ```
135///
136/// [Contract]: crate::Contract
137pub struct Event<S, E> {
138 filter: Filter,
139 env: E,
140 phantom: PhantomData<S>,
141}
142
143impl<F: EvmFactory> Event<(), &GuestEvmEnv<F>> {
144 /// Constructor for executing an event query for a specific Solidity event.
145 pub fn new<S: SolEvent>(env: &GuestEvmEnv<F>) -> Event<S, &GuestEvmEnv<F>> {
146 Event {
147 filter: event_filter::<S, F::Header>(env.header()),
148 env,
149 phantom: PhantomData,
150 }
151 }
152}
153
154impl<S: SolEvent, F: EvmFactory> Event<S, &GuestEvmEnv<F>> {
155 /// Executes the query and returns the matching logs and panics on failure.
156 ///
157 /// A convenience wrapper for [Event::try_query], panicking if the call fails. Useful when
158 /// success is expected.
159 pub fn query(self) -> Vec<Log<S>> {
160 self.try_query().unwrap()
161 }
162
163 /// Attempts to execute the query and returns the matching logs or an error.
164 pub fn try_query(self) -> anyhow::Result<Vec<Log<S>>> {
165 let logs = WrapStateDb::new(self.env.db(), &self.env.header).logs(self.filter)?;
166 logs.iter().map(|log| Ok(S::decode_log(log)?)).collect()
167 }
168}
169
170impl<S, E> Event<S, E> {
171 /// Sets the address to query with this filter.
172 ///
173 /// See [`Filter::address`].
174 pub fn address<T: Into<ValueOrArray<Address>>>(mut self, address: T) -> Self {
175 self.filter.address = address.into().into();
176 self
177 }
178
179 /// Sets the 1st indexed topic.
180 pub fn topic1<T: Into<Topic>>(mut self, topic: T) -> Self {
181 self.filter.topics[1] = topic.into();
182 self
183 }
184
185 /// Sets the 2nd indexed topic.
186 pub fn topic2<T: Into<Topic>>(mut self, topic: T) -> Self {
187 self.filter.topics[2] = topic.into();
188 self
189 }
190
191 /// Sets the 3rd indexed topic.
192 pub fn topic3<T: Into<Topic>>(mut self, topic: T) -> Self {
193 self.filter.topics[3] = topic.into();
194 self
195 }
196}
197
198#[cfg(feature = "host")]
199mod host {
200 use super::*;
201 use crate::host::HostEvmEnv;
202 use anyhow::{Context, Result};
203 use revm::Database as RevmDatabase;
204 use std::error::Error as StdError;
205
206 impl<D, F: EvmFactory, C> Event<(), &mut HostEvmEnv<D, F, C>>
207 where
208 D: EvmDatabase + Send + 'static,
209 <D as RevmDatabase>::Error: StdError + Send + Sync + 'static,
210 {
211 /// Constructor for preflighting an event query for a specific EVM event.
212 ///
213 /// Initializes the environment for event queries, fetching necessary data via the
214 /// [Provider], and generating a storage proof for any accessed elements using
215 /// [EvmEnv::into_input].
216 ///
217 /// [EvmEnv::into_input]: crate::EvmEnv::into_input
218 /// [EvmEnv]: crate::EvmEnv
219 /// [Provider]: alloy::providers::Provider
220 pub fn preflight<S: SolEvent>(
221 env: &mut HostEvmEnv<D, F, C>,
222 ) -> Event<S, &mut HostEvmEnv<D, F, C>> {
223 Event {
224 filter: event_filter::<S, F::Header>(env.header()),
225 env,
226 phantom: PhantomData,
227 }
228 }
229 }
230
231 impl<S: SolEvent, D, F: EvmFactory, C> Event<S, &mut HostEvmEnv<D, F, C>>
232 where
233 D: EvmDatabase + Send + 'static,
234 <D as RevmDatabase>::Error: StdError + Send + Sync + 'static,
235 {
236 /// Executes the event query using an [EvmEnv] constructed with [Event::preflight].
237 ///
238 /// This uses [tokio::task::spawn_blocking] to run the blocking revm execution.
239 ///
240 /// [EvmEnv]: crate::EvmEnv
241 pub async fn query(self) -> Result<Vec<Log<S>>> {
242 log::info!("Executing preflight querying event '{}'", S::SIGNATURE);
243
244 let logs = self
245 .env
246 .spawn_with_db(move |db| db.logs(self.filter))
247 .await
248 .with_context(|| format!("querying logs for '{}' failed", S::SIGNATURE))?;
249 logs.iter().map(|log| Ok(S::decode_log(log)?)).collect()
250 }
251 }
252}
253
254/// Creates an event filter for a specific event and block header.
255fn event_filter<S: SolEvent, H: EvmBlockHeader>(header: &Sealed<H>) -> Filter {
256 assert!(!S::ANONYMOUS, "Anonymous events not supported");
257 Filter::new()
258 .event_signature(S::SIGNATURE_HASH)
259 .at_block_hash(header.seal())
260}
261
262/// Checks if a bloom filter matches the given filter parameters.
263#[inline]
264pub(super) fn matches_filter(bloom: Bloom, filter: &Filter) -> bool {
265 FilteredParams::matches_address(bloom, &FilteredParams::address_filter(&filter.address))
266 && FilteredParams::matches_topics(bloom, &FilteredParams::topics_filter(&filter.topics))
267}