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