miden_objects/transaction/
outputs.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct TransactionOutputs {
21 pub account: AccountHeader,
23 pub account_delta_commitment: Digest,
25 pub output_notes: OutputNotes,
27 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#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct OutputNotes {
63 notes: Vec<OutputNote>,
64 commitment: Digest,
65}
66
67impl OutputNotes {
68 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(¬es);
89
90 Ok(Self { notes, commitment })
91 }
92
93 pub fn commitment(&self) -> Digest {
101 self.commitment
102 }
103 pub fn num_notes(&self) -> usize {
105 self.notes.len()
106 }
107
108 pub fn is_empty(&self) -> bool {
110 self.notes.is_empty()
111 }
112
113 pub fn get_note(&self, idx: usize) -> &OutputNote {
115 &self.notes[idx]
116 }
117
118 pub fn iter(&self) -> impl Iterator<Item = &OutputNote> {
123 self.notes.iter()
124 }
125}
126
127impl Serializable for OutputNotes {
131 fn write_into<W: ByteWriter>(&self, target: &mut W) {
132 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
147const FULL: u8 = 0;
151const PARTIAL: u8 = 1;
152const HEADER: u8 = 2;
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum OutputNote {
157 Full(Note),
158 Partial(PartialNote),
159 Header(NoteHeader),
160}
161
162impl OutputNote {
163 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 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 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 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 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 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 pub fn commitment(&self) -> Digest {
236 compute_note_commitment(self.id(), self.metadata())
237 }
238}
239
240impl 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
259impl 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
292fn 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#[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}