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 = Self::compute_commitment(notes.iter().map(NoteHeader::from));
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    // HELPERS
145    // --------------------------------------------------------------------------------------------
146
147    /// Computes a commitment to output notes.
148    ///
149    /// For a non-empty list of notes, this is a sequential hash of (note_id, metadata) tuples for
150    /// the notes created in a transaction. For an empty list, [EMPTY_WORD] is returned.
151    pub(crate) fn compute_commitment(notes: impl ExactSizeIterator<Item = NoteHeader>) -> Word {
152        if notes.len() == 0 {
153            return Word::empty();
154        }
155
156        let mut elements: Vec<Felt> = Vec::with_capacity(notes.len() * 8);
157        for note_header in notes {
158            elements.extend_from_slice(note_header.id().as_elements());
159            elements.extend_from_slice(Word::from(note_header.metadata()).as_elements());
160        }
161
162        Hasher::hash_elements(&elements)
163    }
164}
165
166// SERIALIZATION
167// ------------------------------------------------------------------------------------------------
168
169impl Serializable for OutputNotes {
170    fn write_into<W: ByteWriter>(&self, target: &mut W) {
171        // assert is OK here because we enforce max number of notes in the constructor
172        assert!(self.notes.len() <= u16::MAX.into());
173        target.write_u16(self.notes.len() as u16);
174        target.write_many(&self.notes);
175    }
176}
177
178impl Deserializable for OutputNotes {
179    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
180        let num_notes = source.read_u16()?;
181        let notes = source.read_many::<OutputNote>(num_notes.into())?;
182        Self::new(notes).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
183    }
184}
185
186// OUTPUT NOTE
187// ================================================================================================
188
189const FULL: u8 = 0;
190const PARTIAL: u8 = 1;
191const HEADER: u8 = 2;
192
193/// The types of note outputs supported by the transaction kernel.
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub enum OutputNote {
196    Full(Note),
197    Partial(PartialNote),
198    Header(NoteHeader),
199}
200
201impl OutputNote {
202    /// The assets contained in the note.
203    pub fn assets(&self) -> Option<&NoteAssets> {
204        match self {
205            OutputNote::Full(note) => Some(note.assets()),
206            OutputNote::Partial(note) => Some(note.assets()),
207            OutputNote::Header(_) => None,
208        }
209    }
210
211    /// Unique note identifier.
212    ///
213    /// This value is both an unique identifier and a commitment to the note.
214    pub fn id(&self) -> NoteId {
215        match self {
216            OutputNote::Full(note) => note.id(),
217            OutputNote::Partial(note) => note.id(),
218            OutputNote::Header(note) => note.id(),
219        }
220    }
221
222    /// Returns the recipient of the processed [`Full`](OutputNote::Full) output note, [`None`] if
223    /// the note type is not [`Full`](OutputNote::Full).
224    ///
225    /// See [crate::note::NoteRecipient] for more details.
226    pub fn recipient(&self) -> Option<&NoteRecipient> {
227        match self {
228            OutputNote::Full(note) => Some(note.recipient()),
229            OutputNote::Partial(_) => None,
230            OutputNote::Header(_) => None,
231        }
232    }
233
234    /// Returns the recipient digest of the processed [`Full`](OutputNote::Full) or
235    /// [`Partial`](OutputNote::Partial) output note. Returns [`None`] if the note type is
236    /// [`Header`](OutputNote::Header).
237    ///
238    /// See [crate::note::NoteRecipient] for more details.
239    pub fn recipient_digest(&self) -> Option<Word> {
240        match self {
241            OutputNote::Full(note) => Some(note.recipient().digest()),
242            OutputNote::Partial(note) => Some(note.recipient_digest()),
243            OutputNote::Header(_) => None,
244        }
245    }
246
247    /// Note's metadata.
248    pub fn metadata(&self) -> &NoteMetadata {
249        match self {
250            OutputNote::Full(note) => note.metadata(),
251            OutputNote::Partial(note) => note.metadata(),
252            OutputNote::Header(note) => note.metadata(),
253        }
254    }
255
256    /// Erase private note information.
257    ///
258    /// Specifically:
259    /// - Full private notes are converted into note headers.
260    /// - All partial notes are converted into note headers.
261    pub fn shrink(&self) -> Self {
262        match self {
263            OutputNote::Full(note) if note.metadata().is_private() => {
264                OutputNote::Header(*note.header())
265            },
266            OutputNote::Partial(note) => OutputNote::Header(note.into()),
267            _ => self.clone(),
268        }
269    }
270
271    /// Returns a commitment to the note and its metadata.
272    ///
273    /// > hash(NOTE_ID || NOTE_METADATA)
274    pub fn commitment(&self) -> Word {
275        compute_note_commitment(self.id(), self.metadata())
276    }
277}
278
279// CONVERSIONS
280// ------------------------------------------------------------------------------------------------
281
282impl From<OutputNote> for NoteHeader {
283    fn from(value: OutputNote) -> Self {
284        (&value).into()
285    }
286}
287
288impl From<&OutputNote> for NoteHeader {
289    fn from(value: &OutputNote) -> Self {
290        match value {
291            OutputNote::Full(note) => note.into(),
292            OutputNote::Partial(note) => note.into(),
293            OutputNote::Header(note) => *note,
294        }
295    }
296}
297
298// SERIALIZATION
299// ------------------------------------------------------------------------------------------------
300
301impl Serializable for OutputNote {
302    fn write_into<W: ByteWriter>(&self, target: &mut W) {
303        match self {
304            OutputNote::Full(note) => {
305                target.write(FULL);
306                target.write(note);
307            },
308            OutputNote::Partial(note) => {
309                target.write(PARTIAL);
310                target.write(note);
311            },
312            OutputNote::Header(note) => {
313                target.write(HEADER);
314                target.write(note);
315            },
316        }
317    }
318}
319
320impl Deserializable for OutputNote {
321    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
322        match source.read_u8()? {
323            FULL => Ok(OutputNote::Full(Note::read_from(source)?)),
324            PARTIAL => Ok(OutputNote::Partial(PartialNote::read_from(source)?)),
325            HEADER => Ok(OutputNote::Header(NoteHeader::read_from(source)?)),
326            v => Err(DeserializationError::InvalidValue(format!("invalid note type: {v}"))),
327        }
328    }
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}