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