aztec_pxe/sync/
event_service.rs

1//! Event service for validating and storing private events.
2//!
3//! Ports the TS `EventService` which validates that events exist in
4//! transaction effects (via siloed event commitment as nullifier)
5//! and stores them in the PrivateEventStore.
6
7use aztec_core::error::Error;
8use aztec_core::tx::TxHash;
9use aztec_core::types::{AztecAddress, Fr};
10use aztec_node_client::AztecNode;
11
12use crate::stores::private_event_store::{PrivateEventQueryFilter, StoredPrivateEvent};
13use crate::stores::{AnchorBlockStore, PrivateEventStore};
14
15use aztec_core::abi::EventSelector;
16
17/// Service for validating and storing private events.
18///
19/// Matches the TS `EventService` — validates that events exist in the
20/// transaction's nullifier set (via siloed event commitment) and stores
21/// them in the PrivateEventStore with proper metadata.
22pub struct EventService<'a, N: AztecNode> {
23    node: &'a N,
24    private_event_store: &'a PrivateEventStore,
25    anchor_block_store: &'a AnchorBlockStore,
26}
27
28impl<'a, N: AztecNode> EventService<'a, N> {
29    pub fn new(
30        node: &'a N,
31        private_event_store: &'a PrivateEventStore,
32        anchor_block_store: &'a AnchorBlockStore,
33    ) -> Self {
34        Self {
35            node,
36            private_event_store,
37            anchor_block_store,
38        }
39    }
40
41    /// Validate and store a private event.
42    ///
43    /// Validates:
44    /// 1. The tx effect exists and is at or before the anchor block
45    /// 2. The siloed event commitment is present as a nullifier in the tx
46    ///
47    /// Then stores the event in the PrivateEventStore with full metadata
48    /// (block hash, tx index in block, event index in tx).
49    pub async fn validate_and_store_event(
50        &self,
51        contract_address: &AztecAddress,
52        selector: &EventSelector,
53        randomness: Fr,
54        content: Vec<Fr>,
55        event_commitment: Fr,
56        tx_hash: TxHash,
57        scope: &AztecAddress,
58    ) -> Result<(), Error> {
59        // Compute the siloed event commitment
60        let siloed_event_commitment = aztec_core::hash::poseidon2_hash_with_separator(
61            &[Fr::from(*contract_address), event_commitment],
62            0,
63        );
64
65        // Get the anchor block number to verify the tx is within range
66        let anchor_block_number = self.anchor_block_store.get_block_number().await?;
67
68        // Get the tx effect to validate inclusion and extract metadata.
69        // Upstream uses getTxEffect() to verify the siloed event commitment
70        // is present in the tx nullifiers and to extract positional metadata.
71        let tx_effect =
72            self.node.get_tx_effect(&tx_hash).await?.ok_or_else(|| {
73                Error::InvalidData(format!("tx effect not found for tx {tx_hash}"))
74            })?;
75
76        // Extract block number from the tx effect
77        let block_number = tx_effect
78            .pointer("/l2BlockNumber")
79            .or_else(|| tx_effect.get("l2BlockNumber"))
80            .and_then(|v| v.as_u64())
81            .unwrap_or(0);
82
83        if block_number > anchor_block_number && anchor_block_number > 0 {
84            return Err(Error::InvalidData(format!(
85                "tx {} is in block {block_number} which is after anchor block {anchor_block_number}",
86                tx_hash
87            )));
88        }
89
90        // Extract block hash
91        let l2_block_hash = tx_effect
92            .pointer("/l2BlockHash")
93            .or_else(|| tx_effect.get("l2BlockHash"))
94            .and_then(|v| v.as_str())
95            .unwrap_or("0x0")
96            .to_owned();
97
98        // Extract positional metadata
99        let tx_index_in_block = tx_effect
100            .pointer("/txIndexInBlock")
101            .or_else(|| tx_effect.get("txIndexInBlock"))
102            .and_then(|v| v.as_u64());
103
104        // Validate the siloed event commitment is present as a nullifier
105        let nullifiers = tx_effect
106            .pointer("/nullifiers")
107            .or_else(|| tx_effect.get("nullifiers"))
108            .and_then(|v| v.as_array());
109
110        if let Some(nullifiers) = nullifiers {
111            let commitment_hex = format!("{siloed_event_commitment}");
112            let found = nullifiers
113                .iter()
114                .any(|n| n.as_str().map_or(false, |s| s == commitment_hex));
115            if !found {
116                return Err(Error::InvalidData(format!(
117                    "siloed event commitment {commitment_hex} not found in tx {tx_hash} nullifiers"
118                )));
119            }
120        }
121
122        // Derive event index from nullifier position
123        let event_index_in_tx = nullifiers.and_then(|nullifiers| {
124            let commitment_hex = format!("{siloed_event_commitment}");
125            nullifiers
126                .iter()
127                .position(|n| n.as_str().map_or(false, |s| s == commitment_hex))
128                .map(|i| i as u64)
129        });
130
131        // Store the event
132        let event = StoredPrivateEvent {
133            event_selector: *selector,
134            randomness,
135            msg_content: content,
136            siloed_event_commitment,
137            contract_address: *contract_address,
138            scopes: vec![],
139            tx_hash,
140            l2_block_number: block_number,
141            l2_block_hash,
142            tx_index_in_block,
143            event_index_in_tx,
144        };
145
146        self.private_event_store
147            .store_private_event_log(&event, scope)
148            .await?;
149
150        tracing::debug!(
151            contract = %contract_address,
152            event_selector = %selector.0,
153            block = block_number,
154            "stored private event"
155        );
156
157        Ok(())
158    }
159
160    /// Get private events for a contract and event selector.
161    pub async fn get_private_events(
162        &self,
163        event_selector: &EventSelector,
164        filter: &PrivateEventQueryFilter,
165    ) -> Result<Vec<StoredPrivateEvent>, Error> {
166        self.private_event_store
167            .get_private_events(event_selector, filter)
168            .await
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::stores::InMemoryKvStore;
176    use std::sync::Arc;
177
178    // Minimal mock node for event service tests
179    struct MockNode;
180
181    #[async_trait::async_trait]
182    impl AztecNode for MockNode {
183        async fn get_node_info(&self) -> Result<aztec_node_client::NodeInfo, Error> {
184            Ok(aztec_node_client::NodeInfo {
185                node_version: "mock".into(),
186                l1_chain_id: 1,
187                rollup_version: 1,
188                enr: None,
189                l1_contract_addresses: serde_json::Value::Null,
190                protocol_contract_addresses: serde_json::Value::Null,
191                real_proofs: false,
192                l2_circuits_vk_tree_root: None,
193                l2_protocol_contracts_hash: None,
194            })
195        }
196        async fn get_block_number(&self) -> Result<u64, Error> {
197            Ok(5)
198        }
199        async fn get_proven_block_number(&self) -> Result<u64, Error> {
200            Ok(5)
201        }
202        async fn get_tx_receipt(
203            &self,
204            _tx_hash: &TxHash,
205        ) -> Result<aztec_core::tx::TxReceipt, Error> {
206            Ok(aztec_core::tx::TxReceipt {
207                tx_hash: TxHash::zero(),
208                status: aztec_core::tx::TxStatus::Proposed,
209                execution_result: Some(aztec_core::tx::TxExecutionResult::Success),
210                error: None,
211                transaction_fee: None,
212                block_hash: None,
213                block_number: Some(3),
214                epoch_number: None,
215            })
216        }
217        async fn get_tx_effect(
218            &self,
219            _tx_hash: &TxHash,
220        ) -> Result<Option<serde_json::Value>, Error> {
221            // Return a tx effect with block metadata.
222            // Nullifiers field is omitted so validation is skipped in this basic mock.
223            // Tests that need nullifier validation should use a richer mock.
224            Ok(Some(serde_json::json!({
225                "l2BlockNumber": 3,
226                "l2BlockHash": "0x0000000000000000000000000000000000000000000000000000000000000003",
227                "txIndexInBlock": 0
228            })))
229        }
230        async fn get_tx_by_hash(
231            &self,
232            _tx_hash: &TxHash,
233        ) -> Result<Option<serde_json::Value>, Error> {
234            Ok(None)
235        }
236        async fn get_public_logs(
237            &self,
238            _: aztec_node_client::PublicLogFilter,
239        ) -> Result<aztec_node_client::PublicLogsResponse, Error> {
240            Ok(aztec_node_client::PublicLogsResponse {
241                logs: vec![],
242                max_logs_hit: false,
243            })
244        }
245        async fn send_tx(&self, _: &serde_json::Value) -> Result<(), Error> {
246            Ok(())
247        }
248        async fn get_contract(
249            &self,
250            _: &AztecAddress,
251        ) -> Result<Option<aztec_core::types::ContractInstanceWithAddress>, Error> {
252            Ok(None)
253        }
254        async fn get_contract_class(&self, _: &Fr) -> Result<Option<serde_json::Value>, Error> {
255            Ok(None)
256        }
257        async fn get_block_header(&self, _: u64) -> Result<serde_json::Value, Error> {
258            Ok(serde_json::json!({}))
259        }
260        async fn get_block(&self, _: u64) -> Result<Option<serde_json::Value>, Error> {
261            Ok(None)
262        }
263        async fn get_note_hash_membership_witness(
264            &self,
265            _: u64,
266            _: &Fr,
267        ) -> Result<Option<serde_json::Value>, Error> {
268            Ok(None)
269        }
270        async fn get_nullifier_membership_witness(
271            &self,
272            _: u64,
273            _: &Fr,
274        ) -> Result<Option<serde_json::Value>, Error> {
275            Ok(None)
276        }
277        async fn get_low_nullifier_membership_witness(
278            &self,
279            _: u64,
280            _: &Fr,
281        ) -> Result<Option<serde_json::Value>, Error> {
282            Ok(None)
283        }
284        async fn get_public_storage_at(
285            &self,
286            _: u64,
287            _: &AztecAddress,
288            _: &Fr,
289        ) -> Result<Fr, Error> {
290            Ok(Fr::zero())
291        }
292        async fn get_public_data_witness(
293            &self,
294            _: u64,
295            _: &Fr,
296        ) -> Result<Option<serde_json::Value>, Error> {
297            Ok(None)
298        }
299        async fn get_l1_to_l2_message_membership_witness(
300            &self,
301            _: u64,
302            _: &Fr,
303        ) -> Result<Option<serde_json::Value>, Error> {
304            Ok(None)
305        }
306        async fn simulate_public_calls(
307            &self,
308            _: &serde_json::Value,
309            _: bool,
310        ) -> Result<serde_json::Value, Error> {
311            Ok(serde_json::Value::Null)
312        }
313        async fn is_valid_tx(
314            &self,
315            _: &serde_json::Value,
316        ) -> Result<aztec_node_client::TxValidationResult, Error> {
317            Ok(aztec_node_client::TxValidationResult::Valid)
318        }
319        async fn get_private_logs_by_tags(&self, _: &[Fr]) -> Result<serde_json::Value, Error> {
320            Ok(serde_json::json!([]))
321        }
322        async fn get_public_logs_by_tags_from_contract(
323            &self,
324            _: &AztecAddress,
325            _: &[Fr],
326        ) -> Result<serde_json::Value, Error> {
327            Ok(serde_json::json!([]))
328        }
329        async fn register_contract_function_signatures(&self, _: &[String]) -> Result<(), Error> {
330            Ok(())
331        }
332        async fn get_block_hash_membership_witness(
333            &self,
334            _: u64,
335            _: &Fr,
336        ) -> Result<Option<serde_json::Value>, Error> {
337            Ok(None)
338        }
339        async fn find_leaves_indexes(
340            &self,
341            _: u64,
342            _: &str,
343            _: &[Fr],
344        ) -> Result<Vec<Option<u64>>, Error> {
345            Ok(vec![])
346        }
347    }
348
349    #[tokio::test]
350    async fn validate_and_store_event_stores_correctly() {
351        let kv: Arc<dyn crate::stores::kv::KvStore> = Arc::new(InMemoryKvStore::new());
352        let event_store = PrivateEventStore::new(Arc::clone(&kv));
353        let anchor_store = AnchorBlockStore::new(Arc::clone(&kv));
354
355        // Set anchor to block 5
356        let anchor = crate::stores::anchor_block_store::AnchorBlockHeader::from_header_json(
357            serde_json::json!({"globalVariables": {"blockNumber": 5}}),
358        );
359        anchor_store.set_header(&anchor).await.unwrap();
360
361        let node = MockNode;
362        let service = EventService::new(&node, &event_store, &anchor_store);
363
364        let contract = AztecAddress::from(1u64);
365        let selector = EventSelector(Fr::from(0x12345678u64));
366        let scope = AztecAddress::from(99u64);
367        let tx_hash = TxHash::zero();
368
369        service
370            .validate_and_store_event(
371                &contract,
372                &selector,
373                Fr::from(1u64),
374                vec![Fr::from(10u64)],
375                Fr::from(100u64),
376                tx_hash,
377                &scope,
378            )
379            .await
380            .unwrap();
381
382        let events = event_store
383            .get_private_events(
384                &selector,
385                &PrivateEventQueryFilter {
386                    contract_address: contract,
387                    from_block: None,
388                    to_block: None,
389                    scopes: vec![scope],
390                    tx_hash: None,
391                },
392            )
393            .await
394            .unwrap();
395        assert_eq!(events.len(), 1);
396        assert_eq!(events[0].l2_block_number, 3);
397    }
398}