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(&ETH_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(&ETH_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(&ETH_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}