1use alloc::collections::BTreeMap;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::block::{BlockHeader, BlockNumber};
5use miden_protocol::note::{
6 Note,
7 NoteDetailsCommitment,
8 NoteHeader,
9 NoteId,
10 NoteInclusionProof,
11 NoteMetadata,
12 Nullifier,
13};
14use miden_standards::note::NetworkAccountTarget;
15use miden_tx::utils::serde::{
16 ByteReader,
17 ByteWriter,
18 Deserializable,
19 DeserializationError,
20 Serializable,
21};
22
23use crate::ClientError;
24use crate::rpc::domain::note::CommittedNote;
25use crate::store::{InputNoteRecord, OutputNoteRecord};
26use crate::transaction::{TransactionRecord, TransactionStatus};
27
28pub struct NoteConsumption {
33 pub nullifier: Nullifier,
35 pub block_num: BlockNumber,
37 pub external_consumer: Option<AccountId>,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49#[repr(u8)]
50pub enum NoteUpdateType {
51 None = 0,
53 Insert = 1,
55 Update = 2,
57 InsertCommitted = 3,
62}
63
64impl NoteUpdateType {
65 pub fn is_modified(self) -> bool {
69 matches!(self, Self::Insert | Self::Update | Self::InsertCommitted)
70 }
71}
72
73impl TryFrom<u8> for NoteUpdateType {
74 type Error = u8;
75
76 fn try_from(value: u8) -> Result<Self, Self::Error> {
77 match value {
78 0 => Ok(NoteUpdateType::None),
79 1 => Ok(NoteUpdateType::Insert),
80 2 => Ok(NoteUpdateType::Update),
81 3 => Ok(NoteUpdateType::InsertCommitted),
82 other => Err(other),
83 }
84 }
85}
86
87#[derive(Clone, Debug, PartialEq)]
89pub struct InputNoteUpdate {
90 note: InputNoteRecord,
92 update_type: NoteUpdateType,
94}
95
96impl InputNoteUpdate {
97 fn new_none(note: InputNoteRecord) -> Self {
99 Self { note, update_type: NoteUpdateType::None }
100 }
101
102 fn new_insert(note: InputNoteRecord) -> Self {
104 Self {
105 note,
106 update_type: NoteUpdateType::Insert,
107 }
108 }
109
110 fn new_update(note: InputNoteRecord) -> Self {
112 Self {
113 note,
114 update_type: NoteUpdateType::Update,
115 }
116 }
117
118 fn new_insert_committed(note: InputNoteRecord) -> Self {
121 Self {
122 note,
123 update_type: NoteUpdateType::InsertCommitted,
124 }
125 }
126
127 pub fn inner(&self) -> &InputNoteRecord {
129 &self.note
130 }
131
132 fn inner_mut(&mut self) -> &mut InputNoteRecord {
135 self.update_type = match self.update_type {
136 NoteUpdateType::None | NoteUpdateType::Update => NoteUpdateType::Update,
137 NoteUpdateType::Insert => NoteUpdateType::Insert,
138 NoteUpdateType::InsertCommitted => NoteUpdateType::InsertCommitted,
139 };
140
141 &mut self.note
142 }
143
144 pub fn update_type(&self) -> &NoteUpdateType {
146 &self.update_type
147 }
148
149 pub fn id(&self) -> Option<NoteId> {
152 self.note.id()
153 }
154
155 pub fn consumed_tx_order(&self) -> Option<u32> {
159 self.note.state().consumed_tx_order()
160 }
161}
162
163#[derive(Clone, Debug, PartialEq)]
165pub struct OutputNoteUpdate {
166 note: OutputNoteRecord,
168 update_type: NoteUpdateType,
170}
171
172impl OutputNoteUpdate {
173 fn new_none(note: OutputNoteRecord) -> Self {
175 Self { note, update_type: NoteUpdateType::None }
176 }
177
178 fn new_insert(note: OutputNoteRecord) -> Self {
180 Self {
181 note,
182 update_type: NoteUpdateType::Insert,
183 }
184 }
185
186 fn new_update(note: OutputNoteRecord) -> Self {
188 Self {
189 note,
190 update_type: NoteUpdateType::Update,
191 }
192 }
193
194 pub fn inner(&self) -> &OutputNoteRecord {
196 &self.note
197 }
198
199 fn inner_mut(&mut self) -> &mut OutputNoteRecord {
202 self.update_type = match self.update_type {
203 NoteUpdateType::None | NoteUpdateType::Update => NoteUpdateType::Update,
204 NoteUpdateType::Insert | NoteUpdateType::InsertCommitted => NoteUpdateType::Insert,
207 };
208
209 &mut self.note
210 }
211
212 pub fn update_type(&self) -> &NoteUpdateType {
214 &self.update_type
215 }
216
217 pub fn id(&self) -> NoteId {
219 self.note.id()
220 }
221}
222
223#[derive(Clone, Debug, Default, PartialEq)]
232pub struct NoteUpdateTracker {
233 input_notes: BTreeMap<NoteDetailsCommitment, InputNoteUpdate>,
238 output_notes: BTreeMap<NoteId, OutputNoteUpdate>,
240 input_notes_by_nullifier: BTreeMap<Nullifier, NoteDetailsCommitment>,
243 input_notes_by_id: BTreeMap<NoteId, NoteDetailsCommitment>,
247 output_notes_by_nullifier: BTreeMap<Nullifier, NoteId>,
249 nullifier_order: BTreeMap<Nullifier, u32>,
253}
254
255impl NoteUpdateTracker {
256 pub fn new(
258 input_notes: impl IntoIterator<Item = InputNoteRecord>,
259 output_notes: impl IntoIterator<Item = OutputNoteRecord>,
260 ) -> Self {
261 let mut tracker = Self::default();
262 for note in input_notes {
263 tracker.insert_input_note(note, NoteUpdateType::None);
264 }
265 for note in output_notes {
266 tracker.insert_output_note(note, NoteUpdateType::None);
267 }
268
269 tracker
270 }
271
272 pub fn for_transaction_updates(
280 new_input_notes: impl IntoIterator<Item = InputNoteRecord>,
281 updated_input_notes: impl IntoIterator<Item = InputNoteRecord>,
282 new_output_notes: impl IntoIterator<Item = OutputNoteRecord>,
283 ) -> Self {
284 let mut tracker = Self::default();
285
286 for note in new_input_notes {
287 tracker.insert_input_note(note, NoteUpdateType::Insert);
288 }
289
290 for note in updated_input_notes {
291 tracker.insert_input_note(note, NoteUpdateType::Update);
292 }
293
294 for note in new_output_notes {
295 tracker.insert_output_note(note, NoteUpdateType::Insert);
296 }
297
298 tracker
299 }
300
301 pub fn updated_input_notes(&self) -> impl Iterator<Item = &InputNoteUpdate> {
315 self.input_notes.values().filter(|note| note.update_type.is_modified())
316 }
317
318 pub fn consumed_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
321 self.input_notes_by_id.iter().filter_map(|(note_id, commitment)| {
322 let update = self.input_notes.get(commitment)?;
323 (update.update_type.is_modified() && update.inner().is_consumed()).then_some(*note_id)
324 })
325 }
326
327 pub fn consumed_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
330 let output = self.output_notes.iter().filter_map(|(note_id, update)| {
331 (update.update_type.is_modified() && update.inner().is_consumed()).then_some(*note_id)
332 });
333 self.consumed_input_note_ids().chain(output)
334 }
335
336 pub fn updated_output_notes(&self) -> impl Iterator<Item = &OutputNoteUpdate> {
342 self.output_notes.values().filter(|note| note.update_type.is_modified())
343 }
344
345 pub fn is_empty(&self) -> bool {
347 self.input_notes.is_empty() && self.output_notes.is_empty()
348 }
349
350 pub fn unspent_nullifiers(&self) -> impl Iterator<Item = Nullifier> {
352 let input_note_unspent_nullifiers = self
353 .input_notes
354 .values()
355 .filter(|note| !note.inner().is_consumed())
356 .filter_map(|note| note.inner().nullifier());
357
358 let output_note_unspent_nullifiers = self
359 .output_notes
360 .values()
361 .filter(|note| !note.inner().is_consumed())
362 .filter_map(|note| note.inner().nullifier());
363
364 input_note_unspent_nullifiers.chain(output_note_unspent_nullifiers)
365 }
366
367 pub(crate) fn unspent_input_note_block_numbers(
369 &self,
370 ) -> impl Iterator<Item = BlockNumber> + '_ {
371 self.input_notes
372 .values()
373 .filter(|update| !update.inner().is_consumed())
374 .filter_map(|update| {
375 update.inner().inclusion_proof().map(|proof| proof.location().block_num())
376 })
377 }
378
379 pub(crate) fn track_existing_input_notes(
385 &mut self,
386 notes: impl IntoIterator<Item = InputNoteRecord>,
387 ) {
388 for note in notes {
389 self.insert_input_note(note, NoteUpdateType::None);
390 }
391 }
392
393 pub fn extend_nullifiers(&mut self, nullifiers: impl IntoIterator<Item = Nullifier>) {
398 for nullifier in nullifiers {
399 let next_pos =
400 u32::try_from(self.nullifier_order.len()).expect("nullifier count exceeds u32");
401 self.nullifier_order.entry(nullifier).or_insert(next_pos);
402 }
403 }
404
405 pub(crate) fn apply_new_public_note(
412 &mut self,
413 mut public_note_data: InputNoteRecord,
414 block_header: &BlockHeader,
415 ) -> Result<(), ClientError> {
416 public_note_data.block_header_received(block_header)?;
417 self.insert_input_note(public_note_data, NoteUpdateType::Insert);
418
419 Ok(())
420 }
421
422 pub(crate) fn apply_committed_note_state_transitions(
425 &mut self,
426 committed_note: &CommittedNote,
427 block_header: &BlockHeader,
428 ) -> Result<bool, ClientError> {
429 let inclusion_proof = committed_note.inclusion_proof().clone();
430 let metadata = *committed_note.metadata();
431 let note_id = *committed_note.note_id();
432 let attachments =
433 committed_note.attachments().filter(|attachments| !attachments.is_empty());
434
435 let is_tracked_as_input_note =
436 if let Some(input_note_record) = self.get_input_note_by_id(note_id) {
437 input_note_record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
438 input_note_record.block_header_received(block_header)?;
439 if let Some(attachments) = attachments {
440 input_note_record.attachments_received(attachments.clone());
441 }
442
443 true
444 } else if let Some(commitment) = self.expected_note_matching(note_id, &metadata) {
445 let nullifier = {
448 let update = self
449 .input_notes
450 .get_mut(&commitment)
451 .expect("commitment was just matched against the tracked notes");
452 let record = &mut update.note;
453 record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
454 record.block_header_received(block_header)?;
455 if let Some(attachments) = attachments {
456 record.attachments_received(attachments.clone());
457 }
458
459 update.update_type = NoteUpdateType::InsertCommitted;
463 record.nullifier().expect("note with an id has metadata")
464 };
465
466 self.input_notes_by_nullifier.insert(nullifier, commitment);
468 self.input_notes_by_id.insert(note_id, commitment);
469
470 true
471 } else {
472 false
473 };
474
475 self.try_commit_output_note(note_id, inclusion_proof)?;
476
477 Ok(is_tracked_as_input_note)
478 }
479
480 pub(crate) fn apply_output_note_inclusion_proofs(
485 &mut self,
486 committed_notes: &[CommittedNote],
487 ) -> Result<(), ClientError> {
488 for committed_note in committed_notes {
489 self.try_commit_output_note(
490 *committed_note.note_id(),
491 committed_note.inclusion_proof().clone(),
492 )?;
493 }
494 Ok(())
495 }
496
497 pub(crate) fn mark_erased_note_as_consumed(
507 &mut self,
508 note_header: &NoteHeader,
509 block_num: BlockNumber,
510 ) -> Result<(), ClientError> {
511 let note_id = note_header.id();
512
513 if let Some(output_note) = self.get_output_note_by_id(note_id)
514 && output_note.is_inclusion_pending()
515 && let Some(nullifier) = output_note.nullifier()
516 {
517 output_note.nullifier_received(nullifier, block_num)?;
518 }
519
520 if let Some(commitment) = self.input_notes_by_id.get(¬e_id).copied()
521 && let Some(input_note_update) = self.input_notes.get_mut(&commitment)
522 && !input_note_update.inner().is_consumed()
523 && let Some(nullifier) = input_note_update.inner().nullifier()
524 {
525 let consumer_account =
526 NetworkAccountTarget::try_from(input_note_update.inner().attachments())
527 .ok()
528 .map(|target| target.target_id());
529 input_note_update.inner_mut().consumed_externally(
530 nullifier,
531 block_num,
532 consumer_account,
533 )?;
534 input_note_update.inner_mut().set_consumed_tx_order(Some(0));
535 }
536
537 Ok(())
538 }
539
540 pub(crate) fn tracks_note(&self, note_id: NoteId) -> bool {
542 self.input_notes_by_id.contains_key(¬e_id) || self.output_notes.contains_key(¬e_id)
543 }
544
545 pub(crate) fn insert_consumed_public_note(
552 &mut self,
553 note: Note,
554 consumer: AccountId,
555 block_num: BlockNumber,
556 ) -> Result<(), ClientError> {
557 let note_id = note.id();
558 if self.tracks_note(note_id) {
559 return Ok(());
560 }
561 let nullifier = note.nullifier();
562 let order = self
566 .get_nullifier_order(nullifier)
567 .ok_or(ClientError::MissingConsumedNoteOrder(note_id))?;
568 let mut record = InputNoteRecord::from(note);
569 record.consumed_externally(nullifier, block_num, Some(consumer))?;
570 record.set_consumed_tx_order(Some(order));
571 self.insert_input_note(record, NoteUpdateType::Insert);
572 Ok(())
573 }
574
575 fn try_insert_consumed_input_from_output(
582 &mut self,
583 note_id: NoteId,
584 consumer: AccountId,
585 block_num: BlockNumber,
586 consumed_tx_order: Option<u32>,
587 ) -> Result<(), ClientError> {
588 if self.input_notes_by_id.contains_key(¬e_id) {
589 return Ok(());
590 }
591 let Some(output_note) = self.output_notes.get(¬e_id) else {
592 return Ok(());
593 };
594 let Ok(note) = Note::try_from(output_note.inner().clone()) else {
595 return Ok(());
596 };
597
598 let mut input_record = InputNoteRecord::from(note);
599 let nullifier =
600 input_record.nullifier().expect("record built from a full note has metadata");
601 input_record.consumed_externally(nullifier, block_num, Some(consumer))?;
602 input_record.set_consumed_tx_order(consumed_tx_order);
603 self.insert_input_note(input_record, NoteUpdateType::Insert);
604 Ok(())
605 }
606
607 fn try_commit_output_note(
610 &mut self,
611 note_id: NoteId,
612 inclusion_proof: NoteInclusionProof,
613 ) -> Result<(), ClientError> {
614 if let Some(output_note) = self.get_output_note_by_id(note_id) {
615 output_note.inclusion_proof_received(inclusion_proof)?;
616 }
617 Ok(())
618 }
619
620 pub(crate) fn apply_note_consumption<'a>(
634 &mut self,
635 consumption: &NoteConsumption,
636 mut committed_transactions: impl Iterator<Item = &'a TransactionRecord>,
637 ) -> Result<(), ClientError> {
638 let nullifier = consumption.nullifier;
639 let block_num = consumption.block_num;
640 let external_consumer = consumption.external_consumer;
641 let order = self.get_nullifier_order(nullifier);
642 let input_present = self.input_notes_by_nullifier.contains_key(&nullifier);
643
644 if let Some(input_note_update) = self.get_input_note_update_by_nullifier(nullifier) {
645 if let Some(consumer_transaction) = committed_transactions
646 .find(|t| input_note_update.inner().consumer_transaction_id() == Some(&t.id))
647 {
648 if let TransactionStatus::Committed { block_number, .. } =
650 consumer_transaction.status
651 {
652 input_note_update
653 .inner_mut()
654 .transaction_committed(consumer_transaction.id, block_number)?;
655 }
656 } else {
657 input_note_update.inner_mut().consumed_externally(
660 nullifier,
661 block_num,
662 external_consumer,
663 )?;
664 }
665 input_note_update.inner_mut().set_consumed_tx_order(order);
666 }
667
668 if let Some(output_note_record) = self.get_output_note_by_nullifier(nullifier) {
669 output_note_record.nullifier_received(nullifier, block_num)?;
670 }
671
672 if !input_present
673 && let Some(consumer) = external_consumer
674 && let Some(note_id) = self.output_notes_by_nullifier.get(&nullifier).copied()
675 {
676 self.try_insert_consumed_input_from_output(note_id, consumer, block_num, order)?;
677 }
678
679 Ok(())
680 }
681
682 fn get_nullifier_order(&self, nullifier: Nullifier) -> Option<u32> {
688 self.nullifier_order.get(&nullifier).copied()
689 }
690
691 fn get_input_note_by_id(&mut self, note_id: NoteId) -> Option<&mut InputNoteRecord> {
693 let commitment = self.input_notes_by_id.get(¬e_id).copied()?;
694 self.input_notes.get_mut(&commitment).map(InputNoteUpdate::inner_mut)
695 }
696
697 fn expected_note_matching(
700 &self,
701 note_id: NoteId,
702 metadata: &NoteMetadata,
703 ) -> Option<NoteDetailsCommitment> {
704 self.input_notes
705 .iter()
706 .filter(|(_, update)| update.inner().metadata().is_none())
707 .map(|(commitment, _)| *commitment)
708 .find(|commitment| NoteId::new(*commitment, metadata) == note_id)
709 }
710
711 fn get_output_note_by_id(&mut self, note_id: NoteId) -> Option<&mut OutputNoteRecord> {
713 self.output_notes.get_mut(¬e_id).map(OutputNoteUpdate::inner_mut)
714 }
715
716 fn get_input_note_update_by_nullifier(
719 &mut self,
720 nullifier: Nullifier,
721 ) -> Option<&mut InputNoteUpdate> {
722 let commitment = self.input_notes_by_nullifier.get(&nullifier).copied()?;
723 self.input_notes.get_mut(&commitment)
724 }
725
726 fn get_output_note_by_nullifier(
729 &mut self,
730 nullifier: Nullifier,
731 ) -> Option<&mut OutputNoteRecord> {
732 let note_id = self.output_notes_by_nullifier.get(&nullifier).copied()?;
733 self.output_notes.get_mut(¬e_id).map(OutputNoteUpdate::inner_mut)
734 }
735
736 fn insert_input_note(&mut self, note: InputNoteRecord, update_type: NoteUpdateType) {
738 let update = match update_type {
739 NoteUpdateType::None => InputNoteUpdate::new_none(note),
740 NoteUpdateType::Insert => InputNoteUpdate::new_insert(note),
741 NoteUpdateType::Update => InputNoteUpdate::new_update(note),
742 NoteUpdateType::InsertCommitted => InputNoteUpdate::new_insert_committed(note),
743 };
744
745 let commitment = update.inner().details_commitment();
746 if let Some(note_id) = update.inner().id() {
747 let nullifier = update.inner().nullifier().expect("note with an id has metadata");
749 self.input_notes_by_nullifier.insert(nullifier, commitment);
750 self.input_notes_by_id.insert(note_id, commitment);
751 self.input_notes.insert(commitment, update);
752 } else if self.input_notes.get(&commitment).is_none_or(|u| u.inner().id().is_none()) {
753 self.input_notes.insert(commitment, update);
757 }
758 }
759
760 fn insert_output_note(&mut self, note: OutputNoteRecord, update_type: NoteUpdateType) {
762 let note_id = note.id();
763 if let Some(nullifier) = note.nullifier() {
764 self.output_notes_by_nullifier.insert(nullifier, note_id);
765 }
766 let update = match update_type {
767 NoteUpdateType::None => OutputNoteUpdate::new_none(note),
768 NoteUpdateType::Update => OutputNoteUpdate::new_update(note),
769 NoteUpdateType::Insert | NoteUpdateType::InsertCommitted => {
772 OutputNoteUpdate::new_insert(note)
773 },
774 };
775 self.output_notes.insert(note_id, update);
776 }
777}
778
779impl Serializable for NoteUpdateType {
783 fn write_into<W: ByteWriter>(&self, target: &mut W) {
784 target.write_u8(*self as u8);
785 }
786}
787
788impl Deserializable for NoteUpdateType {
789 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
790 NoteUpdateType::try_from(source.read_u8()?).map_err(|val| {
791 DeserializationError::InvalidValue(format!("invalid note update type: {val}"))
792 })
793 }
794}
795
796impl Serializable for InputNoteUpdate {
797 fn write_into<W: ByteWriter>(&self, target: &mut W) {
798 self.note.write_into(target);
799 self.update_type.write_into(target);
800 }
801}
802
803impl Deserializable for InputNoteUpdate {
804 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
805 let note = InputNoteRecord::read_from(source)?;
806 let update_type = NoteUpdateType::read_from(source)?;
807 Ok(Self { note, update_type })
808 }
809}
810
811impl Serializable for OutputNoteUpdate {
812 fn write_into<W: ByteWriter>(&self, target: &mut W) {
813 self.note.write_into(target);
814 self.update_type.write_into(target);
815 }
816}
817
818impl Deserializable for OutputNoteUpdate {
819 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
820 let note = OutputNoteRecord::read_from(source)?;
821 let update_type = NoteUpdateType::read_from(source)?;
822 Ok(Self { note, update_type })
823 }
824}
825
826impl Serializable for NoteUpdateTracker {
827 fn write_into<W: ByteWriter>(&self, target: &mut W) {
828 self.input_notes.write_into(target);
831 self.output_notes.write_into(target);
832 self.nullifier_order.write_into(target);
833 self.input_notes_by_id.write_into(target);
834 self.input_notes_by_nullifier.write_into(target);
835 }
836}
837
838impl Deserializable for NoteUpdateTracker {
839 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
840 let input_notes = BTreeMap::<NoteDetailsCommitment, InputNoteUpdate>::read_from(source)?;
841 let output_notes = BTreeMap::<NoteId, OutputNoteUpdate>::read_from(source)?;
842 let nullifier_order = BTreeMap::<Nullifier, u32>::read_from(source)?;
843 let input_notes_by_id = BTreeMap::<NoteId, NoteDetailsCommitment>::read_from(source)?;
844 let input_notes_by_nullifier =
845 BTreeMap::<Nullifier, NoteDetailsCommitment>::read_from(source)?;
846
847 let output_notes_by_nullifier = output_notes
849 .iter()
850 .filter_map(|(note_id, update)| {
851 update.inner().nullifier().map(|nullifier| (nullifier, *note_id))
852 })
853 .collect();
854
855 Ok(Self {
856 input_notes,
857 output_notes,
858 input_notes_by_nullifier,
859 input_notes_by_id,
860 output_notes_by_nullifier,
861 nullifier_order,
862 })
863 }
864}
865
866#[cfg(test)]
870mod tests {
871 use alloc::vec;
872
873 use miden_protocol::account::AccountId;
874 use miden_protocol::block::BlockNumber;
875 use miden_protocol::note::{
876 NoteAssets,
877 NoteAttachments,
878 NoteDetails,
879 NoteId,
880 NoteMetadata,
881 NoteRecipient,
882 NoteStorage,
883 NoteType,
884 PartialNoteMetadata,
885 };
886 use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER;
887 use miden_protocol::transaction::TransactionId;
888 use miden_protocol::utils::serde::{Deserializable, Serializable};
889 use miden_protocol::{Felt, Word, ZERO};
890 use miden_standards::note::StandardNote;
891
892 use super::{NoteConsumption, NoteUpdateTracker};
893 use crate::store::InputNoteRecord;
894 use crate::store::input_note_states::{
895 ConsumedExternalNoteState,
896 ConsumedUnauthenticatedLocalNoteState,
897 ExpectedNoteState,
898 NoteSubmissionData,
899 ProcessingUnauthenticatedNoteState,
900 };
901 use crate::transaction::TransactionRecord;
902
903 fn note_details(seed: u64) -> NoteDetails {
907 let serial_number: Word = [Felt::new_unchecked(seed), ZERO, ZERO, ZERO].into();
908 let recipient = NoteRecipient::new(
909 serial_number,
910 StandardNote::SWAP.script(),
911 NoteStorage::new(vec![]).unwrap(),
912 );
913 NoteDetails::new(NoteAssets::new(vec![]).unwrap(), recipient)
914 }
915
916 fn note_metadata(sender: AccountId) -> NoteMetadata {
917 NoteMetadata::new(
918 PartialNoteMetadata::new(sender, NoteType::Public),
919 &NoteAttachments::empty(),
920 )
921 }
922
923 fn expected_note(seed: u64) -> InputNoteRecord {
925 let state = ExpectedNoteState {
926 metadata: None,
927 after_block_num: BlockNumber::from(0u32),
928 tag: None,
929 };
930 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
931 }
932
933 fn processing_note(seed: u64, sender: AccountId) -> InputNoteRecord {
935 let state = ProcessingUnauthenticatedNoteState {
936 metadata: note_metadata(sender),
937 after_block_num: BlockNumber::from(0u32),
938 submission_data: NoteSubmissionData {
939 submitted_at: Some(0),
940 consumer_account: sender,
941 consumer_transaction: TransactionId::from_raw(Word::default()),
942 },
943 };
944 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
945 }
946
947 fn consumed_local_note(seed: u64, sender: AccountId) -> InputNoteRecord {
949 let state = ConsumedUnauthenticatedLocalNoteState {
950 metadata: note_metadata(sender),
951 nullifier_block_height: BlockNumber::from(1u32),
952 submission_data: NoteSubmissionData {
953 submitted_at: Some(0),
954 consumer_account: sender,
955 consumer_transaction: TransactionId::from_raw(Word::default()),
956 },
957 consumed_tx_order: Some(0),
958 };
959 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
960 }
961
962 fn consumed_external_note(seed: u64) -> InputNoteRecord {
964 let state = ConsumedExternalNoteState {
965 nullifier_block_height: BlockNumber::from(1u32),
966 consumer_account: None,
967 consumed_tx_order: None,
968 metadata: None,
969 };
970 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
971 }
972
973 #[test]
977 fn consumed_input_note_ids_reports_metadata_bearing_consumed_note() {
978 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
979 let note = consumed_local_note(1, sender);
980 let id = note.id().expect("consumed-local note has metadata");
981
982 let tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
983
984 let consumed: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
985 assert_eq!(consumed, vec![id]);
986 }
987
988 #[test]
989 fn consumed_input_note_ids_omits_note_that_never_had_an_id() {
990 let note = consumed_external_note(2);
993 assert!(note.id().is_none());
994
995 let tracker = NoteUpdateTracker::for_transaction_updates(vec![note], vec![], vec![]);
996
997 assert_eq!(tracker.consumed_input_note_ids().count(), 0);
998 assert_eq!(tracker.updated_input_notes().count(), 1);
999 }
1000
1001 #[test]
1002 fn external_consumption_retains_note_id() {
1003 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1004 let note = processing_note(3, sender);
1005 let id = note.id().expect("processing note has metadata");
1006 let nullifier = note.nullifier().expect("processing note has metadata");
1007
1008 let mut tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
1009 assert_eq!(tracker.consumed_input_note_ids().count(), 0);
1010
1011 tracker
1013 .apply_note_consumption(
1014 &NoteConsumption {
1015 nullifier,
1016 block_num: BlockNumber::from(5u32),
1017 external_consumer: None,
1018 },
1019 core::iter::empty::<&TransactionRecord>(),
1020 )
1021 .expect("external consumption should apply");
1022
1023 let consumed: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
1024 assert_eq!(
1025 consumed,
1026 vec![id],
1027 "an externally consumed note must still be reported by its id"
1028 );
1029 }
1030
1031 #[test]
1032 fn externally_consumed_note_id_survives_round_trip() {
1033 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1034 let note = processing_note(12, sender);
1035 let id = note.id().expect("processing note has metadata");
1036 let nullifier = note.nullifier().expect("processing note has metadata");
1037
1038 let mut tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
1039
1040 tracker
1043 .apply_note_consumption(
1044 &NoteConsumption {
1045 nullifier,
1046 block_num: BlockNumber::from(5u32),
1047 external_consumer: None,
1048 },
1049 core::iter::empty::<&TransactionRecord>(),
1050 )
1051 .expect("external consumption should apply");
1052
1053 let before: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
1055 assert_eq!(before, vec![id]);
1056
1057 let bytes = tracker.to_bytes();
1059 let restored = NoteUpdateTracker::read_from_bytes(&bytes).expect("round-trip should work");
1060 let after: alloc::vec::Vec<NoteId> = restored.consumed_input_note_ids().collect();
1061 assert_eq!(
1062 after,
1063 vec![id],
1064 "the retained id of an externally consumed note must survive serialization"
1065 );
1066 }
1067
1068 #[test]
1069 fn serialize_round_trip_preserves_lookup_indices() {
1070 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1071 let expected = expected_note(10);
1072 let processing = processing_note(11, sender);
1073 let processing_id = processing.id().expect("processing note has metadata");
1074 let processing_commitment = processing.details_commitment();
1075 let processing_nullifier = processing.nullifier().expect("processing note has metadata");
1076
1077 let tracker =
1078 NoteUpdateTracker::for_transaction_updates(vec![expected], vec![processing], vec![]);
1079
1080 let bytes = tracker.to_bytes();
1081 let restored = NoteUpdateTracker::read_from_bytes(&bytes).expect("round-trip should work");
1082
1083 assert_eq!(tracker, restored);
1086 assert_eq!(restored.updated_input_notes().count(), 2);
1087 assert_eq!(
1088 restored.input_notes_by_id.get(&processing_id).copied(),
1089 Some(processing_commitment)
1090 );
1091 assert_eq!(
1092 restored.input_notes_by_nullifier.get(&processing_nullifier).copied(),
1093 Some(processing_commitment)
1094 );
1095 }
1096}