aztec_pxe/sync/
note_service.rs

1//! Note service for note validation, storage, and nullifier synchronization.
2//!
3//! Ports the TS `NoteService` which manages note operations including
4//! retrieving notes, syncing nullifiers, and validating/storing new notes.
5
6use aztec_core::error::Error;
7use aztec_core::types::{AztecAddress, Fr};
8use aztec_node_client::AztecNode;
9
10use crate::stores::note_store::{NoteFilter, NoteStatus, StoredNote};
11use crate::stores::NoteStore;
12
13/// Maximum number of items per RPC request (matches TS MAX_RPC_LEN).
14const MAX_RPC_LEN: usize = 128;
15
16/// Service for note-related operations.
17pub struct NoteService<'a, N: AztecNode> {
18    node: &'a N,
19    note_store: &'a NoteStore,
20}
21
22impl<'a, N: AztecNode> NoteService<'a, N> {
23    pub fn new(node: &'a N, note_store: &'a NoteStore) -> Self {
24        Self { node, note_store }
25    }
26
27    /// Get notes from the store matching a filter.
28    pub async fn get_notes(&self, filter: &NoteFilter) -> Result<Vec<StoredNote>, Error> {
29        self.note_store.get_notes(filter).await
30    }
31
32    /// Sync note nullifiers for a contract.
33    ///
34    /// Fetches all active notes for a contract and checks if their nullifiers
35    /// have been included in the nullifier tree (i.e., the note was spent).
36    /// If so, marks the note as nullified.
37    ///
38    /// Batches queries for efficiency using MAX_RPC_LEN.
39    pub async fn sync_note_nullifiers(
40        &self,
41        contract_address: &AztecAddress,
42        scopes: &[AztecAddress],
43        anchor_block_number: u64,
44    ) -> Result<u64, Error> {
45        let filter = NoteFilter {
46            contract_address: Some(*contract_address),
47            status: NoteStatus::Active,
48            scopes: scopes.to_vec(),
49            ..Default::default()
50        };
51
52        let notes = self.note_store.get_notes(&filter).await?;
53        if notes.is_empty() {
54            return Ok(0);
55        }
56
57        let mut nullified_count = 0u64;
58
59        // Process in batches
60        for chunk in notes.chunks(MAX_RPC_LEN) {
61            let nullifiers: Vec<Fr> = chunk.iter().map(|n| n.siloed_nullifier).collect();
62
63            // Check nullifiers exist in tree using membership witness
64            let mut indexes: Vec<Option<u64>> = Vec::with_capacity(nullifiers.len());
65            for nullifier in &nullifiers {
66                let witness = self
67                    .node
68                    .get_nullifier_membership_witness(anchor_block_number, nullifier)
69                    .await?;
70                indexes.push(witness.map(|_| 0));
71            }
72
73            // Mark found nullifiers
74            let mut to_nullify = Vec::new();
75            for (i, maybe_index) in indexes.iter().enumerate() {
76                if maybe_index.is_some() {
77                    to_nullify.push((chunk[i].siloed_nullifier, 0u64));
78                    nullified_count += 1;
79                }
80            }
81
82            if !to_nullify.is_empty() {
83                self.note_store.apply_nullifiers(&to_nullify).await?;
84            }
85        }
86
87        if nullified_count > 0 {
88            tracing::debug!(
89                contract = %contract_address,
90                nullified = nullified_count,
91                "synced note nullifiers"
92            );
93        }
94
95        Ok(nullified_count)
96    }
97
98    /// Validate and store a note.
99    ///
100    /// Validates:
101    /// 1. Note hash exists in the note hash tree
102    /// 2. Note is not already nullified
103    /// 3. Computes siloed hash and nullifier
104    ///
105    /// Then stores the note in the NoteStore.
106    pub async fn validate_and_store_note(
107        &self,
108        note: &StoredNote,
109        scope: &AztecAddress,
110    ) -> Result<(), Error> {
111        // The Noir `sync_state` already validated the note client-side
112        // (decrypted the log, computed the note hash, matched it against
113        // unique note hashes in the tx). We trust the validation request.
114
115        // Check if already nullified
116        let nullifier_witness = self
117            .node
118            .get_nullifier_membership_witness(0, &note.siloed_nullifier)
119            .await?;
120
121        let mut stored = note.clone();
122        if nullifier_witness.is_some() {
123            stored.nullified = true;
124            stored.nullification_block_number = Some(0); // Unknown exact block
125        }
126
127        // Store the note
128        self.note_store.add_notes(&[stored], scope).await?;
129
130        Ok(())
131    }
132}