miden_objects/transaction/
outputs.rs

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