1use alloc::collections::BTreeMap;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::block::{BlockHeader, BlockNumber};
5use miden_protocol::note::{
6 Note,
7 NoteAttachments,
8 NoteDetailsCommitment,
9 NoteHeader,
10 NoteId,
11 NoteInclusionProof,
12 NoteMetadata,
13 Nullifier,
14};
15use miden_standards::note::NetworkAccountTarget;
16use miden_tx::utils::serde::{
17 ByteReader,
18 ByteWriter,
19 Deserializable,
20 DeserializationError,
21 Serializable,
22};
23
24use crate::ClientError;
25use crate::rpc::domain::note::CommittedNote;
26use crate::store::{InputNoteRecord, OutputNoteRecord};
27use crate::transaction::{TransactionRecord, TransactionStatus};
28
29pub struct NoteConsumption {
34 pub nullifier: Nullifier,
36 pub block_num: BlockNumber,
38 pub external_consumer: Option<AccountId>,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50#[repr(u8)]
51pub enum NoteUpdateType {
52 None = 0,
54 Insert = 1,
56 Update = 2,
58 InsertCommitted = 3,
63}
64
65impl NoteUpdateType {
66 pub fn is_modified(self) -> bool {
70 matches!(self, Self::Insert | Self::Update | Self::InsertCommitted)
71 }
72}
73
74impl TryFrom<u8> for NoteUpdateType {
75 type Error = u8;
76
77 fn try_from(value: u8) -> Result<Self, Self::Error> {
78 match value {
79 0 => Ok(NoteUpdateType::None),
80 1 => Ok(NoteUpdateType::Insert),
81 2 => Ok(NoteUpdateType::Update),
82 3 => Ok(NoteUpdateType::InsertCommitted),
83 other => Err(other),
84 }
85 }
86}
87
88#[derive(Clone, Debug, PartialEq)]
90pub struct InputNoteUpdate {
91 note: InputNoteRecord,
93 update_type: NoteUpdateType,
95}
96
97impl InputNoteUpdate {
98 fn new_none(note: InputNoteRecord) -> Self {
100 Self { note, update_type: NoteUpdateType::None }
101 }
102
103 fn new_insert(note: InputNoteRecord) -> Self {
105 Self {
106 note,
107 update_type: NoteUpdateType::Insert,
108 }
109 }
110
111 fn new_update(note: InputNoteRecord) -> Self {
113 Self {
114 note,
115 update_type: NoteUpdateType::Update,
116 }
117 }
118
119 fn new_insert_committed(note: InputNoteRecord) -> Self {
122 Self {
123 note,
124 update_type: NoteUpdateType::InsertCommitted,
125 }
126 }
127
128 pub fn inner(&self) -> &InputNoteRecord {
130 &self.note
131 }
132
133 fn inner_mut(&mut self) -> &mut InputNoteRecord {
136 self.update_type = match self.update_type {
137 NoteUpdateType::None | NoteUpdateType::Update => NoteUpdateType::Update,
138 NoteUpdateType::Insert => NoteUpdateType::Insert,
139 NoteUpdateType::InsertCommitted => NoteUpdateType::InsertCommitted,
140 };
141
142 &mut self.note
143 }
144
145 pub fn update_type(&self) -> &NoteUpdateType {
147 &self.update_type
148 }
149
150 pub fn id(&self) -> Option<NoteId> {
153 self.note.id()
154 }
155
156 pub fn consumed_tx_order(&self) -> Option<u32> {
160 self.note.state().consumed_tx_order()
161 }
162}
163
164#[derive(Clone, Debug, PartialEq)]
166pub struct OutputNoteUpdate {
167 note: OutputNoteRecord,
169 update_type: NoteUpdateType,
171}
172
173impl OutputNoteUpdate {
174 fn new_none(note: OutputNoteRecord) -> Self {
176 Self { note, update_type: NoteUpdateType::None }
177 }
178
179 fn new_insert(note: OutputNoteRecord) -> Self {
181 Self {
182 note,
183 update_type: NoteUpdateType::Insert,
184 }
185 }
186
187 fn new_update(note: OutputNoteRecord) -> Self {
189 Self {
190 note,
191 update_type: NoteUpdateType::Update,
192 }
193 }
194
195 pub fn inner(&self) -> &OutputNoteRecord {
197 &self.note
198 }
199
200 fn inner_mut(&mut self) -> &mut OutputNoteRecord {
203 self.update_type = match self.update_type {
204 NoteUpdateType::None | NoteUpdateType::Update => NoteUpdateType::Update,
205 NoteUpdateType::Insert | NoteUpdateType::InsertCommitted => NoteUpdateType::Insert,
208 };
209
210 &mut self.note
211 }
212
213 pub fn update_type(&self) -> &NoteUpdateType {
215 &self.update_type
216 }
217
218 pub fn id(&self) -> NoteId {
220 self.note.id()
221 }
222}
223
224#[derive(Clone, Debug, Default, PartialEq)]
233pub struct NoteUpdateTracker {
234 input_notes: BTreeMap<NoteDetailsCommitment, InputNoteUpdate>,
239 output_notes: BTreeMap<NoteId, OutputNoteUpdate>,
241 input_notes_by_nullifier: BTreeMap<Nullifier, NoteDetailsCommitment>,
244 input_notes_by_id: BTreeMap<NoteId, NoteDetailsCommitment>,
248 output_notes_by_nullifier: BTreeMap<Nullifier, NoteId>,
250 nullifier_order: BTreeMap<Nullifier, u32>,
254}
255
256impl NoteUpdateTracker {
257 pub fn new(
259 input_notes: impl IntoIterator<Item = InputNoteRecord>,
260 output_notes: impl IntoIterator<Item = OutputNoteRecord>,
261 ) -> Self {
262 let mut tracker = Self::default();
263 for note in input_notes {
264 tracker.insert_input_note(note, NoteUpdateType::None);
265 }
266 for note in output_notes {
267 tracker.insert_output_note(note, NoteUpdateType::None);
268 }
269
270 tracker
271 }
272
273 pub fn for_transaction_updates(
281 new_input_notes: impl IntoIterator<Item = InputNoteRecord>,
282 updated_input_notes: impl IntoIterator<Item = InputNoteRecord>,
283 new_output_notes: impl IntoIterator<Item = OutputNoteRecord>,
284 ) -> Self {
285 let mut tracker = Self::default();
286
287 for note in new_input_notes {
288 tracker.insert_input_note(note, NoteUpdateType::Insert);
289 }
290
291 for note in updated_input_notes {
292 tracker.insert_input_note(note, NoteUpdateType::Update);
293 }
294
295 for note in new_output_notes {
296 tracker.insert_output_note(note, NoteUpdateType::Insert);
297 }
298
299 tracker
300 }
301
302 pub fn updated_input_notes(&self) -> impl Iterator<Item = &InputNoteUpdate> {
316 self.input_notes.values().filter(|note| note.update_type.is_modified())
317 }
318
319 pub fn consumed_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
322 self.input_notes_by_id.iter().filter_map(|(note_id, commitment)| {
323 let update = self.input_notes.get(commitment)?;
324 (update.update_type.is_modified() && update.inner().is_consumed()).then_some(*note_id)
325 })
326 }
327
328 pub fn consumed_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
331 let output = self.output_notes.iter().filter_map(|(note_id, update)| {
332 (update.update_type.is_modified() && update.inner().is_consumed()).then_some(*note_id)
333 });
334 self.consumed_input_note_ids().chain(output)
335 }
336
337 pub fn updated_output_notes(&self) -> impl Iterator<Item = &OutputNoteUpdate> {
343 self.output_notes.values().filter(|note| note.update_type.is_modified())
344 }
345
346 pub fn is_empty(&self) -> bool {
348 self.input_notes.is_empty() && self.output_notes.is_empty()
349 }
350
351 pub fn unspent_nullifiers(&self) -> impl Iterator<Item = Nullifier> {
353 let input_note_unspent_nullifiers = self
354 .input_notes
355 .values()
356 .filter(|note| !note.inner().is_consumed())
357 .filter_map(|note| note.inner().nullifier());
358
359 let output_note_unspent_nullifiers = self
360 .output_notes
361 .values()
362 .filter(|note| !note.inner().is_consumed())
363 .filter_map(|note| note.inner().nullifier());
364
365 input_note_unspent_nullifiers.chain(output_note_unspent_nullifiers)
366 }
367
368 pub(crate) fn unspent_input_note_block_numbers(
370 &self,
371 ) -> impl Iterator<Item = BlockNumber> + '_ {
372 self.input_notes
373 .values()
374 .filter(|update| !update.inner().is_consumed())
375 .filter_map(|update| {
376 update.inner().inclusion_proof().map(|proof| proof.location().block_num())
377 })
378 }
379
380 pub fn extend_nullifiers(&mut self, nullifiers: impl IntoIterator<Item = Nullifier>) {
385 for nullifier in nullifiers {
386 let next_pos =
387 u32::try_from(self.nullifier_order.len()).expect("nullifier count exceeds u32");
388 self.nullifier_order.entry(nullifier).or_insert(next_pos);
389 }
390 }
391
392 pub(crate) fn apply_new_public_note(
399 &mut self,
400 mut public_note_data: InputNoteRecord,
401 block_header: &BlockHeader,
402 ) -> Result<(), ClientError> {
403 public_note_data.block_header_received(block_header)?;
404 self.insert_input_note(public_note_data, NoteUpdateType::Insert);
405
406 Ok(())
407 }
408
409 pub(crate) fn apply_committed_note_state_transitions(
412 &mut self,
413 committed_note: &CommittedNote,
414 block_header: &BlockHeader,
415 attachments: &NoteAttachments,
416 ) -> Result<bool, ClientError> {
417 let inclusion_proof = committed_note.inclusion_proof().clone();
418 let metadata = *committed_note.metadata();
419 let note_id = *committed_note.note_id();
420 let attachments = (!attachments.is_empty()).then_some(attachments);
421
422 let is_tracked_as_input_note = if let Some(input_note_record) =
423 self.get_input_note_by_id(note_id)
424 {
425 input_note_record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
426 input_note_record.block_header_received(block_header)?;
427 if let Some(attachments) = attachments {
428 input_note_record.attachments_received(attachments.clone());
429 }
430
431 true
432 } else if let Some(commitment) = self.expected_note_matching(note_id, &metadata) {
433 let nullifier = {
436 let update = self
437 .input_notes
438 .get_mut(&commitment)
439 .expect("commitment was just matched against the tracked notes");
440 let record = &mut update.note;
441 record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
442 record.block_header_received(block_header)?;
443 if let Some(attachments) = attachments {
444 record.attachments_received(attachments.clone());
445 }
446
447 update.update_type = NoteUpdateType::InsertCommitted;
451 record.nullifier().expect("note with an id has metadata")
452 };
453
454 self.input_notes_by_nullifier.insert(nullifier, commitment);
456 self.input_notes_by_id.insert(note_id, commitment);
457
458 true
459 } else {
460 false
461 };
462
463 self.try_commit_output_note(note_id, inclusion_proof)?;
464
465 Ok(is_tracked_as_input_note)
466 }
467
468 pub(crate) fn apply_output_note_inclusion_proofs(
473 &mut self,
474 committed_notes: &[CommittedNote],
475 ) -> Result<(), ClientError> {
476 for committed_note in committed_notes {
477 self.try_commit_output_note(
478 *committed_note.note_id(),
479 committed_note.inclusion_proof().clone(),
480 )?;
481 }
482 Ok(())
483 }
484
485 pub(crate) fn mark_erased_note_as_consumed(
495 &mut self,
496 note_header: &NoteHeader,
497 block_num: BlockNumber,
498 ) -> Result<(), ClientError> {
499 let note_id = note_header.id();
500
501 if let Some(output_note) = self.get_output_note_by_id(note_id)
502 && output_note.is_inclusion_pending()
503 && let Some(nullifier) = output_note.nullifier()
504 {
505 output_note.nullifier_received(nullifier, block_num)?;
506 }
507
508 if let Some(commitment) = self.input_notes_by_id.get(¬e_id).copied()
509 && let Some(input_note_update) = self.input_notes.get_mut(&commitment)
510 && !input_note_update.inner().is_consumed()
511 && let Some(nullifier) = input_note_update.inner().nullifier()
512 {
513 let consumer_account =
514 NetworkAccountTarget::try_from(input_note_update.inner().attachments())
515 .ok()
516 .map(|target| target.target_id());
517 input_note_update.inner_mut().consumed_externally(
518 nullifier,
519 block_num,
520 consumer_account,
521 )?;
522 input_note_update.inner_mut().set_consumed_tx_order(Some(0));
523 }
524
525 Ok(())
526 }
527
528 pub(crate) fn tracks_note(&self, note_id: NoteId) -> bool {
530 self.input_notes_by_id.contains_key(¬e_id) || self.output_notes.contains_key(¬e_id)
531 }
532
533 pub(crate) fn insert_consumed_public_note(
540 &mut self,
541 note: Note,
542 consumer: AccountId,
543 block_num: BlockNumber,
544 ) -> Result<(), ClientError> {
545 let note_id = note.id();
546 if self.tracks_note(note_id) {
547 return Ok(());
548 }
549 let nullifier = note.nullifier();
550 let order = self
554 .get_nullifier_order(nullifier)
555 .ok_or(ClientError::MissingConsumedNoteOrder(note_id))?;
556 let mut record = InputNoteRecord::from(note);
557 record.consumed_externally(nullifier, block_num, Some(consumer))?;
558 record.set_consumed_tx_order(Some(order));
559 self.insert_input_note(record, NoteUpdateType::Insert);
560 Ok(())
561 }
562
563 fn try_insert_consumed_input_from_output(
570 &mut self,
571 note_id: NoteId,
572 consumer: AccountId,
573 block_num: BlockNumber,
574 consumed_tx_order: Option<u32>,
575 ) -> Result<(), ClientError> {
576 if self.input_notes_by_id.contains_key(¬e_id) {
577 return Ok(());
578 }
579 let Some(output_note) = self.output_notes.get(¬e_id) else {
580 return Ok(());
581 };
582 let Ok(note) = Note::try_from(output_note.inner().clone()) else {
583 return Ok(());
584 };
585
586 let mut input_record = InputNoteRecord::from(note);
587 let nullifier =
588 input_record.nullifier().expect("record built from a full note has metadata");
589 input_record.consumed_externally(nullifier, block_num, Some(consumer))?;
590 input_record.set_consumed_tx_order(consumed_tx_order);
591 self.insert_input_note(input_record, NoteUpdateType::Insert);
592 Ok(())
593 }
594
595 fn try_commit_output_note(
598 &mut self,
599 note_id: NoteId,
600 inclusion_proof: NoteInclusionProof,
601 ) -> Result<(), ClientError> {
602 if let Some(output_note) = self.get_output_note_by_id(note_id) {
603 output_note.inclusion_proof_received(inclusion_proof)?;
604 }
605 Ok(())
606 }
607
608 pub(crate) fn apply_note_consumption<'a>(
622 &mut self,
623 consumption: &NoteConsumption,
624 mut committed_transactions: impl Iterator<Item = &'a TransactionRecord>,
625 ) -> Result<(), ClientError> {
626 let nullifier = consumption.nullifier;
627 let block_num = consumption.block_num;
628 let external_consumer = consumption.external_consumer;
629 let order = self.get_nullifier_order(nullifier);
630 let input_present = self.input_notes_by_nullifier.contains_key(&nullifier);
631
632 if let Some(input_note_update) = self.get_input_note_update_by_nullifier(nullifier) {
633 if let Some(consumer_transaction) = committed_transactions
634 .find(|t| input_note_update.inner().consumer_transaction_id() == Some(&t.id))
635 {
636 if let TransactionStatus::Committed { block_number, .. } =
638 consumer_transaction.status
639 {
640 input_note_update
641 .inner_mut()
642 .transaction_committed(consumer_transaction.id, block_number)?;
643 }
644 } else {
645 input_note_update.inner_mut().consumed_externally(
648 nullifier,
649 block_num,
650 external_consumer,
651 )?;
652 }
653 input_note_update.inner_mut().set_consumed_tx_order(order);
654 }
655
656 if let Some(output_note_record) = self.get_output_note_by_nullifier(nullifier) {
657 output_note_record.nullifier_received(nullifier, block_num)?;
658 }
659
660 if !input_present
661 && let Some(consumer) = external_consumer
662 && let Some(note_id) = self.output_notes_by_nullifier.get(&nullifier).copied()
663 {
664 self.try_insert_consumed_input_from_output(note_id, consumer, block_num, order)?;
665 }
666
667 Ok(())
668 }
669
670 fn get_nullifier_order(&self, nullifier: Nullifier) -> Option<u32> {
676 self.nullifier_order.get(&nullifier).copied()
677 }
678
679 fn get_input_note_by_id(&mut self, note_id: NoteId) -> Option<&mut InputNoteRecord> {
681 let commitment = self.input_notes_by_id.get(¬e_id).copied()?;
682 self.input_notes.get_mut(&commitment).map(InputNoteUpdate::inner_mut)
683 }
684
685 fn expected_note_matching(
688 &self,
689 note_id: NoteId,
690 metadata: &NoteMetadata,
691 ) -> Option<NoteDetailsCommitment> {
692 self.input_notes
693 .iter()
694 .filter(|(_, update)| update.inner().metadata().is_none())
695 .map(|(commitment, _)| *commitment)
696 .find(|commitment| NoteId::new(*commitment, metadata) == note_id)
697 }
698
699 fn get_output_note_by_id(&mut self, note_id: NoteId) -> Option<&mut OutputNoteRecord> {
701 self.output_notes.get_mut(¬e_id).map(OutputNoteUpdate::inner_mut)
702 }
703
704 fn get_input_note_update_by_nullifier(
707 &mut self,
708 nullifier: Nullifier,
709 ) -> Option<&mut InputNoteUpdate> {
710 let commitment = self.input_notes_by_nullifier.get(&nullifier).copied()?;
711 self.input_notes.get_mut(&commitment)
712 }
713
714 fn get_output_note_by_nullifier(
717 &mut self,
718 nullifier: Nullifier,
719 ) -> Option<&mut OutputNoteRecord> {
720 let note_id = self.output_notes_by_nullifier.get(&nullifier).copied()?;
721 self.output_notes.get_mut(¬e_id).map(OutputNoteUpdate::inner_mut)
722 }
723
724 fn insert_input_note(&mut self, note: InputNoteRecord, update_type: NoteUpdateType) {
726 let update = match update_type {
727 NoteUpdateType::None => InputNoteUpdate::new_none(note),
728 NoteUpdateType::Insert => InputNoteUpdate::new_insert(note),
729 NoteUpdateType::Update => InputNoteUpdate::new_update(note),
730 NoteUpdateType::InsertCommitted => InputNoteUpdate::new_insert_committed(note),
731 };
732
733 let commitment = update.inner().details_commitment();
734 if let Some(note_id) = update.inner().id() {
735 let nullifier = update.inner().nullifier().expect("note with an id has metadata");
737 self.input_notes_by_nullifier.insert(nullifier, commitment);
738 self.input_notes_by_id.insert(note_id, commitment);
739 self.input_notes.insert(commitment, update);
740 } else if self.input_notes.get(&commitment).is_none_or(|u| u.inner().id().is_none()) {
741 self.input_notes.insert(commitment, update);
745 }
746 }
747
748 fn insert_output_note(&mut self, note: OutputNoteRecord, update_type: NoteUpdateType) {
750 let note_id = note.id();
751 if let Some(nullifier) = note.nullifier() {
752 self.output_notes_by_nullifier.insert(nullifier, note_id);
753 }
754 let update = match update_type {
755 NoteUpdateType::None => OutputNoteUpdate::new_none(note),
756 NoteUpdateType::Update => OutputNoteUpdate::new_update(note),
757 NoteUpdateType::Insert | NoteUpdateType::InsertCommitted => {
760 OutputNoteUpdate::new_insert(note)
761 },
762 };
763 self.output_notes.insert(note_id, update);
764 }
765}
766
767impl Serializable for NoteUpdateType {
771 fn write_into<W: ByteWriter>(&self, target: &mut W) {
772 target.write_u8(*self as u8);
773 }
774}
775
776impl Deserializable for NoteUpdateType {
777 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
778 NoteUpdateType::try_from(source.read_u8()?).map_err(|val| {
779 DeserializationError::InvalidValue(format!("invalid note update type: {val}"))
780 })
781 }
782}
783
784impl Serializable for InputNoteUpdate {
785 fn write_into<W: ByteWriter>(&self, target: &mut W) {
786 self.note.write_into(target);
787 self.update_type.write_into(target);
788 }
789}
790
791impl Deserializable for InputNoteUpdate {
792 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
793 let note = InputNoteRecord::read_from(source)?;
794 let update_type = NoteUpdateType::read_from(source)?;
795 Ok(Self { note, update_type })
796 }
797}
798
799impl Serializable for OutputNoteUpdate {
800 fn write_into<W: ByteWriter>(&self, target: &mut W) {
801 self.note.write_into(target);
802 self.update_type.write_into(target);
803 }
804}
805
806impl Deserializable for OutputNoteUpdate {
807 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
808 let note = OutputNoteRecord::read_from(source)?;
809 let update_type = NoteUpdateType::read_from(source)?;
810 Ok(Self { note, update_type })
811 }
812}
813
814impl Serializable for NoteUpdateTracker {
815 fn write_into<W: ByteWriter>(&self, target: &mut W) {
816 self.input_notes.write_into(target);
819 self.output_notes.write_into(target);
820 self.nullifier_order.write_into(target);
821 self.input_notes_by_id.write_into(target);
822 self.input_notes_by_nullifier.write_into(target);
823 }
824}
825
826impl Deserializable for NoteUpdateTracker {
827 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
828 let input_notes = BTreeMap::<NoteDetailsCommitment, InputNoteUpdate>::read_from(source)?;
829 let output_notes = BTreeMap::<NoteId, OutputNoteUpdate>::read_from(source)?;
830 let nullifier_order = BTreeMap::<Nullifier, u32>::read_from(source)?;
831 let input_notes_by_id = BTreeMap::<NoteId, NoteDetailsCommitment>::read_from(source)?;
832 let input_notes_by_nullifier =
833 BTreeMap::<Nullifier, NoteDetailsCommitment>::read_from(source)?;
834
835 let output_notes_by_nullifier = output_notes
837 .iter()
838 .filter_map(|(note_id, update)| {
839 update.inner().nullifier().map(|nullifier| (nullifier, *note_id))
840 })
841 .collect();
842
843 Ok(Self {
844 input_notes,
845 output_notes,
846 input_notes_by_nullifier,
847 input_notes_by_id,
848 output_notes_by_nullifier,
849 nullifier_order,
850 })
851 }
852}
853
854#[cfg(test)]
858mod tests {
859 use alloc::vec;
860
861 use miden_protocol::account::AccountId;
862 use miden_protocol::block::BlockNumber;
863 use miden_protocol::note::{
864 NoteAssets,
865 NoteAttachments,
866 NoteDetails,
867 NoteId,
868 NoteMetadata,
869 NoteRecipient,
870 NoteStorage,
871 NoteType,
872 PartialNoteMetadata,
873 };
874 use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER;
875 use miden_protocol::transaction::TransactionId;
876 use miden_protocol::utils::serde::{Deserializable, Serializable};
877 use miden_protocol::{Felt, Word, ZERO};
878 use miden_standards::note::StandardNote;
879
880 use super::{NoteConsumption, NoteUpdateTracker};
881 use crate::store::InputNoteRecord;
882 use crate::store::input_note_states::{
883 ConsumedExternalNoteState,
884 ConsumedUnauthenticatedLocalNoteState,
885 ExpectedNoteState,
886 NoteSubmissionData,
887 ProcessingUnauthenticatedNoteState,
888 };
889 use crate::transaction::TransactionRecord;
890
891 fn note_details(seed: u64) -> NoteDetails {
895 let serial_number: Word = [Felt::new_unchecked(seed), ZERO, ZERO, ZERO].into();
896 let recipient = NoteRecipient::new(
897 serial_number,
898 StandardNote::SWAP.script(),
899 NoteStorage::new(vec![]).unwrap(),
900 );
901 NoteDetails::new(NoteAssets::new(vec![]).unwrap(), recipient)
902 }
903
904 fn note_metadata(sender: AccountId) -> NoteMetadata {
905 NoteMetadata::new(
906 PartialNoteMetadata::new(sender, NoteType::Public),
907 &NoteAttachments::empty(),
908 )
909 }
910
911 fn expected_note(seed: u64) -> InputNoteRecord {
913 let state = ExpectedNoteState {
914 metadata: None,
915 after_block_num: BlockNumber::from(0u32),
916 tag: None,
917 };
918 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
919 }
920
921 fn processing_note(seed: u64, sender: AccountId) -> InputNoteRecord {
923 let state = ProcessingUnauthenticatedNoteState {
924 metadata: note_metadata(sender),
925 after_block_num: BlockNumber::from(0u32),
926 submission_data: NoteSubmissionData {
927 submitted_at: Some(0),
928 consumer_account: sender,
929 consumer_transaction: TransactionId::from_raw(Word::default()),
930 },
931 };
932 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
933 }
934
935 fn consumed_local_note(seed: u64, sender: AccountId) -> InputNoteRecord {
937 let state = ConsumedUnauthenticatedLocalNoteState {
938 metadata: note_metadata(sender),
939 nullifier_block_height: BlockNumber::from(1u32),
940 submission_data: NoteSubmissionData {
941 submitted_at: Some(0),
942 consumer_account: sender,
943 consumer_transaction: TransactionId::from_raw(Word::default()),
944 },
945 consumed_tx_order: Some(0),
946 };
947 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
948 }
949
950 fn consumed_external_note(seed: u64) -> InputNoteRecord {
952 let state = ConsumedExternalNoteState {
953 nullifier_block_height: BlockNumber::from(1u32),
954 consumer_account: None,
955 consumed_tx_order: None,
956 metadata: None,
957 };
958 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
959 }
960
961 #[test]
965 fn consumed_input_note_ids_reports_metadata_bearing_consumed_note() {
966 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
967 let note = consumed_local_note(1, sender);
968 let id = note.id().expect("consumed-local note has metadata");
969
970 let tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
971
972 let consumed: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
973 assert_eq!(consumed, vec![id]);
974 }
975
976 #[test]
977 fn consumed_input_note_ids_omits_note_that_never_had_an_id() {
978 let note = consumed_external_note(2);
981 assert!(note.id().is_none());
982
983 let tracker = NoteUpdateTracker::for_transaction_updates(vec![note], vec![], vec![]);
984
985 assert_eq!(tracker.consumed_input_note_ids().count(), 0);
986 assert_eq!(tracker.updated_input_notes().count(), 1);
987 }
988
989 #[test]
990 fn external_consumption_retains_note_id() {
991 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
992 let note = processing_note(3, sender);
993 let id = note.id().expect("processing note has metadata");
994 let nullifier = note.nullifier().expect("processing note has metadata");
995
996 let mut tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
997 assert_eq!(tracker.consumed_input_note_ids().count(), 0);
998
999 tracker
1001 .apply_note_consumption(
1002 &NoteConsumption {
1003 nullifier,
1004 block_num: BlockNumber::from(5u32),
1005 external_consumer: None,
1006 },
1007 core::iter::empty::<&TransactionRecord>(),
1008 )
1009 .expect("external consumption should apply");
1010
1011 let consumed: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
1012 assert_eq!(
1013 consumed,
1014 vec![id],
1015 "an externally consumed note must still be reported by its id"
1016 );
1017 }
1018
1019 #[test]
1020 fn externally_consumed_note_id_survives_round_trip() {
1021 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1022 let note = processing_note(12, sender);
1023 let id = note.id().expect("processing note has metadata");
1024 let nullifier = note.nullifier().expect("processing note has metadata");
1025
1026 let mut tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
1027
1028 tracker
1031 .apply_note_consumption(
1032 &NoteConsumption {
1033 nullifier,
1034 block_num: BlockNumber::from(5u32),
1035 external_consumer: None,
1036 },
1037 core::iter::empty::<&TransactionRecord>(),
1038 )
1039 .expect("external consumption should apply");
1040
1041 let before: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
1043 assert_eq!(before, vec![id]);
1044
1045 let bytes = tracker.to_bytes();
1047 let restored = NoteUpdateTracker::read_from_bytes(&bytes).expect("round-trip should work");
1048 let after: alloc::vec::Vec<NoteId> = restored.consumed_input_note_ids().collect();
1049 assert_eq!(
1050 after,
1051 vec![id],
1052 "the retained id of an externally consumed note must survive serialization"
1053 );
1054 }
1055
1056 #[test]
1057 fn serialize_round_trip_preserves_lookup_indices() {
1058 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1059 let expected = expected_note(10);
1060 let processing = processing_note(11, sender);
1061 let processing_id = processing.id().expect("processing note has metadata");
1062 let processing_commitment = processing.details_commitment();
1063 let processing_nullifier = processing.nullifier().expect("processing note has metadata");
1064
1065 let tracker =
1066 NoteUpdateTracker::for_transaction_updates(vec![expected], vec![processing], vec![]);
1067
1068 let bytes = tracker.to_bytes();
1069 let restored = NoteUpdateTracker::read_from_bytes(&bytes).expect("round-trip should work");
1070
1071 assert_eq!(tracker, restored);
1074 assert_eq!(restored.updated_input_notes().count(), 2);
1075 assert_eq!(
1076 restored.input_notes_by_id.get(&processing_id).copied(),
1077 Some(processing_commitment)
1078 );
1079 assert_eq!(
1080 restored.input_notes_by_nullifier.get(&processing_nullifier).copied(),
1081 Some(processing_commitment)
1082 );
1083 }
1084}