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: Option<&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
421 let is_tracked_as_input_note = if let Some(input_note_record) =
422 self.get_input_note_by_id(note_id)
423 {
424 input_note_record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
425 input_note_record.block_header_received(block_header)?;
426 if let Some(attachments) = attachments {
427 input_note_record.attachments_received(attachments.clone());
428 }
429
430 true
431 } else if let Some(commitment) = self.expected_note_matching(note_id, &metadata) {
432 let nullifier = {
435 let update = self
436 .input_notes
437 .get_mut(&commitment)
438 .expect("commitment was just matched against the tracked notes");
439 let record = &mut update.note;
440 record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
441 record.block_header_received(block_header)?;
442 if let Some(attachments) = attachments {
443 record.attachments_received(attachments.clone());
444 }
445
446 update.update_type = NoteUpdateType::InsertCommitted;
450 record.nullifier().expect("note with an id has metadata")
451 };
452
453 self.input_notes_by_nullifier.insert(nullifier, commitment);
455 self.input_notes_by_id.insert(note_id, commitment);
456
457 true
458 } else {
459 false
460 };
461
462 self.try_commit_output_note(note_id, inclusion_proof)?;
463
464 Ok(is_tracked_as_input_note)
465 }
466
467 pub(crate) fn apply_output_note_inclusion_proofs(
472 &mut self,
473 committed_notes: &[CommittedNote],
474 ) -> Result<(), ClientError> {
475 for committed_note in committed_notes {
476 self.try_commit_output_note(
477 *committed_note.note_id(),
478 committed_note.inclusion_proof().clone(),
479 )?;
480 }
481 Ok(())
482 }
483
484 pub(crate) fn mark_erased_note_as_consumed(
494 &mut self,
495 note_header: &NoteHeader,
496 block_num: BlockNumber,
497 ) -> Result<(), ClientError> {
498 let note_id = note_header.id();
499
500 if let Some(output_note) = self.get_output_note_by_id(note_id)
501 && output_note.is_inclusion_pending()
502 && let Some(nullifier) = output_note.nullifier()
503 {
504 output_note.nullifier_received(nullifier, block_num)?;
505 }
506
507 if let Some(commitment) = self.input_notes_by_id.get(¬e_id).copied()
508 && let Some(input_note_update) = self.input_notes.get_mut(&commitment)
509 && !input_note_update.inner().is_consumed()
510 && let Some(nullifier) = input_note_update.inner().nullifier()
511 {
512 let consumer_account =
513 NetworkAccountTarget::try_from(input_note_update.inner().attachments())
514 .ok()
515 .map(|target| target.target_id());
516 input_note_update.inner_mut().consumed_externally(
517 nullifier,
518 block_num,
519 consumer_account,
520 )?;
521 input_note_update.inner_mut().set_consumed_tx_order(Some(0));
522 }
523
524 Ok(())
525 }
526
527 pub(crate) fn tracks_note(&self, note_id: NoteId) -> bool {
529 self.input_notes_by_id.contains_key(¬e_id) || self.output_notes.contains_key(¬e_id)
530 }
531
532 pub(crate) fn insert_consumed_public_note(
539 &mut self,
540 note: Note,
541 consumer: AccountId,
542 block_num: BlockNumber,
543 ) -> Result<(), ClientError> {
544 let note_id = note.id();
545 if self.tracks_note(note_id) {
546 return Ok(());
547 }
548 let nullifier = note.nullifier();
549 let order = self
553 .get_nullifier_order(nullifier)
554 .ok_or(ClientError::MissingConsumedNoteOrder(note_id))?;
555 let mut record = InputNoteRecord::from(note);
556 record.consumed_externally(nullifier, block_num, Some(consumer))?;
557 record.set_consumed_tx_order(Some(order));
558 self.insert_input_note(record, NoteUpdateType::Insert);
559 Ok(())
560 }
561
562 fn try_insert_consumed_input_from_output(
569 &mut self,
570 note_id: NoteId,
571 consumer: AccountId,
572 block_num: BlockNumber,
573 consumed_tx_order: Option<u32>,
574 ) -> Result<(), ClientError> {
575 if self.input_notes_by_id.contains_key(¬e_id) {
576 return Ok(());
577 }
578 let Some(output_note) = self.output_notes.get(¬e_id) else {
579 return Ok(());
580 };
581 let Ok(note) = Note::try_from(output_note.inner().clone()) else {
582 return Ok(());
583 };
584
585 let mut input_record = InputNoteRecord::from(note);
586 let nullifier =
587 input_record.nullifier().expect("record built from a full note has metadata");
588 input_record.consumed_externally(nullifier, block_num, Some(consumer))?;
589 input_record.set_consumed_tx_order(consumed_tx_order);
590 self.insert_input_note(input_record, NoteUpdateType::Insert);
591 Ok(())
592 }
593
594 fn try_commit_output_note(
597 &mut self,
598 note_id: NoteId,
599 inclusion_proof: NoteInclusionProof,
600 ) -> Result<(), ClientError> {
601 if let Some(output_note) = self.get_output_note_by_id(note_id) {
602 output_note.inclusion_proof_received(inclusion_proof)?;
603 }
604 Ok(())
605 }
606
607 pub(crate) fn apply_note_consumption<'a>(
621 &mut self,
622 consumption: &NoteConsumption,
623 mut committed_transactions: impl Iterator<Item = &'a TransactionRecord>,
624 ) -> Result<(), ClientError> {
625 let nullifier = consumption.nullifier;
626 let block_num = consumption.block_num;
627 let external_consumer = consumption.external_consumer;
628 let order = self.get_nullifier_order(nullifier);
629 let input_present = self.input_notes_by_nullifier.contains_key(&nullifier);
630
631 if let Some(input_note_update) = self.get_input_note_update_by_nullifier(nullifier) {
632 if let Some(consumer_transaction) = committed_transactions
633 .find(|t| input_note_update.inner().consumer_transaction_id() == Some(&t.id))
634 {
635 if let TransactionStatus::Committed { block_number, .. } =
637 consumer_transaction.status
638 {
639 input_note_update
640 .inner_mut()
641 .transaction_committed(consumer_transaction.id, block_number)?;
642 }
643 } else {
644 input_note_update.inner_mut().consumed_externally(
647 nullifier,
648 block_num,
649 external_consumer,
650 )?;
651 }
652 input_note_update.inner_mut().set_consumed_tx_order(order);
653 }
654
655 if let Some(output_note_record) = self.get_output_note_by_nullifier(nullifier) {
656 output_note_record.nullifier_received(nullifier, block_num)?;
657 }
658
659 if !input_present
660 && let Some(consumer) = external_consumer
661 && let Some(note_id) = self.output_notes_by_nullifier.get(&nullifier).copied()
662 {
663 self.try_insert_consumed_input_from_output(note_id, consumer, block_num, order)?;
664 }
665
666 Ok(())
667 }
668
669 fn get_nullifier_order(&self, nullifier: Nullifier) -> Option<u32> {
675 self.nullifier_order.get(&nullifier).copied()
676 }
677
678 fn get_input_note_by_id(&mut self, note_id: NoteId) -> Option<&mut InputNoteRecord> {
680 let commitment = self.input_notes_by_id.get(¬e_id).copied()?;
681 self.input_notes.get_mut(&commitment).map(InputNoteUpdate::inner_mut)
682 }
683
684 fn expected_note_matching(
687 &self,
688 note_id: NoteId,
689 metadata: &NoteMetadata,
690 ) -> Option<NoteDetailsCommitment> {
691 self.input_notes
692 .iter()
693 .filter(|(_, update)| update.inner().metadata().is_none())
694 .map(|(commitment, _)| *commitment)
695 .find(|commitment| NoteId::new(*commitment, metadata) == note_id)
696 }
697
698 fn get_output_note_by_id(&mut self, note_id: NoteId) -> Option<&mut OutputNoteRecord> {
700 self.output_notes.get_mut(¬e_id).map(OutputNoteUpdate::inner_mut)
701 }
702
703 fn get_input_note_update_by_nullifier(
706 &mut self,
707 nullifier: Nullifier,
708 ) -> Option<&mut InputNoteUpdate> {
709 let commitment = self.input_notes_by_nullifier.get(&nullifier).copied()?;
710 self.input_notes.get_mut(&commitment)
711 }
712
713 fn get_output_note_by_nullifier(
716 &mut self,
717 nullifier: Nullifier,
718 ) -> Option<&mut OutputNoteRecord> {
719 let note_id = self.output_notes_by_nullifier.get(&nullifier).copied()?;
720 self.output_notes.get_mut(¬e_id).map(OutputNoteUpdate::inner_mut)
721 }
722
723 fn insert_input_note(&mut self, note: InputNoteRecord, update_type: NoteUpdateType) {
725 let update = match update_type {
726 NoteUpdateType::None => InputNoteUpdate::new_none(note),
727 NoteUpdateType::Insert => InputNoteUpdate::new_insert(note),
728 NoteUpdateType::Update => InputNoteUpdate::new_update(note),
729 NoteUpdateType::InsertCommitted => InputNoteUpdate::new_insert_committed(note),
730 };
731
732 let commitment = update.inner().details_commitment();
733 if let Some(note_id) = update.inner().id() {
734 let nullifier = update.inner().nullifier().expect("note with an id has metadata");
736 self.input_notes_by_nullifier.insert(nullifier, commitment);
737 self.input_notes_by_id.insert(note_id, commitment);
738 self.input_notes.insert(commitment, update);
739 } else if self.input_notes.get(&commitment).is_none_or(|u| u.inner().id().is_none()) {
740 self.input_notes.insert(commitment, update);
744 }
745 }
746
747 fn insert_output_note(&mut self, note: OutputNoteRecord, update_type: NoteUpdateType) {
749 let note_id = note.id();
750 if let Some(nullifier) = note.nullifier() {
751 self.output_notes_by_nullifier.insert(nullifier, note_id);
752 }
753 let update = match update_type {
754 NoteUpdateType::None => OutputNoteUpdate::new_none(note),
755 NoteUpdateType::Update => OutputNoteUpdate::new_update(note),
756 NoteUpdateType::Insert | NoteUpdateType::InsertCommitted => {
759 OutputNoteUpdate::new_insert(note)
760 },
761 };
762 self.output_notes.insert(note_id, update);
763 }
764}
765
766impl Serializable for NoteUpdateType {
770 fn write_into<W: ByteWriter>(&self, target: &mut W) {
771 target.write_u8(*self as u8);
772 }
773}
774
775impl Deserializable for NoteUpdateType {
776 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
777 NoteUpdateType::try_from(source.read_u8()?).map_err(|val| {
778 DeserializationError::InvalidValue(format!("invalid note update type: {val}"))
779 })
780 }
781}
782
783impl Serializable for InputNoteUpdate {
784 fn write_into<W: ByteWriter>(&self, target: &mut W) {
785 self.note.write_into(target);
786 self.update_type.write_into(target);
787 }
788}
789
790impl Deserializable for InputNoteUpdate {
791 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
792 let note = InputNoteRecord::read_from(source)?;
793 let update_type = NoteUpdateType::read_from(source)?;
794 Ok(Self { note, update_type })
795 }
796}
797
798impl Serializable for OutputNoteUpdate {
799 fn write_into<W: ByteWriter>(&self, target: &mut W) {
800 self.note.write_into(target);
801 self.update_type.write_into(target);
802 }
803}
804
805impl Deserializable for OutputNoteUpdate {
806 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
807 let note = OutputNoteRecord::read_from(source)?;
808 let update_type = NoteUpdateType::read_from(source)?;
809 Ok(Self { note, update_type })
810 }
811}
812
813impl Serializable for NoteUpdateTracker {
814 fn write_into<W: ByteWriter>(&self, target: &mut W) {
815 self.input_notes.write_into(target);
818 self.output_notes.write_into(target);
819 self.nullifier_order.write_into(target);
820 self.input_notes_by_id.write_into(target);
821 self.input_notes_by_nullifier.write_into(target);
822 }
823}
824
825impl Deserializable for NoteUpdateTracker {
826 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
827 let input_notes = BTreeMap::<NoteDetailsCommitment, InputNoteUpdate>::read_from(source)?;
828 let output_notes = BTreeMap::<NoteId, OutputNoteUpdate>::read_from(source)?;
829 let nullifier_order = BTreeMap::<Nullifier, u32>::read_from(source)?;
830 let input_notes_by_id = BTreeMap::<NoteId, NoteDetailsCommitment>::read_from(source)?;
831 let input_notes_by_nullifier =
832 BTreeMap::<Nullifier, NoteDetailsCommitment>::read_from(source)?;
833
834 let output_notes_by_nullifier = output_notes
836 .iter()
837 .filter_map(|(note_id, update)| {
838 update.inner().nullifier().map(|nullifier| (nullifier, *note_id))
839 })
840 .collect();
841
842 Ok(Self {
843 input_notes,
844 output_notes,
845 input_notes_by_nullifier,
846 input_notes_by_id,
847 output_notes_by_nullifier,
848 nullifier_order,
849 })
850 }
851}
852
853#[cfg(test)]
857mod tests {
858 use alloc::vec;
859
860 use miden_protocol::account::AccountId;
861 use miden_protocol::block::BlockNumber;
862 use miden_protocol::note::{
863 NoteAssets,
864 NoteAttachments,
865 NoteDetails,
866 NoteId,
867 NoteMetadata,
868 NoteRecipient,
869 NoteStorage,
870 NoteType,
871 PartialNoteMetadata,
872 };
873 use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER;
874 use miden_protocol::transaction::TransactionId;
875 use miden_protocol::utils::serde::{Deserializable, Serializable};
876 use miden_protocol::{Felt, Word, ZERO};
877 use miden_standards::note::StandardNote;
878
879 use super::{NoteConsumption, NoteUpdateTracker};
880 use crate::store::InputNoteRecord;
881 use crate::store::input_note_states::{
882 ConsumedExternalNoteState,
883 ConsumedUnauthenticatedLocalNoteState,
884 ExpectedNoteState,
885 NoteSubmissionData,
886 ProcessingUnauthenticatedNoteState,
887 };
888 use crate::transaction::TransactionRecord;
889
890 fn note_details(seed: u64) -> NoteDetails {
894 let serial_number: Word = [Felt::new_unchecked(seed), ZERO, ZERO, ZERO].into();
895 let recipient = NoteRecipient::new(
896 serial_number,
897 StandardNote::SWAP.script(),
898 NoteStorage::new(vec![]).unwrap(),
899 );
900 NoteDetails::new(NoteAssets::new(vec![]).unwrap(), recipient)
901 }
902
903 fn note_metadata(sender: AccountId) -> NoteMetadata {
904 NoteMetadata::new(
905 PartialNoteMetadata::new(sender, NoteType::Public),
906 &NoteAttachments::empty(),
907 )
908 }
909
910 fn expected_note(seed: u64) -> InputNoteRecord {
912 let state = ExpectedNoteState {
913 metadata: None,
914 after_block_num: BlockNumber::from(0u32),
915 tag: None,
916 };
917 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
918 }
919
920 fn processing_note(seed: u64, sender: AccountId) -> InputNoteRecord {
922 let state = ProcessingUnauthenticatedNoteState {
923 metadata: note_metadata(sender),
924 after_block_num: BlockNumber::from(0u32),
925 submission_data: NoteSubmissionData {
926 submitted_at: Some(0),
927 consumer_account: sender,
928 consumer_transaction: TransactionId::from_raw(Word::default()),
929 },
930 };
931 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
932 }
933
934 fn consumed_local_note(seed: u64, sender: AccountId) -> InputNoteRecord {
936 let state = ConsumedUnauthenticatedLocalNoteState {
937 metadata: note_metadata(sender),
938 nullifier_block_height: BlockNumber::from(1u32),
939 submission_data: NoteSubmissionData {
940 submitted_at: Some(0),
941 consumer_account: sender,
942 consumer_transaction: TransactionId::from_raw(Word::default()),
943 },
944 consumed_tx_order: Some(0),
945 };
946 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
947 }
948
949 fn consumed_external_note(seed: u64) -> InputNoteRecord {
951 let state = ConsumedExternalNoteState {
952 nullifier_block_height: BlockNumber::from(1u32),
953 consumer_account: None,
954 consumed_tx_order: None,
955 metadata: None,
956 };
957 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
958 }
959
960 #[test]
964 fn consumed_input_note_ids_reports_metadata_bearing_consumed_note() {
965 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
966 let note = consumed_local_note(1, sender);
967 let id = note.id().expect("consumed-local note has metadata");
968
969 let tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
970
971 let consumed: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
972 assert_eq!(consumed, vec![id]);
973 }
974
975 #[test]
976 fn consumed_input_note_ids_omits_note_that_never_had_an_id() {
977 let note = consumed_external_note(2);
980 assert!(note.id().is_none());
981
982 let tracker = NoteUpdateTracker::for_transaction_updates(vec![note], vec![], vec![]);
983
984 assert_eq!(tracker.consumed_input_note_ids().count(), 0);
985 assert_eq!(tracker.updated_input_notes().count(), 1);
986 }
987
988 #[test]
989 fn external_consumption_retains_note_id() {
990 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
991 let note = processing_note(3, sender);
992 let id = note.id().expect("processing note has metadata");
993 let nullifier = note.nullifier().expect("processing note has metadata");
994
995 let mut tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
996 assert_eq!(tracker.consumed_input_note_ids().count(), 0);
997
998 tracker
1000 .apply_note_consumption(
1001 &NoteConsumption {
1002 nullifier,
1003 block_num: BlockNumber::from(5u32),
1004 external_consumer: None,
1005 },
1006 core::iter::empty::<&TransactionRecord>(),
1007 )
1008 .expect("external consumption should apply");
1009
1010 let consumed: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
1011 assert_eq!(
1012 consumed,
1013 vec![id],
1014 "an externally consumed note must still be reported by its id"
1015 );
1016 }
1017
1018 #[test]
1019 fn externally_consumed_note_id_survives_round_trip() {
1020 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1021 let note = processing_note(12, sender);
1022 let id = note.id().expect("processing note has metadata");
1023 let nullifier = note.nullifier().expect("processing note has metadata");
1024
1025 let mut tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
1026
1027 tracker
1030 .apply_note_consumption(
1031 &NoteConsumption {
1032 nullifier,
1033 block_num: BlockNumber::from(5u32),
1034 external_consumer: None,
1035 },
1036 core::iter::empty::<&TransactionRecord>(),
1037 )
1038 .expect("external consumption should apply");
1039
1040 let before: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
1042 assert_eq!(before, vec![id]);
1043
1044 let bytes = tracker.to_bytes();
1046 let restored = NoteUpdateTracker::read_from_bytes(&bytes).expect("round-trip should work");
1047 let after: alloc::vec::Vec<NoteId> = restored.consumed_input_note_ids().collect();
1048 assert_eq!(
1049 after,
1050 vec![id],
1051 "the retained id of an externally consumed note must survive serialization"
1052 );
1053 }
1054
1055 #[test]
1056 fn serialize_round_trip_preserves_lookup_indices() {
1057 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1058 let expected = expected_note(10);
1059 let processing = processing_note(11, sender);
1060 let processing_id = processing.id().expect("processing note has metadata");
1061 let processing_commitment = processing.details_commitment();
1062 let processing_nullifier = processing.nullifier().expect("processing note has metadata");
1063
1064 let tracker =
1065 NoteUpdateTracker::for_transaction_updates(vec![expected], vec![processing], vec![]);
1066
1067 let bytes = tracker.to_bytes();
1068 let restored = NoteUpdateTracker::read_from_bytes(&bytes).expect("round-trip should work");
1069
1070 assert_eq!(tracker, restored);
1073 assert_eq!(restored.updated_input_notes().count(), 2);
1074 assert_eq!(
1075 restored.input_notes_by_id.get(&processing_id).copied(),
1076 Some(processing_commitment)
1077 );
1078 assert_eq!(
1079 restored.input_notes_by_nullifier.get(&processing_nullifier).copied(),
1080 Some(processing_commitment)
1081 );
1082 }
1083}