Skip to main content

miden_client/note/
note_update_tracker.rs

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