Skip to main content

miden_protocol/transaction/outputs/
notes.rs

1use alloc::collections::BTreeSet;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4use core::fmt::Debug;
5
6use crate::constants::NOTE_MAX_SIZE;
7use crate::errors::{OutputNoteError, TransactionOutputError};
8use crate::note::{
9    Note,
10    NoteAssets,
11    NoteHeader,
12    NoteId,
13    NoteMetadata,
14    NoteRecipient,
15    PartialNote,
16    compute_note_commitment,
17};
18use crate::utils::serde::{
19    ByteReader,
20    ByteWriter,
21    Deserializable,
22    DeserializationError,
23    Serializable,
24};
25use crate::{Felt, Hasher, MAX_OUTPUT_NOTES_PER_TX, Word};
26
27// OUTPUT NOTE COLLECTION
28// ================================================================================================
29
30/// Contains a list of output notes of a transaction. The list can be empty if the transaction does
31/// not produce any notes.
32///
33/// This struct is generic over the note type `N`, allowing it to be used with both
34/// [`RawOutputNote`] (in [`ExecutedTransaction`](crate::transaction::ExecutedTransaction)) and
35/// [`OutputNote`] (in [`ProvenTransaction`](crate::transaction::ProvenTransaction)).
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct OutputNoteCollection<N> {
38    notes: Vec<N>,
39    commitment: Word,
40}
41
42impl<N> OutputNoteCollection<N>
43where
44    for<'a> &'a NoteHeader: From<&'a N>,
45    for<'a> NoteId: From<&'a N>,
46{
47    // CONSTRUCTOR
48    // --------------------------------------------------------------------------------------------
49
50    /// Returns new [OutputNoteCollection] instantiated from the provided vector of notes.
51    ///
52    /// # Errors
53    /// Returns an error if:
54    /// - The total number of notes is greater than [`MAX_OUTPUT_NOTES_PER_TX`].
55    /// - The vector of notes contains duplicates.
56    pub fn new(notes: Vec<N>) -> Result<Self, TransactionOutputError> {
57        if notes.len() > MAX_OUTPUT_NOTES_PER_TX {
58            return Err(TransactionOutputError::TooManyOutputNotes(notes.len()));
59        }
60
61        let mut seen_notes = BTreeSet::new();
62        for note in notes.iter() {
63            let note_id = NoteId::from(note);
64            if !seen_notes.insert(note_id) {
65                return Err(TransactionOutputError::DuplicateOutputNote(note_id));
66            }
67        }
68
69        let commitment = Self::compute_commitment(notes.iter().map(<&NoteHeader>::from));
70
71        Ok(Self { notes, commitment })
72    }
73
74    // PUBLIC ACCESSORS
75    // --------------------------------------------------------------------------------------------
76
77    /// Returns the commitment to the output notes.
78    ///
79    /// The commitment is computed as a sequential hash of (note ID, metadata) tuples for the notes
80    /// created in a transaction.
81    pub fn commitment(&self) -> Word {
82        self.commitment
83    }
84
85    /// Returns total number of output notes.
86    pub fn num_notes(&self) -> usize {
87        self.notes.len()
88    }
89
90    /// Returns true if this [OutputNoteCollection] does not contain any notes.
91    pub fn is_empty(&self) -> bool {
92        self.notes.is_empty()
93    }
94
95    /// Returns a reference to the note located at the specified index.
96    pub fn get_note(&self, idx: usize) -> &N {
97        &self.notes[idx]
98    }
99
100    // ITERATORS
101    // --------------------------------------------------------------------------------------------
102
103    /// Returns an iterator over notes in this [OutputNoteCollection].
104    pub fn iter(&self) -> impl Iterator<Item = &N> {
105        self.notes.iter()
106    }
107
108    // HELPERS
109    // --------------------------------------------------------------------------------------------
110
111    /// Computes a commitment to output notes.
112    ///
113    /// - For an empty list, [`Word::empty`] is returned.
114    /// - For a non-empty list of notes, this is a sequential hash of (note_id, metadata_commitment)
115    ///   tuples for the notes created in a transaction, where `metadata_commitment` is the return
116    ///   value of [`NoteMetadata::to_commitment`].
117    pub(crate) fn compute_commitment<'header>(
118        notes: impl ExactSizeIterator<Item = &'header NoteHeader>,
119    ) -> Word {
120        if notes.len() == 0 {
121            return Word::empty();
122        }
123
124        let mut elements: Vec<Felt> = Vec::with_capacity(notes.len() * 8);
125        for note_header in notes {
126            elements.extend_from_slice(note_header.id().as_elements());
127            elements.extend_from_slice(note_header.metadata().to_commitment().as_elements());
128        }
129
130        Hasher::hash_elements(&elements)
131    }
132}
133
134// SERIALIZATION
135// ------------------------------------------------------------------------------------------------
136
137impl<N: Serializable> Serializable for OutputNoteCollection<N> {
138    fn write_into<W: ByteWriter>(&self, target: &mut W) {
139        // assert is OK here because we enforce max number of notes in the constructor
140        assert!(self.notes.len() <= u16::MAX.into());
141        target.write_u16(self.notes.len() as u16);
142        target.write_many(&self.notes);
143    }
144}
145
146impl<N> Deserializable for OutputNoteCollection<N>
147where
148    N: Deserializable,
149    for<'a> &'a NoteHeader: From<&'a N>,
150    for<'a> NoteId: From<&'a N>,
151{
152    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
153        let num_notes = source.read_u16()?;
154        let notes = source.read_many_iter::<N>(num_notes.into())?.collect::<Result<_, _>>()?;
155        Self::new(notes).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
156    }
157}
158
159// RAW OUTPUT NOTES
160// ================================================================================================
161
162/// Output notes produced during transaction execution (before proving).
163///
164/// Contains [`RawOutputNote`] instances which represent notes as they exist immediately after
165/// transaction execution.
166pub type RawOutputNotes = OutputNoteCollection<RawOutputNote>;
167
168/// The types of note outputs produced during transaction execution (before proving).
169///
170/// This enum represents notes as they exist immediately after transaction execution,
171/// before they are processed for inclusion in a proven transaction. It includes:
172/// - Full notes with all details (public or private)
173/// - Partial notes (notes created with only recipient digest, not full recipient details)
174///
175/// During proving, these are converted to [`OutputNote`] via the
176/// [`to_output_note`](Self::to_output_note) method, which enforces size limits on public notes and
177/// converts private/partial notes to headers.
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum RawOutputNote {
180    Full(Note),
181    Partial(PartialNote),
182}
183
184impl RawOutputNote {
185    const FULL: u8 = 0;
186    const PARTIAL: u8 = 1;
187
188    /// The assets contained in the note.
189    pub fn assets(&self) -> &NoteAssets {
190        match self {
191            Self::Full(note) => note.assets(),
192            Self::Partial(note) => note.assets(),
193        }
194    }
195
196    /// Unique note identifier.
197    ///
198    /// This value is both an unique identifier and a commitment to the note.
199    pub fn id(&self) -> NoteId {
200        match self {
201            Self::Full(note) => note.id(),
202            Self::Partial(note) => note.id(),
203        }
204    }
205
206    /// Returns the recipient of the processed [`Full`](RawOutputNote::Full) output note, [`None`]
207    /// if the note type is not [`Full`](RawOutputNote::Full).
208    ///
209    /// See [crate::note::NoteRecipient] for more details.
210    pub fn recipient(&self) -> Option<&NoteRecipient> {
211        match self {
212            Self::Full(note) => Some(note.recipient()),
213            Self::Partial(_) => None,
214        }
215    }
216
217    /// Returns the recipient digest of the output note.
218    ///
219    /// See [crate::note::NoteRecipient] for more details.
220    pub fn recipient_digest(&self) -> Word {
221        match self {
222            RawOutputNote::Full(note) => note.recipient().digest(),
223            RawOutputNote::Partial(note) => note.recipient_digest(),
224        }
225    }
226
227    /// Returns the note's metadata.
228    pub fn metadata(&self) -> &NoteMetadata {
229        match self {
230            Self::Full(note) => note.metadata(),
231            Self::Partial(note) => note.metadata(),
232        }
233    }
234
235    /// Converts this output note to a proven output note.
236    ///
237    /// This method performs the following transformations:
238    /// - Private notes (full or partial) are converted into note headers (only public info
239    ///   retained).
240    /// - Full public notes are wrapped in [`PublicOutputNote`], which enforces size limits
241    ///
242    /// # Errors
243    /// Returns an error if a public note exceeds the maximum allowed size ([`NOTE_MAX_SIZE`]).
244    pub fn to_output_note(&self) -> Result<OutputNote, OutputNoteError> {
245        match self {
246            Self::Full(note) if note.metadata().is_private() => {
247                Ok(OutputNote::Private(PrivateNoteHeader::new(note.header().clone())?))
248            },
249            Self::Full(note) => Ok(OutputNote::Public(PublicOutputNote::new(note.clone())?)),
250            Self::Partial(note) => {
251                Ok(OutputNote::Private(PrivateNoteHeader::new(note.header().clone())?))
252            },
253        }
254    }
255
256    /// Returns a reference to the [`NoteHeader`] of this note.
257    pub fn header(&self) -> &NoteHeader {
258        match self {
259            Self::Full(note) => note.header(),
260            Self::Partial(note) => note.header(),
261        }
262    }
263
264    /// Returns a commitment to the note and its metadata.
265    ///
266    /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT)
267    pub fn commitment(&self) -> Word {
268        compute_note_commitment(self.id(), self.metadata())
269    }
270}
271
272impl From<&RawOutputNote> for NoteId {
273    fn from(note: &RawOutputNote) -> Self {
274        note.id()
275    }
276}
277
278impl<'note> From<&'note RawOutputNote> for &'note NoteHeader {
279    fn from(note: &'note RawOutputNote) -> Self {
280        note.header()
281    }
282}
283
284impl Serializable for RawOutputNote {
285    fn write_into<W: ByteWriter>(&self, target: &mut W) {
286        match self {
287            Self::Full(note) => {
288                target.write(Self::FULL);
289                target.write(note);
290            },
291            Self::Partial(note) => {
292                target.write(Self::PARTIAL);
293                target.write(note);
294            },
295        }
296    }
297
298    fn get_size_hint(&self) -> usize {
299        // Serialized size of the enum tag.
300        let tag_size = 0u8.get_size_hint();
301
302        match self {
303            Self::Full(note) => tag_size + note.get_size_hint(),
304            Self::Partial(note) => tag_size + note.get_size_hint(),
305        }
306    }
307}
308
309impl Deserializable for RawOutputNote {
310    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
311        match source.read_u8()? {
312            Self::FULL => Ok(Self::Full(Note::read_from(source)?)),
313            Self::PARTIAL => Ok(Self::Partial(PartialNote::read_from(source)?)),
314            v => Err(DeserializationError::InvalidValue(format!("invalid output note type: {v}"))),
315        }
316    }
317}
318
319// OUTPUT NOTES
320// ================================================================================================
321
322/// Output notes in a proven transaction.
323///
324/// Contains [`OutputNote`] instances which have been processed for inclusion in proven
325/// transactions, with size limits enforced on public notes.
326pub type OutputNotes = OutputNoteCollection<OutputNote>;
327
328/// Output note types that can appear in a proven transaction.
329///
330/// This enum represents the final form of output notes after proving. Unlike [`RawOutputNote`],
331/// this enum:
332/// - Does not include partial notes (they are converted to headers).
333/// - Wraps public notes in [`PublicOutputNote`] which enforces size limits.
334/// - Contains only the minimal information needed for verification.
335#[allow(clippy::large_enum_variant)]
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub enum OutputNote {
338    /// A public note with full details, size-validated.
339    Public(PublicOutputNote),
340    /// A note private header (for private notes).
341    Private(PrivateNoteHeader),
342}
343
344impl OutputNote {
345    const PUBLIC: u8 = 0;
346    const PRIVATE: u8 = 1;
347
348    /// Unique note identifier.
349    ///
350    /// This value is both an unique identifier and a commitment to the note.
351    pub fn id(&self) -> NoteId {
352        match self {
353            Self::Public(note) => note.id(),
354            Self::Private(header) => header.id(),
355        }
356    }
357
358    /// Note's metadata.
359    pub fn metadata(&self) -> &NoteMetadata {
360        match self {
361            Self::Public(note) => note.metadata(),
362            Self::Private(header) => header.metadata(),
363        }
364    }
365
366    /// The assets contained in the note, if available.
367    ///
368    /// Returns `Some` for public notes, `None` for private notes.
369    pub fn assets(&self) -> Option<&NoteAssets> {
370        match self {
371            Self::Public(note) => Some(note.assets()),
372            Self::Private(_) => None,
373        }
374    }
375
376    /// Returns a commitment to the note and its metadata.
377    ///
378    /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT)
379    pub fn to_commitment(&self) -> Word {
380        compute_note_commitment(self.id(), self.metadata())
381    }
382
383    /// Returns the recipient of the public note, if this is a public note.
384    pub fn recipient(&self) -> Option<&NoteRecipient> {
385        match self {
386            Self::Public(note) => Some(note.recipient()),
387            Self::Private(_) => None,
388        }
389    }
390}
391
392// CONVERSIONS
393// ------------------------------------------------------------------------------------------------
394
395impl<'note> From<&'note OutputNote> for &'note NoteHeader {
396    fn from(value: &'note OutputNote) -> Self {
397        match value {
398            OutputNote::Public(note) => note.header(),
399            OutputNote::Private(header) => &header.0,
400        }
401    }
402}
403
404impl From<&OutputNote> for NoteId {
405    fn from(value: &OutputNote) -> Self {
406        value.id()
407    }
408}
409
410// SERIALIZATION
411// ------------------------------------------------------------------------------------------------
412
413impl Serializable for OutputNote {
414    fn write_into<W: ByteWriter>(&self, target: &mut W) {
415        match self {
416            Self::Public(note) => {
417                target.write(Self::PUBLIC);
418                target.write(note);
419            },
420            Self::Private(header) => {
421                target.write(Self::PRIVATE);
422                target.write(header);
423            },
424        }
425    }
426
427    fn get_size_hint(&self) -> usize {
428        let tag_size = 0u8.get_size_hint();
429        match self {
430            Self::Public(note) => tag_size + note.get_size_hint(),
431            Self::Private(header) => tag_size + header.get_size_hint(),
432        }
433    }
434}
435
436impl Deserializable for OutputNote {
437    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
438        match source.read_u8()? {
439            Self::PUBLIC => Ok(Self::Public(PublicOutputNote::read_from(source)?)),
440            Self::PRIVATE => Ok(Self::Private(PrivateNoteHeader::read_from(source)?)),
441            v => Err(DeserializationError::InvalidValue(format!(
442                "invalid proven output note type: {v}"
443            ))),
444        }
445    }
446}
447
448// PUBLIC OUTPUT NOTE
449// ================================================================================================
450
451/// A public output note with enforced size limits.
452///
453/// This struct wraps a [`Note`] and guarantees that:
454/// - The note is public (not private).
455/// - The serialized size does not exceed [`NOTE_MAX_SIZE`].
456///
457/// This type is used in [`OutputNote::Public`] to ensure that all public notes in proven
458/// transactions meet the protocol's size requirements.
459#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct PublicOutputNote(Note);
461
462impl PublicOutputNote {
463    /// Creates a new [`PublicOutputNote`] from the given note.
464    ///
465    /// # Errors
466    /// Returns an error if:
467    /// - The note is private.
468    /// - The serialized size exceeds [`NOTE_MAX_SIZE`].
469    pub fn new(mut note: Note) -> Result<Self, OutputNoteError> {
470        // Ensure the note is public
471        if note.metadata().is_private() {
472            return Err(OutputNoteError::NoteIsPrivate(note.id()));
473        }
474
475        // Strip decorators from the note script
476        note.minify_script();
477
478        // Check the size limit after stripping decorators
479        let note_size = note.get_size_hint();
480        if note_size > NOTE_MAX_SIZE as usize {
481            return Err(OutputNoteError::NoteSizeLimitExceeded { note_id: note.id(), note_size });
482        }
483
484        Ok(Self(note))
485    }
486
487    /// Returns the unique identifier of this note.
488    pub fn id(&self) -> NoteId {
489        self.0.id()
490    }
491
492    /// Returns the note's metadata.
493    pub fn metadata(&self) -> &NoteMetadata {
494        self.0.metadata()
495    }
496
497    /// Returns the note's assets.
498    pub fn assets(&self) -> &NoteAssets {
499        self.0.assets()
500    }
501
502    /// Returns the note's recipient.
503    pub fn recipient(&self) -> &NoteRecipient {
504        self.0.recipient()
505    }
506
507    /// Returns the note's header.
508    pub fn header(&self) -> &NoteHeader {
509        self.0.header()
510    }
511
512    /// Returns a reference to the underlying note.
513    pub fn as_note(&self) -> &Note {
514        &self.0
515    }
516
517    /// Consumes this wrapper and returns the underlying note.
518    pub fn into_note(self) -> Note {
519        self.0
520    }
521}
522
523impl Serializable for PublicOutputNote {
524    fn write_into<W: ByteWriter>(&self, target: &mut W) {
525        self.0.write_into(target);
526    }
527
528    fn get_size_hint(&self) -> usize {
529        self.0.get_size_hint()
530    }
531}
532
533impl Deserializable for PublicOutputNote {
534    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
535        let note = Note::read_from(source)?;
536        Self::new(note).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
537    }
538}
539
540// PRIVATE NOTE HEADER
541// ================================================================================================
542
543/// A [NoteHeader] of a private note.
544#[derive(Debug, Clone, PartialEq, Eq)]
545pub struct PrivateNoteHeader(NoteHeader);
546
547impl PrivateNoteHeader {
548    /// Creates a new [`PrivateNoteHeader`] from the given note header.
549    ///
550    /// # Errors
551    /// Returns an error if:
552    /// - The provided header is for a public note.
553    pub fn new(header: NoteHeader) -> Result<Self, OutputNoteError> {
554        if !header.metadata().is_private() {
555            return Err(OutputNoteError::NoteIsPublic(header.id()));
556        }
557
558        Ok(Self(header))
559    }
560
561    /// Returns the note's identifier.
562    ///
563    /// The [NoteId] value is both an unique identifier and a commitment to the note.
564    pub fn id(&self) -> NoteId {
565        self.0.id()
566    }
567
568    /// Returns the note's metadata.
569    pub fn metadata(&self) -> &NoteMetadata {
570        self.0.metadata()
571    }
572
573    /// Consumes self and returns the note header's metadata.
574    pub fn into_metadata(self) -> NoteMetadata {
575        self.0.into_metadata()
576    }
577
578    /// Returns a commitment to the note and its metadata.
579    ///
580    /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT)
581    ///
582    /// This value is used primarily for authenticating notes consumed when they are consumed
583    /// in a transaction.
584    pub fn commitment(&self) -> Word {
585        self.0.commitment()
586    }
587
588    /// Returns a reference to the underlying note header.
589    pub fn as_header(&self) -> &NoteHeader {
590        &self.0
591    }
592
593    /// Consumes this wrapper and returns the underlying note header.
594    pub fn into_header(self) -> NoteHeader {
595        self.0
596    }
597}
598
599impl Serializable for PrivateNoteHeader {
600    fn write_into<W: ByteWriter>(&self, target: &mut W) {
601        self.0.write_into(target);
602    }
603
604    fn get_size_hint(&self) -> usize {
605        self.0.get_size_hint()
606    }
607}
608
609impl Deserializable for PrivateNoteHeader {
610    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
611        let header = NoteHeader::read_from(source)?;
612        Self::new(header).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
613    }
614}