Skip to main content

miden_client/note/
note_update_tracker.rs

1use alloc::collections::BTreeMap;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::block::BlockHeader;
5use miden_protocol::note::{NoteId, NoteInclusionProof, Nullifier};
6
7use crate::ClientError;
8use crate::rpc::RpcError;
9use crate::rpc::domain::note::CommittedNote;
10use crate::rpc::domain::nullifier::NullifierUpdate;
11use crate::store::{InputNoteRecord, OutputNoteRecord};
12use crate::transaction::{TransactionRecord, TransactionStatus};
13
14// NOTE UPDATE
15// ================================================================================================
16
17/// Represents the possible types of updates that can be applied to a note in a
18/// [`NoteUpdateTracker`].
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum NoteUpdateType {
21    /// Indicates that the note was already tracked but it was not updated.
22    None,
23    /// Indicates that the note is new and should be inserted in the store.
24    Insert,
25    /// Indicates that the note was already tracked and should be updated.
26    Update,
27}
28
29/// Represents the possible states of an input note record in a [`NoteUpdateTracker`].
30#[derive(Clone, Debug)]
31pub struct InputNoteUpdate {
32    /// Input note being updated.
33    note: InputNoteRecord,
34    /// Type of the note update.
35    update_type: NoteUpdateType,
36}
37
38impl InputNoteUpdate {
39    /// Creates a new [`InputNoteUpdate`] with the provided note with a `None` update type.
40    fn new_none(note: InputNoteRecord) -> Self {
41        Self { note, update_type: NoteUpdateType::None }
42    }
43
44    /// Creates a new [`InputNoteUpdate`] with the provided note with an `Insert` update type.
45    fn new_insert(note: InputNoteRecord) -> Self {
46        Self {
47            note,
48            update_type: NoteUpdateType::Insert,
49        }
50    }
51
52    /// Creates a new [`InputNoteUpdate`] with the provided note with an `Update` update type.
53    fn new_update(note: InputNoteRecord) -> Self {
54        Self {
55            note,
56            update_type: NoteUpdateType::Update,
57        }
58    }
59
60    /// Returns a reference the inner note record.
61    pub fn inner(&self) -> &InputNoteRecord {
62        &self.note
63    }
64
65    /// Returns a mutable reference to the inner note record. If the update type is `None` or
66    /// `Update`, it will be set to `Update`.
67    fn inner_mut(&mut self) -> &mut InputNoteRecord {
68        self.update_type = match self.update_type {
69            NoteUpdateType::None | NoteUpdateType::Update => NoteUpdateType::Update,
70            NoteUpdateType::Insert => NoteUpdateType::Insert,
71        };
72
73        &mut self.note
74    }
75
76    /// Returns the type of the note update.
77    pub fn update_type(&self) -> &NoteUpdateType {
78        &self.update_type
79    }
80
81    /// Returns the identifier of the inner note.
82    pub fn id(&self) -> NoteId {
83        self.note.id()
84    }
85
86    /// Returns the per-account position of the consuming transaction within the account's
87    /// execution chain for the block. `None` for non-consumed notes or when the order has not
88    /// been determined yet.
89    pub fn consumed_tx_order(&self) -> Option<u32> {
90        self.note.state().consumed_tx_order()
91    }
92}
93
94/// Represents the possible states of an output note record in a [`NoteUpdateTracker`].
95#[derive(Clone, Debug)]
96pub struct OutputNoteUpdate {
97    /// Output note being updated.
98    note: OutputNoteRecord,
99    /// Type of the note update.
100    update_type: NoteUpdateType,
101}
102
103impl OutputNoteUpdate {
104    /// Creates a new [`OutputNoteUpdate`] with the provided note with a `None` update type.
105    fn new_none(note: OutputNoteRecord) -> Self {
106        Self { note, update_type: NoteUpdateType::None }
107    }
108
109    /// Creates a new [`OutputNoteUpdate`] with the provided note with an `Insert` update type.
110    fn new_insert(note: OutputNoteRecord) -> Self {
111        Self {
112            note,
113            update_type: NoteUpdateType::Insert,
114        }
115    }
116
117    /// Creates a new [`OutputNoteUpdate`] with the provided note with an `Update` update type.
118    fn new_update(note: OutputNoteRecord) -> Self {
119        Self {
120            note,
121            update_type: NoteUpdateType::Update,
122        }
123    }
124
125    /// Returns a reference the inner note record.
126    pub fn inner(&self) -> &OutputNoteRecord {
127        &self.note
128    }
129
130    /// Returns a mutable reference to the inner note record. If the update type is `None` or
131    /// `Update`, it will be set to `Update`.
132    fn inner_mut(&mut self) -> &mut OutputNoteRecord {
133        self.update_type = match self.update_type {
134            NoteUpdateType::None | NoteUpdateType::Update => NoteUpdateType::Update,
135            NoteUpdateType::Insert => NoteUpdateType::Insert,
136        };
137
138        &mut self.note
139    }
140
141    /// Returns the type of the note update.
142    pub fn update_type(&self) -> &NoteUpdateType {
143        &self.update_type
144    }
145
146    /// Returns the identifier of the inner note.
147    pub fn id(&self) -> NoteId {
148        self.note.id()
149    }
150}
151
152// NOTE UPDATE TRACKER
153// ================================================================================================
154
155/// Contains note changes to apply to the store.
156///
157/// This includes new notes that have been created and existing notes that have been updated. The
158/// tracker also lets state changes be applied to the contained notes, this allows for already
159/// updated notes to be further updated as new information is received.
160#[derive(Clone, Debug, Default)]
161pub struct NoteUpdateTracker {
162    /// A map of new and updated input note records to be upserted in the store.
163    input_notes: BTreeMap<NoteId, InputNoteUpdate>,
164    /// A map of updated output note records to be upserted in the store.
165    output_notes: BTreeMap<NoteId, OutputNoteUpdate>,
166    /// Fast lookup map from nullifier to input note id.
167    input_notes_by_nullifier: BTreeMap<Nullifier, NoteId>,
168    /// Fast lookup map from nullifier to output note id.
169    output_notes_by_nullifier: BTreeMap<Nullifier, NoteId>,
170    /// Map from nullifier to its per-account position in the consuming transaction order.
171    /// Nullifiers from the same account are in execution order; ordering across different
172    /// accounts is not guaranteed.
173    nullifier_order: BTreeMap<Nullifier, u32>,
174}
175
176impl NoteUpdateTracker {
177    /// Creates a [`NoteUpdateTracker`] with already-tracked notes.
178    pub fn new(
179        input_notes: impl IntoIterator<Item = InputNoteRecord>,
180        output_notes: impl IntoIterator<Item = OutputNoteRecord>,
181    ) -> Self {
182        let mut tracker = Self::default();
183        for note in input_notes {
184            tracker.insert_input_note(note, NoteUpdateType::None);
185        }
186        for note in output_notes {
187            tracker.insert_output_note(note, NoteUpdateType::None);
188        }
189
190        tracker
191    }
192
193    /// Creates a [`NoteUpdateTracker`] for updates related to transactions.
194    ///
195    /// A transaction can:
196    ///
197    /// - Create input notes
198    /// - Update existing input notes (by consuming them)
199    /// - Create output notes
200    pub fn for_transaction_updates(
201        new_input_notes: impl IntoIterator<Item = InputNoteRecord>,
202        updated_input_notes: impl IntoIterator<Item = InputNoteRecord>,
203        new_output_notes: impl IntoIterator<Item = OutputNoteRecord>,
204    ) -> Self {
205        let mut tracker = Self::default();
206
207        for note in new_input_notes {
208            tracker.insert_input_note(note, NoteUpdateType::Insert);
209        }
210
211        for note in updated_input_notes {
212            tracker.insert_input_note(note, NoteUpdateType::Update);
213        }
214
215        for note in new_output_notes {
216            tracker.insert_output_note(note, NoteUpdateType::Insert);
217        }
218
219        tracker
220    }
221
222    // GETTERS
223    // --------------------------------------------------------------------------------------------
224
225    /// Returns all input note records that have been updated.
226    ///
227    /// This may include:
228    /// - New notes that have been created that should be inserted.
229    /// - Existing tracked notes that should be updated.
230    pub fn updated_input_notes(&self) -> impl Iterator<Item = &InputNoteUpdate> {
231        self.input_notes.values().filter(|note| {
232            matches!(note.update_type, NoteUpdateType::Insert | NoteUpdateType::Update)
233        })
234    }
235
236    /// Returns all output note records that have been updated.
237    ///
238    /// This may include:
239    /// - New notes that have been created that should be inserted.
240    /// - Existing tracked notes that should be updated.
241    pub fn updated_output_notes(&self) -> impl Iterator<Item = &OutputNoteUpdate> {
242        self.output_notes.values().filter(|note| {
243            matches!(note.update_type, NoteUpdateType::Insert | NoteUpdateType::Update)
244        })
245    }
246
247    /// Returns whether no new note-related information has been retrieved.
248    pub fn is_empty(&self) -> bool {
249        self.input_notes.is_empty() && self.output_notes.is_empty()
250    }
251
252    /// Returns input and output note unspent nullifiers.
253    pub fn unspent_nullifiers(&self) -> impl Iterator<Item = Nullifier> {
254        let input_note_unspent_nullifiers = self
255            .input_notes
256            .values()
257            .filter(|note| !note.inner().is_consumed())
258            .map(|note| note.inner().nullifier());
259
260        let output_note_unspent_nullifiers = self
261            .output_notes
262            .values()
263            .filter(|note| !note.inner().is_consumed())
264            .filter_map(|note| note.inner().nullifier());
265
266        input_note_unspent_nullifiers.chain(output_note_unspent_nullifiers)
267    }
268
269    /// Appends nullifiers to the per-account ordered nullifier list.
270    ///
271    /// Nullifiers from the same account must be in execution order; ordering across different
272    /// accounts is not guaranteed.
273    pub fn extend_nullifiers(&mut self, nullifiers: impl IntoIterator<Item = Nullifier>) {
274        for nullifier in nullifiers {
275            let next_pos =
276                u32::try_from(self.nullifier_order.len()).expect("nullifier count exceeds u32");
277            self.nullifier_order.entry(nullifier).or_insert(next_pos);
278        }
279    }
280
281    // UPDATE METHODS
282    // --------------------------------------------------------------------------------------------
283
284    /// Inserts the new public note data into the tracker. This method doesn't check the relevance
285    /// of the note, so it should only be used for notes that are guaranteed to be relevant to the
286    /// client.
287    pub(crate) fn apply_new_public_note(
288        &mut self,
289        mut public_note_data: InputNoteRecord,
290        block_header: &BlockHeader,
291    ) -> Result<(), ClientError> {
292        public_note_data.block_header_received(block_header)?;
293        self.insert_input_note(public_note_data, NoteUpdateType::Insert);
294
295        Ok(())
296    }
297
298    /// Applies the necessary state transitions to the [`NoteUpdateTracker`] when a note is
299    /// committed in a block and returns whether the committed note is tracked as input note.
300    pub(crate) fn apply_committed_note_state_transitions(
301        &mut self,
302        committed_note: &CommittedNote,
303        block_header: &BlockHeader,
304    ) -> Result<bool, ClientError> {
305        let inclusion_proof = committed_note.inclusion_proof().clone();
306
307        let is_tracked_as_input_note =
308            if let Some(input_note_record) = self.get_input_note_by_id(*committed_note.note_id()) {
309                let metadata = committed_note.metadata().cloned().ok_or_else(|| {
310                    ClientError::RpcError(RpcError::ExpectedDataMissing(format!(
311                        "full metadata for committed note {}",
312                        committed_note.note_id()
313                    )))
314                })?;
315                input_note_record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
316                input_note_record.block_header_received(block_header)?;
317
318                true
319            } else {
320                false
321            };
322
323        self.try_commit_output_note(*committed_note.note_id(), inclusion_proof)?;
324
325        Ok(is_tracked_as_input_note)
326    }
327
328    /// Applies inclusion proofs from the transaction sync response to tracked output notes.
329    ///
330    /// This transitions output notes from `Expected` to `Committed` state using the
331    /// inclusion proofs returned by `SyncTransactions`.
332    pub(crate) fn apply_output_note_inclusion_proofs(
333        &mut self,
334        committed_notes: &[CommittedNote],
335    ) -> Result<(), ClientError> {
336        for committed_note in committed_notes {
337            self.try_commit_output_note(
338                *committed_note.note_id(),
339                committed_note.inclusion_proof().clone(),
340            )?;
341        }
342        Ok(())
343    }
344
345    /// If the note is tracked as an output note, transitions it to `Committed` with the
346    /// given inclusion proof. No-op if the note is not tracked.
347    fn try_commit_output_note(
348        &mut self,
349        note_id: NoteId,
350        inclusion_proof: NoteInclusionProof,
351    ) -> Result<(), ClientError> {
352        if let Some(output_note) = self.get_output_note_by_id(note_id) {
353            output_note.inclusion_proof_received(inclusion_proof)?;
354        }
355        Ok(())
356    }
357
358    /// Applies the necessary state transitions to the [`NoteUpdateTracker`] when a note is
359    /// nullified in a block.
360    ///
361    /// For input note records two possible scenarios are considered:
362    /// 1. The note was being processed by a local transaction that just got committed.
363    /// 2. The note was consumed by a transaction not submitted by this client. This includes
364    ///    consumption by untracked accounts as well as consumption by tracked accounts whose
365    ///    transactions were submitted by other client instances. If a local transaction was
366    ///    processing the note and it didn't get committed, the transaction should be discarded.
367    pub(crate) fn apply_nullifiers_state_transitions<'a>(
368        &mut self,
369        nullifier_update: &NullifierUpdate,
370        mut committed_transactions: impl Iterator<Item = &'a TransactionRecord>,
371        external_consumer_account: Option<AccountId>,
372    ) -> Result<(), ClientError> {
373        let order = self.get_nullifier_order(nullifier_update.nullifier);
374
375        if let Some(input_note_update) =
376            self.get_input_note_update_by_nullifier(nullifier_update.nullifier)
377        {
378            if let Some(consumer_transaction) = committed_transactions
379                .find(|t| input_note_update.inner().consumer_transaction_id() == Some(&t.id))
380            {
381                // The note was being processed by a local transaction that just got committed
382                if let TransactionStatus::Committed { block_number, .. } =
383                    consumer_transaction.status
384                {
385                    input_note_update
386                        .inner_mut()
387                        .transaction_committed(consumer_transaction.id, block_number)?;
388                }
389            } else {
390                // The note was consumed by a transaction not submitted by this client.
391                // If the consuming account is tracked, external_consumer_account will be Some.
392                input_note_update.inner_mut().consumed_externally(
393                    nullifier_update.nullifier,
394                    nullifier_update.block_num,
395                    external_consumer_account,
396                )?;
397            }
398            input_note_update.inner_mut().set_consumed_tx_order(order);
399        }
400
401        if let Some(output_note_record) =
402            self.get_output_note_by_nullifier(nullifier_update.nullifier)
403        {
404            output_note_record
405                .nullifier_received(nullifier_update.nullifier, nullifier_update.block_num)?;
406        }
407
408        Ok(())
409    }
410
411    // PRIVATE HELPERS
412    // --------------------------------------------------------------------------------------------
413
414    /// Returns the position of the given nullifier in the consuming transaction order, or `None`
415    /// if it is not present.
416    fn get_nullifier_order(&self, nullifier: Nullifier) -> Option<u32> {
417        self.nullifier_order.get(&nullifier).copied()
418    }
419
420    /// Returns a mutable reference to the input note record with the provided ID if it exists.
421    fn get_input_note_by_id(&mut self, note_id: NoteId) -> Option<&mut InputNoteRecord> {
422        self.input_notes.get_mut(&note_id).map(InputNoteUpdate::inner_mut)
423    }
424
425    /// Returns a mutable reference to the output note record with the provided ID if it exists.
426    fn get_output_note_by_id(&mut self, note_id: NoteId) -> Option<&mut OutputNoteRecord> {
427        self.output_notes.get_mut(&note_id).map(OutputNoteUpdate::inner_mut)
428    }
429
430    /// Returns a mutable reference to the input note update with the provided nullifier if it
431    /// exists.
432    fn get_input_note_update_by_nullifier(
433        &mut self,
434        nullifier: Nullifier,
435    ) -> Option<&mut InputNoteUpdate> {
436        let note_id = self.input_notes_by_nullifier.get(&nullifier).copied()?;
437        self.input_notes.get_mut(&note_id)
438    }
439
440    /// Returns a mutable reference to the output note record with the provided nullifier if it
441    /// exists.
442    fn get_output_note_by_nullifier(
443        &mut self,
444        nullifier: Nullifier,
445    ) -> Option<&mut OutputNoteRecord> {
446        let note_id = self.output_notes_by_nullifier.get(&nullifier).copied()?;
447        self.output_notes.get_mut(&note_id).map(OutputNoteUpdate::inner_mut)
448    }
449
450    /// Insert an input note update
451    fn insert_input_note(&mut self, note: InputNoteRecord, update_type: NoteUpdateType) {
452        let note_id = note.id();
453        let nullifier = note.nullifier();
454        self.input_notes_by_nullifier.insert(nullifier, note_id);
455        let update = match update_type {
456            NoteUpdateType::None => InputNoteUpdate::new_none(note),
457            NoteUpdateType::Insert => InputNoteUpdate::new_insert(note),
458            NoteUpdateType::Update => InputNoteUpdate::new_update(note),
459        };
460        self.input_notes.insert(note_id, update);
461    }
462
463    /// Insert an output note update
464    fn insert_output_note(&mut self, note: OutputNoteRecord, update_type: NoteUpdateType) {
465        let note_id = note.id();
466        if let Some(nullifier) = note.nullifier() {
467            self.output_notes_by_nullifier.insert(nullifier, note_id);
468        }
469        let update = match update_type {
470            NoteUpdateType::None => OutputNoteUpdate::new_none(note),
471            NoteUpdateType::Insert => OutputNoteUpdate::new_insert(note),
472            NoteUpdateType::Update => OutputNoteUpdate::new_update(note),
473        };
474        self.output_notes.insert(note_id, update);
475    }
476}