miden_protocol/transaction/
outputs.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct TransactionOutputs {
35 pub account: AccountHeader,
37 pub account_delta_commitment: Word,
39 pub output_notes: OutputNotes,
41 pub fee: FungibleAsset,
43 pub expiration_block_num: BlockNumber,
45}
46
47impl TransactionOutputs {
48 pub const OUTPUT_NOTES_COMMITMENT_WORD_IDX: usize = 0;
53
54 pub const ACCOUNT_UPDATE_COMMITMENT_WORD_IDX: usize = 1;
56
57 pub const FEE_ASSET_WORD_IDX: usize = 2;
59
60 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#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct OutputNotes {
99 notes: Vec<OutputNote>,
100 commitment: Word,
101}
102
103impl OutputNotes {
104 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 pub fn commitment(&self) -> Word {
138 self.commitment
139 }
140 pub fn num_notes(&self) -> usize {
142 self.notes.len()
143 }
144
145 pub fn is_empty(&self) -> bool {
147 self.notes.is_empty()
148 }
149
150 pub fn get_note(&self, idx: usize) -> &OutputNote {
152 &self.notes[idx]
153 }
154
155 pub fn iter(&self) -> impl Iterator<Item = &OutputNote> {
160 self.notes.iter()
161 }
162
163 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
189impl Serializable for OutputNotes {
193 fn write_into<W: ByteWriter>(&self, target: &mut W) {
194 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
209const FULL: u8 = 0;
213const PARTIAL: u8 = 1;
214const HEADER: u8 = 2;
215
216#[derive(Debug, Clone, PartialEq, Eq)]
218pub enum OutputNote {
219 Full(Note),
220 Partial(PartialNote),
221 Header(NoteHeader),
222}
223
224impl OutputNote {
225 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 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 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 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 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 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 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 pub fn commitment(&self) -> Word {
307 compute_note_commitment(self.id(), self.metadata())
308 }
309}
310
311impl 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#[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}