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 fn extend_nullifiers(&mut self, nullifiers: impl IntoIterator<Item = Nullifier>) {
373 for nullifier in nullifiers {
374 let next_pos =
375 u32::try_from(self.nullifier_order.len()).expect("nullifier count exceeds u32");
376 self.nullifier_order.entry(nullifier).or_insert(next_pos);
377 }
378 }
379
380 pub(crate) fn apply_new_public_note(
387 &mut self,
388 mut public_note_data: InputNoteRecord,
389 block_header: &BlockHeader,
390 ) -> Result<(), ClientError> {
391 public_note_data.block_header_received(block_header)?;
392 self.insert_input_note(public_note_data, NoteUpdateType::Insert);
393
394 Ok(())
395 }
396
397 pub(crate) fn apply_committed_note_state_transitions(
400 &mut self,
401 committed_note: &CommittedNote,
402 block_header: &BlockHeader,
403 attachments: Option<&NoteAttachments>,
404 ) -> Result<bool, ClientError> {
405 let inclusion_proof = committed_note.inclusion_proof().clone();
406 let metadata = *committed_note.metadata();
407 let note_id = *committed_note.note_id();
408
409 let is_tracked_as_input_note = if let Some(input_note_record) =
410 self.get_input_note_by_id(note_id)
411 {
412 input_note_record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
413 input_note_record.block_header_received(block_header)?;
414 if let Some(attachments) = attachments {
415 input_note_record.set_attachments(attachments.clone());
416 }
417
418 true
419 } else if let Some(commitment) = self.expected_note_matching(note_id, &metadata) {
420 let nullifier = {
423 let update = self
424 .input_notes
425 .get_mut(&commitment)
426 .expect("commitment was just matched against the tracked notes");
427 let record = &mut update.note;
428 record.inclusion_proof_received(inclusion_proof.clone(), metadata)?;
429 record.block_header_received(block_header)?;
430 if let Some(attachments) = attachments {
431 record.set_attachments(attachments.clone());
432 }
433
434 update.update_type = NoteUpdateType::InsertCommitted;
438 record.nullifier().expect("note with an id has metadata")
439 };
440
441 self.input_notes_by_nullifier.insert(nullifier, commitment);
443 self.input_notes_by_id.insert(note_id, commitment);
444
445 true
446 } else {
447 false
448 };
449
450 self.try_commit_output_note(note_id, inclusion_proof)?;
451
452 Ok(is_tracked_as_input_note)
453 }
454
455 pub(crate) fn apply_output_note_inclusion_proofs(
460 &mut self,
461 committed_notes: &[CommittedNote],
462 ) -> Result<(), ClientError> {
463 for committed_note in committed_notes {
464 self.try_commit_output_note(
465 *committed_note.note_id(),
466 committed_note.inclusion_proof().clone(),
467 )?;
468 }
469 Ok(())
470 }
471
472 pub(crate) fn mark_erased_note_as_consumed(
482 &mut self,
483 note_header: &NoteHeader,
484 block_num: BlockNumber,
485 ) -> Result<(), ClientError> {
486 let note_id = note_header.id();
487
488 if let Some(output_note) = self.get_output_note_by_id(note_id)
489 && !output_note.is_consumed()
490 && !output_note.is_committed()
491 && let Some(nullifier) = output_note.nullifier()
492 {
493 output_note.nullifier_received(nullifier, block_num)?;
494 }
495
496 if let Some(commitment) = self.input_notes_by_id.get(¬e_id).copied()
497 && let Some(input_note_update) = self.input_notes.get_mut(&commitment)
498 && !input_note_update.inner().is_consumed()
499 && let Some(nullifier) = input_note_update.inner().nullifier()
500 {
501 let consumer_account =
502 NetworkAccountTarget::try_from(input_note_update.inner().attachments())
503 .ok()
504 .map(|target| target.target_id());
505 input_note_update.inner_mut().consumed_externally(
506 nullifier,
507 block_num,
508 consumer_account,
509 )?;
510 input_note_update.inner_mut().set_consumed_tx_order(Some(0));
511 }
512
513 Ok(())
514 }
515
516 fn try_insert_consumed_input_from_output(
523 &mut self,
524 note_id: NoteId,
525 consumer: AccountId,
526 block_num: BlockNumber,
527 consumed_tx_order: Option<u32>,
528 ) -> Result<(), ClientError> {
529 if self.input_notes_by_id.contains_key(¬e_id) {
530 return Ok(());
531 }
532 let Some(output_note) = self.output_notes.get(¬e_id) else {
533 return Ok(());
534 };
535 let Ok(note) = Note::try_from(output_note.inner().clone()) else {
536 return Ok(());
537 };
538
539 let mut input_record = InputNoteRecord::from(note);
540 let nullifier =
541 input_record.nullifier().expect("record built from a full note has metadata");
542 input_record.consumed_externally(nullifier, block_num, Some(consumer))?;
543 input_record.set_consumed_tx_order(consumed_tx_order);
544 self.insert_input_note(input_record, NoteUpdateType::Insert);
545 Ok(())
546 }
547
548 fn try_commit_output_note(
551 &mut self,
552 note_id: NoteId,
553 inclusion_proof: NoteInclusionProof,
554 ) -> Result<(), ClientError> {
555 if let Some(output_note) = self.get_output_note_by_id(note_id) {
556 output_note.inclusion_proof_received(inclusion_proof)?;
557 }
558 Ok(())
559 }
560
561 pub(crate) fn apply_note_consumption<'a>(
575 &mut self,
576 consumption: &NoteConsumption,
577 mut committed_transactions: impl Iterator<Item = &'a TransactionRecord>,
578 ) -> Result<(), ClientError> {
579 let nullifier = consumption.nullifier;
580 let block_num = consumption.block_num;
581 let external_consumer = consumption.external_consumer;
582 let order = self.get_nullifier_order(nullifier);
583 let input_present = self.input_notes_by_nullifier.contains_key(&nullifier);
584
585 if let Some(input_note_update) = self.get_input_note_update_by_nullifier(nullifier) {
586 if let Some(consumer_transaction) = committed_transactions
587 .find(|t| input_note_update.inner().consumer_transaction_id() == Some(&t.id))
588 {
589 if let TransactionStatus::Committed { block_number, .. } =
591 consumer_transaction.status
592 {
593 input_note_update
594 .inner_mut()
595 .transaction_committed(consumer_transaction.id, block_number)?;
596 }
597 } else {
598 input_note_update.inner_mut().consumed_externally(
601 nullifier,
602 block_num,
603 external_consumer,
604 )?;
605 }
606 input_note_update.inner_mut().set_consumed_tx_order(order);
607 }
608
609 if let Some(output_note_record) = self.get_output_note_by_nullifier(nullifier) {
610 output_note_record.nullifier_received(nullifier, block_num)?;
611 }
612
613 if !input_present
614 && let Some(consumer) = external_consumer
615 && let Some(note_id) = self.output_notes_by_nullifier.get(&nullifier).copied()
616 {
617 self.try_insert_consumed_input_from_output(note_id, consumer, block_num, order)?;
618 }
619
620 Ok(())
621 }
622
623 fn get_nullifier_order(&self, nullifier: Nullifier) -> Option<u32> {
629 self.nullifier_order.get(&nullifier).copied()
630 }
631
632 fn get_input_note_by_id(&mut self, note_id: NoteId) -> Option<&mut InputNoteRecord> {
634 let commitment = self.input_notes_by_id.get(¬e_id).copied()?;
635 self.input_notes.get_mut(&commitment).map(InputNoteUpdate::inner_mut)
636 }
637
638 fn expected_note_matching(
641 &self,
642 note_id: NoteId,
643 metadata: &NoteMetadata,
644 ) -> Option<NoteDetailsCommitment> {
645 self.input_notes
646 .iter()
647 .filter(|(_, update)| update.inner().metadata().is_none())
648 .map(|(commitment, _)| *commitment)
649 .find(|commitment| NoteId::new(*commitment, metadata) == note_id)
650 }
651
652 fn get_output_note_by_id(&mut self, note_id: NoteId) -> Option<&mut OutputNoteRecord> {
654 self.output_notes.get_mut(¬e_id).map(OutputNoteUpdate::inner_mut)
655 }
656
657 fn get_input_note_update_by_nullifier(
660 &mut self,
661 nullifier: Nullifier,
662 ) -> Option<&mut InputNoteUpdate> {
663 let commitment = self.input_notes_by_nullifier.get(&nullifier).copied()?;
664 self.input_notes.get_mut(&commitment)
665 }
666
667 fn get_output_note_by_nullifier(
670 &mut self,
671 nullifier: Nullifier,
672 ) -> Option<&mut OutputNoteRecord> {
673 let note_id = self.output_notes_by_nullifier.get(&nullifier).copied()?;
674 self.output_notes.get_mut(¬e_id).map(OutputNoteUpdate::inner_mut)
675 }
676
677 fn insert_input_note(&mut self, note: InputNoteRecord, update_type: NoteUpdateType) {
679 let update = match update_type {
680 NoteUpdateType::None => InputNoteUpdate::new_none(note),
681 NoteUpdateType::Insert => InputNoteUpdate::new_insert(note),
682 NoteUpdateType::Update => InputNoteUpdate::new_update(note),
683 NoteUpdateType::InsertCommitted => InputNoteUpdate::new_insert_committed(note),
684 };
685
686 let commitment = update.inner().details_commitment();
687 if let Some(note_id) = update.inner().id() {
688 let nullifier = update.inner().nullifier().expect("note with an id has metadata");
690 self.input_notes_by_nullifier.insert(nullifier, commitment);
691 self.input_notes_by_id.insert(note_id, commitment);
692 self.input_notes.insert(commitment, update);
693 } else if self.input_notes.get(&commitment).is_none_or(|u| u.inner().id().is_none()) {
694 self.input_notes.insert(commitment, update);
698 }
699 }
700
701 fn insert_output_note(&mut self, note: OutputNoteRecord, update_type: NoteUpdateType) {
703 let note_id = note.id();
704 if let Some(nullifier) = note.nullifier() {
705 self.output_notes_by_nullifier.insert(nullifier, note_id);
706 }
707 let update = match update_type {
708 NoteUpdateType::None => OutputNoteUpdate::new_none(note),
709 NoteUpdateType::Update => OutputNoteUpdate::new_update(note),
710 NoteUpdateType::Insert | NoteUpdateType::InsertCommitted => {
713 OutputNoteUpdate::new_insert(note)
714 },
715 };
716 self.output_notes.insert(note_id, update);
717 }
718}
719
720impl Serializable for NoteUpdateType {
724 fn write_into<W: ByteWriter>(&self, target: &mut W) {
725 target.write_u8(*self as u8);
726 }
727}
728
729impl Deserializable for NoteUpdateType {
730 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
731 NoteUpdateType::try_from(source.read_u8()?).map_err(|val| {
732 DeserializationError::InvalidValue(format!("invalid note update type: {val}"))
733 })
734 }
735}
736
737impl Serializable for InputNoteUpdate {
738 fn write_into<W: ByteWriter>(&self, target: &mut W) {
739 self.note.write_into(target);
740 self.update_type.write_into(target);
741 }
742}
743
744impl Deserializable for InputNoteUpdate {
745 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
746 let note = InputNoteRecord::read_from(source)?;
747 let update_type = NoteUpdateType::read_from(source)?;
748 Ok(Self { note, update_type })
749 }
750}
751
752impl Serializable for OutputNoteUpdate {
753 fn write_into<W: ByteWriter>(&self, target: &mut W) {
754 self.note.write_into(target);
755 self.update_type.write_into(target);
756 }
757}
758
759impl Deserializable for OutputNoteUpdate {
760 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
761 let note = OutputNoteRecord::read_from(source)?;
762 let update_type = NoteUpdateType::read_from(source)?;
763 Ok(Self { note, update_type })
764 }
765}
766
767impl Serializable for NoteUpdateTracker {
768 fn write_into<W: ByteWriter>(&self, target: &mut W) {
769 self.input_notes.write_into(target);
772 self.output_notes.write_into(target);
773 self.nullifier_order.write_into(target);
774 self.input_notes_by_id.write_into(target);
775 self.input_notes_by_nullifier.write_into(target);
776 }
777}
778
779impl Deserializable for NoteUpdateTracker {
780 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
781 let input_notes = BTreeMap::<NoteDetailsCommitment, InputNoteUpdate>::read_from(source)?;
782 let output_notes = BTreeMap::<NoteId, OutputNoteUpdate>::read_from(source)?;
783 let nullifier_order = BTreeMap::<Nullifier, u32>::read_from(source)?;
784 let input_notes_by_id = BTreeMap::<NoteId, NoteDetailsCommitment>::read_from(source)?;
785 let input_notes_by_nullifier =
786 BTreeMap::<Nullifier, NoteDetailsCommitment>::read_from(source)?;
787
788 let output_notes_by_nullifier = output_notes
790 .iter()
791 .filter_map(|(note_id, update)| {
792 update.inner().nullifier().map(|nullifier| (nullifier, *note_id))
793 })
794 .collect();
795
796 Ok(Self {
797 input_notes,
798 output_notes,
799 input_notes_by_nullifier,
800 input_notes_by_id,
801 output_notes_by_nullifier,
802 nullifier_order,
803 })
804 }
805}
806
807#[cfg(test)]
811mod tests {
812 use alloc::vec;
813
814 use miden_protocol::account::AccountId;
815 use miden_protocol::block::BlockNumber;
816 use miden_protocol::note::{
817 NoteAssets,
818 NoteAttachments,
819 NoteDetails,
820 NoteId,
821 NoteMetadata,
822 NoteRecipient,
823 NoteStorage,
824 NoteType,
825 PartialNoteMetadata,
826 };
827 use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER;
828 use miden_protocol::transaction::TransactionId;
829 use miden_protocol::utils::serde::{Deserializable, Serializable};
830 use miden_protocol::{Felt, Word, ZERO};
831 use miden_standards::note::StandardNote;
832
833 use super::{NoteConsumption, NoteUpdateTracker};
834 use crate::store::InputNoteRecord;
835 use crate::store::input_note_states::{
836 ConsumedExternalNoteState,
837 ConsumedUnauthenticatedLocalNoteState,
838 ExpectedNoteState,
839 NoteSubmissionData,
840 ProcessingUnauthenticatedNoteState,
841 };
842 use crate::transaction::TransactionRecord;
843
844 fn note_details(seed: u64) -> NoteDetails {
848 let serial_number: Word = [Felt::new_unchecked(seed), ZERO, ZERO, ZERO].into();
849 let recipient = NoteRecipient::new(
850 serial_number,
851 StandardNote::SWAP.script(),
852 NoteStorage::new(vec![]).unwrap(),
853 );
854 NoteDetails::new(NoteAssets::new(vec![]).unwrap(), recipient)
855 }
856
857 fn note_metadata(sender: AccountId) -> NoteMetadata {
858 NoteMetadata::new(
859 PartialNoteMetadata::new(sender, NoteType::Public),
860 &NoteAttachments::empty(),
861 )
862 }
863
864 fn expected_note(seed: u64) -> InputNoteRecord {
866 let state = ExpectedNoteState {
867 metadata: None,
868 after_block_num: BlockNumber::from(0u32),
869 tag: None,
870 };
871 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
872 }
873
874 fn processing_note(seed: u64, sender: AccountId) -> InputNoteRecord {
876 let state = ProcessingUnauthenticatedNoteState {
877 metadata: note_metadata(sender),
878 after_block_num: BlockNumber::from(0u32),
879 submission_data: NoteSubmissionData {
880 submitted_at: Some(0),
881 consumer_account: sender,
882 consumer_transaction: TransactionId::from_raw(Word::default()),
883 },
884 };
885 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
886 }
887
888 fn consumed_local_note(seed: u64, sender: AccountId) -> InputNoteRecord {
890 let state = ConsumedUnauthenticatedLocalNoteState {
891 metadata: note_metadata(sender),
892 nullifier_block_height: BlockNumber::from(1u32),
893 submission_data: NoteSubmissionData {
894 submitted_at: Some(0),
895 consumer_account: sender,
896 consumer_transaction: TransactionId::from_raw(Word::default()),
897 },
898 consumed_tx_order: Some(0),
899 };
900 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
901 }
902
903 fn consumed_external_note(seed: u64) -> InputNoteRecord {
905 let state = ConsumedExternalNoteState {
906 nullifier_block_height: BlockNumber::from(1u32),
907 consumer_account: None,
908 consumed_tx_order: None,
909 metadata: None,
910 };
911 InputNoteRecord::new(note_details(seed), NoteAttachments::empty(), Some(0), state.into())
912 }
913
914 #[test]
918 fn consumed_input_note_ids_reports_metadata_bearing_consumed_note() {
919 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
920 let note = consumed_local_note(1, sender);
921 let id = note.id().expect("consumed-local note has metadata");
922
923 let tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
924
925 let consumed: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
926 assert_eq!(consumed, vec![id]);
927 }
928
929 #[test]
930 fn consumed_input_note_ids_omits_note_that_never_had_an_id() {
931 let note = consumed_external_note(2);
934 assert!(note.id().is_none());
935
936 let tracker = NoteUpdateTracker::for_transaction_updates(vec![note], vec![], vec![]);
937
938 assert_eq!(tracker.consumed_input_note_ids().count(), 0);
939 assert_eq!(tracker.updated_input_notes().count(), 1);
940 }
941
942 #[test]
943 fn external_consumption_retains_note_id() {
944 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
945 let note = processing_note(3, sender);
946 let id = note.id().expect("processing note has metadata");
947 let nullifier = note.nullifier().expect("processing note has metadata");
948
949 let mut tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
950 assert_eq!(tracker.consumed_input_note_ids().count(), 0);
951
952 tracker
954 .apply_note_consumption(
955 &NoteConsumption {
956 nullifier,
957 block_num: BlockNumber::from(5u32),
958 external_consumer: None,
959 },
960 core::iter::empty::<&TransactionRecord>(),
961 )
962 .expect("external consumption should apply");
963
964 let consumed: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
965 assert_eq!(
966 consumed,
967 vec![id],
968 "an externally consumed note must still be reported by its id"
969 );
970 }
971
972 #[test]
973 fn externally_consumed_note_id_survives_round_trip() {
974 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
975 let note = processing_note(12, sender);
976 let id = note.id().expect("processing note has metadata");
977 let nullifier = note.nullifier().expect("processing note has metadata");
978
979 let mut tracker = NoteUpdateTracker::for_transaction_updates(vec![], vec![note], vec![]);
980
981 tracker
984 .apply_note_consumption(
985 &NoteConsumption {
986 nullifier,
987 block_num: BlockNumber::from(5u32),
988 external_consumer: None,
989 },
990 core::iter::empty::<&TransactionRecord>(),
991 )
992 .expect("external consumption should apply");
993
994 let before: alloc::vec::Vec<NoteId> = tracker.consumed_input_note_ids().collect();
996 assert_eq!(before, vec![id]);
997
998 let bytes = tracker.to_bytes();
1000 let restored = NoteUpdateTracker::read_from_bytes(&bytes).expect("round-trip should work");
1001 let after: alloc::vec::Vec<NoteId> = restored.consumed_input_note_ids().collect();
1002 assert_eq!(
1003 after,
1004 vec![id],
1005 "the retained id of an externally consumed note must survive serialization"
1006 );
1007 }
1008
1009 #[test]
1010 fn serialize_round_trip_preserves_lookup_indices() {
1011 let sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1012 let expected = expected_note(10);
1013 let processing = processing_note(11, sender);
1014 let processing_id = processing.id().expect("processing note has metadata");
1015 let processing_commitment = processing.details_commitment();
1016 let processing_nullifier = processing.nullifier().expect("processing note has metadata");
1017
1018 let tracker =
1019 NoteUpdateTracker::for_transaction_updates(vec![expected], vec![processing], vec![]);
1020
1021 let bytes = tracker.to_bytes();
1022 let restored = NoteUpdateTracker::read_from_bytes(&bytes).expect("round-trip should work");
1023
1024 assert_eq!(tracker, restored);
1027 assert_eq!(restored.updated_input_notes().count(), 2);
1028 assert_eq!(
1029 restored.input_notes_by_id.get(&processing_id).copied(),
1030 Some(processing_commitment)
1031 );
1032 assert_eq!(
1033 restored.input_notes_by_nullifier.get(&processing_nullifier).copied(),
1034 Some(processing_commitment)
1035 );
1036 }
1037}