Skip to main content

miden_protocol/transaction/
outputs.rs

1use alloc::collections::BTreeSet;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4use core::fmt::Debug;
5
6use crate::account::AccountHeader;
7use crate::asset::FungibleAsset;
8use crate::block::BlockNumber;
9use crate::errors::TransactionOutputError;
10use crate::note::{
11    Note,
12    NoteAssets,
13    NoteHeader,
14    NoteId,
15    NoteMetadata,
16    NoteRecipient,
17    PartialNote,
18    compute_note_commitment,
19};
20use crate::utils::serde::{
21    ByteReader,
22    ByteWriter,
23    Deserializable,
24    DeserializationError,
25    Serializable,
26};
27use crate::{Felt, Hasher, MAX_OUTPUT_NOTES_PER_TX, Word};
28
29// TRANSACTION OUTPUTS
30// ================================================================================================
31
32/// Describes the result of executing a transaction.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct TransactionOutputs {
35    /// Information related to the account's final state.
36    pub account: AccountHeader,
37    /// The commitment to the delta computed by the transaction kernel.
38    pub account_delta_commitment: Word,
39    /// Set of output notes created by the transaction.
40    pub output_notes: OutputNotes,
41    /// The fee of the transaction.
42    pub fee: FungibleAsset,
43    /// Defines up to which block the transaction is considered valid.
44    pub expiration_block_num: BlockNumber,
45}
46
47impl TransactionOutputs {
48    // CONSTANTS
49    // --------------------------------------------------------------------------------------------
50
51    /// The index of the word at which the final account nonce is stored on the output stack.
52    pub const OUTPUT_NOTES_COMMITMENT_WORD_IDX: usize = 0;
53
54    /// The index of the word at which the account update commitment is stored on the output stack.
55    pub const ACCOUNT_UPDATE_COMMITMENT_WORD_IDX: usize = 1;
56
57    /// The index of the word at which the fee asset is stored on the output stack.
58    pub const FEE_ASSET_WORD_IDX: usize = 2;
59
60    /// The index of the item at which the expiration block height is stored on the output stack.
61    pub const EXPIRATION_BLOCK_ELEMENT_IDX: usize = 12;
62}
63
64impl Serializable for TransactionOutputs {
65    fn write_into<W: ByteWriter>(&self, target: &mut W) {
66        self.account.write_into(target);
67        self.account_delta_commitment.write_into(target);
68        self.output_notes.write_into(target);
69        self.fee.write_into(target);
70        self.expiration_block_num.write_into(target);
71    }
72}
73
74impl Deserializable for TransactionOutputs {
75    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
76        let account = AccountHeader::read_from(source)?;
77        let account_delta_commitment = Word::read_from(source)?;
78        let output_notes = OutputNotes::read_from(source)?;
79        let fee = FungibleAsset::read_from(source)?;
80        let expiration_block_num = BlockNumber::read_from(source)?;
81
82        Ok(Self {
83            account,
84            account_delta_commitment,
85            output_notes,
86            fee,
87            expiration_block_num,
88        })
89    }
90}
91
92// OUTPUT NOTES
93// ================================================================================================
94
95/// Contains a list of output notes of a transaction. The list can be empty if the transaction does
96/// not produce any notes.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct OutputNotes {
99    notes: Vec<OutputNote>,
100    commitment: Word,
101}
102
103impl OutputNotes {
104    // CONSTRUCTOR
105    // --------------------------------------------------------------------------------------------
106
107    /// Returns new [OutputNotes] instantiated from the provide vector of notes.
108    ///
109    /// # Errors
110    /// Returns an error if:
111    /// - The total number of notes is greater than [`MAX_OUTPUT_NOTES_PER_TX`].
112    /// - The vector of notes contains duplicates.
113    pub fn new(notes: Vec<OutputNote>) -> Result<Self, TransactionOutputError> {
114        if notes.len() > MAX_OUTPUT_NOTES_PER_TX {
115            return Err(TransactionOutputError::TooManyOutputNotes(notes.len()));
116        }
117
118        let mut seen_notes = BTreeSet::new();
119        for note in notes.iter() {
120            if !seen_notes.insert(note.id()) {
121                return Err(TransactionOutputError::DuplicateOutputNote(note.id()));
122            }
123        }
124
125        let commitment = Self::compute_commitment(notes.iter().map(OutputNote::header));
126
127        Ok(Self { notes, commitment })
128    }
129
130    // PUBLIC ACCESSORS
131    // --------------------------------------------------------------------------------------------
132
133    /// Returns the commitment to the output notes.
134    ///
135    /// The commitment is computed as a sequential hash of (hash, metadata) tuples for the notes
136    /// created in a transaction.
137    pub fn commitment(&self) -> Word {
138        self.commitment
139    }
140    /// Returns total number of output notes.
141    pub fn num_notes(&self) -> usize {
142        self.notes.len()
143    }
144
145    /// Returns true if this [OutputNotes] does not contain any notes.
146    pub fn is_empty(&self) -> bool {
147        self.notes.is_empty()
148    }
149
150    /// Returns a reference to the note located at the specified index.
151    pub fn get_note(&self, idx: usize) -> &OutputNote {
152        &self.notes[idx]
153    }
154
155    // ITERATORS
156    // --------------------------------------------------------------------------------------------
157
158    /// Returns an iterator over notes in this [OutputNotes].
159    pub fn iter(&self) -> impl Iterator<Item = &OutputNote> {
160        self.notes.iter()
161    }
162
163    // HELPERS
164    // --------------------------------------------------------------------------------------------
165
166    /// Computes a commitment to output notes.
167    ///
168    /// - For an empty list, [`Word::empty`] is returned.
169    /// - For a non-empty list of notes, this is a sequential hash of (note_id, metadata_commitment)
170    ///   tuples for the notes created in a transaction, where `metadata_commitment` is the return
171    ///   value of [`NoteMetadata::to_commitment`].
172    pub(crate) fn compute_commitment<'header>(
173        notes: impl ExactSizeIterator<Item = &'header NoteHeader>,
174    ) -> Word {
175        if notes.len() == 0 {
176            return Word::empty();
177        }
178
179        let mut elements: Vec<Felt> = Vec::with_capacity(notes.len() * 8);
180        for note_header in notes {
181            elements.extend_from_slice(note_header.id().as_elements());
182            elements.extend_from_slice(note_header.metadata().to_commitment().as_elements());
183        }
184
185        Hasher::hash_elements(&elements)
186    }
187}
188
189// SERIALIZATION
190// ------------------------------------------------------------------------------------------------
191
192impl Serializable for OutputNotes {
193    fn write_into<W: ByteWriter>(&self, target: &mut W) {
194        // assert is OK here because we enforce max number of notes in the constructor
195        assert!(self.notes.len() <= u16::MAX.into());
196        target.write_u16(self.notes.len() as u16);
197        target.write_many(&self.notes);
198    }
199}
200
201impl Deserializable for OutputNotes {
202    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
203        let num_notes = source.read_u16()?;
204        let notes = source.read_many::<OutputNote>(num_notes.into())?;
205        Self::new(notes).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
206    }
207}
208
209// OUTPUT NOTE
210// ================================================================================================
211
212const FULL: u8 = 0;
213const PARTIAL: u8 = 1;
214const HEADER: u8 = 2;
215
216/// The types of note outputs supported by the transaction kernel.
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub enum OutputNote {
219    Full(Note),
220    Partial(PartialNote),
221    Header(NoteHeader),
222}
223
224impl OutputNote {
225    /// The assets contained in the note.
226    pub fn assets(&self) -> Option<&NoteAssets> {
227        match self {
228            OutputNote::Full(note) => Some(note.assets()),
229            OutputNote::Partial(note) => Some(note.assets()),
230            OutputNote::Header(_) => None,
231        }
232    }
233
234    /// Unique note identifier.
235    ///
236    /// This value is both an unique identifier and a commitment to the note.
237    pub fn id(&self) -> NoteId {
238        match self {
239            OutputNote::Full(note) => note.id(),
240            OutputNote::Partial(note) => note.id(),
241            OutputNote::Header(note) => note.id(),
242        }
243    }
244
245    /// Returns the recipient of the processed [`Full`](OutputNote::Full) output note, [`None`] if
246    /// the note type is not [`Full`](OutputNote::Full).
247    ///
248    /// See [crate::note::NoteRecipient] for more details.
249    pub fn recipient(&self) -> Option<&NoteRecipient> {
250        match self {
251            OutputNote::Full(note) => Some(note.recipient()),
252            OutputNote::Partial(_) => None,
253            OutputNote::Header(_) => None,
254        }
255    }
256
257    /// Returns the recipient digest of the processed [`Full`](OutputNote::Full) or
258    /// [`Partial`](OutputNote::Partial) output note. Returns [`None`] if the note type is
259    /// [`Header`](OutputNote::Header).
260    ///
261    /// See [crate::note::NoteRecipient] for more details.
262    pub fn recipient_digest(&self) -> Option<Word> {
263        match self {
264            OutputNote::Full(note) => Some(note.recipient().digest()),
265            OutputNote::Partial(note) => Some(note.recipient_digest()),
266            OutputNote::Header(_) => None,
267        }
268    }
269
270    /// Note's metadata.
271    pub fn metadata(&self) -> &NoteMetadata {
272        match self {
273            OutputNote::Full(note) => note.metadata(),
274            OutputNote::Partial(note) => note.metadata(),
275            OutputNote::Header(note) => note.metadata(),
276        }
277    }
278
279    /// Erase private note information.
280    ///
281    /// Specifically:
282    /// - Full private notes are converted into note headers.
283    /// - All partial notes are converted into note headers.
284    pub fn shrink(&self) -> Self {
285        match self {
286            OutputNote::Full(note) if note.metadata().is_private() => {
287                OutputNote::Header(note.header().clone())
288            },
289            OutputNote::Partial(note) => OutputNote::Header(note.header().clone()),
290            _ => self.clone(),
291        }
292    }
293
294    /// Returns a reference to the [`NoteHeader`] of this note.
295    pub fn header(&self) -> &NoteHeader {
296        match self {
297            OutputNote::Full(note) => note.header(),
298            OutputNote::Partial(note) => note.header(),
299            OutputNote::Header(header) => header,
300        }
301    }
302
303    /// Returns a commitment to the note and its metadata.
304    ///
305    /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT)
306    pub fn commitment(&self) -> Word {
307        compute_note_commitment(self.id(), self.metadata())
308    }
309}
310
311// SERIALIZATION
312// ------------------------------------------------------------------------------------------------
313
314impl Serializable for OutputNote {
315    fn write_into<W: ByteWriter>(&self, target: &mut W) {
316        match self {
317            OutputNote::Full(note) => {
318                target.write(FULL);
319                target.write(note);
320            },
321            OutputNote::Partial(note) => {
322                target.write(PARTIAL);
323                target.write(note);
324            },
325            OutputNote::Header(note) => {
326                target.write(HEADER);
327                target.write(note);
328            },
329        }
330    }
331}
332
333impl Deserializable for OutputNote {
334    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
335        match source.read_u8()? {
336            FULL => Ok(OutputNote::Full(Note::read_from(source)?)),
337            PARTIAL => Ok(OutputNote::Partial(PartialNote::read_from(source)?)),
338            HEADER => Ok(OutputNote::Header(NoteHeader::read_from(source)?)),
339            v => Err(DeserializationError::InvalidValue(format!("invalid note type: {v}"))),
340        }
341    }
342}
343
344// TESTS
345// ================================================================================================
346
347#[cfg(test)]
348mod output_notes_tests {
349    use assert_matches::assert_matches;
350
351    use super::OutputNotes;
352    use crate::Word;
353    use crate::errors::TransactionOutputError;
354    use crate::note::Note;
355    use crate::transaction::OutputNote;
356
357    #[test]
358    fn test_duplicate_output_notes() -> anyhow::Result<()> {
359        let mock_note = Note::mock_noop(Word::empty());
360        let mock_note_id = mock_note.id();
361        let mock_note_clone = mock_note.clone();
362
363        let error =
364            OutputNotes::new(vec![OutputNote::Full(mock_note), OutputNote::Full(mock_note_clone)])
365                .expect_err("input notes creation should fail");
366
367        assert_matches!(error, TransactionOutputError::DuplicateOutputNote(note_id) if note_id == mock_note_id);
368
369        Ok(())
370    }
371}