miden_objects/transaction/
inputs.rs1use alloc::{collections::BTreeSet, vec::Vec};
2use core::fmt::Debug;
3
4use super::{BlockHeader, ChainMmr, Digest, Felt, Hasher, Word};
5use crate::{
6 MAX_INPUT_NOTES_PER_TX, TransactionInputError,
7 account::{Account, AccountId, AccountIdAnchor},
8 block::BlockNumber,
9 note::{Note, NoteId, NoteInclusionProof, NoteLocation, Nullifier},
10 utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
11};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct TransactionInputs {
19 account: Account,
20 account_seed: Option<Word>,
21 block_header: BlockHeader,
22 block_chain: ChainMmr,
23 input_notes: InputNotes<InputNote>,
24}
25
26impl TransactionInputs {
27 pub fn new(
36 account: Account,
37 account_seed: Option<Word>,
38 block_header: BlockHeader,
39 block_chain: ChainMmr,
40 input_notes: InputNotes<InputNote>,
41 ) -> Result<Self, TransactionInputError> {
42 validate_account_seed(&account, &block_header, &block_chain, account_seed)?;
44
45 let block_num = block_header.block_num();
47 if block_chain.chain_length() != block_header.block_num() {
48 return Err(TransactionInputError::InconsistentChainLength {
49 expected: block_header.block_num(),
50 actual: block_chain.chain_length(),
51 });
52 }
53
54 if block_chain.peaks().hash_peaks() != block_header.chain_commitment() {
55 return Err(TransactionInputError::InconsistentChainCommitment {
56 expected: block_header.chain_commitment(),
57 actual: block_chain.peaks().hash_peaks(),
58 });
59 }
60
61 for note in input_notes.iter() {
63 if let InputNote::Authenticated { note, proof } = note {
64 let note_block_num = proof.location().block_num();
65
66 let block_header = if note_block_num == block_num {
67 &block_header
68 } else {
69 block_chain
70 .get_block(note_block_num)
71 .ok_or(TransactionInputError::InputNoteBlockNotInChainMmr(note.id()))?
72 };
73
74 validate_is_in_block(note, proof, block_header)?;
75 }
76 }
77
78 Ok(Self {
79 account,
80 account_seed,
81 block_header,
82 block_chain,
83 input_notes,
84 })
85 }
86
87 pub fn account(&self) -> &Account {
92 &self.account
93 }
94
95 pub fn account_seed(&self) -> Option<Word> {
97 self.account_seed
98 }
99
100 pub fn block_header(&self) -> &BlockHeader {
102 &self.block_header
103 }
104
105 pub fn block_chain(&self) -> &ChainMmr {
108 &self.block_chain
109 }
110
111 pub fn input_notes(&self) -> &InputNotes<InputNote> {
113 &self.input_notes
114 }
115
116 pub fn into_parts(
121 self,
122 ) -> (Account, Option<Word>, BlockHeader, ChainMmr, InputNotes<InputNote>) {
123 (
124 self.account,
125 self.account_seed,
126 self.block_header,
127 self.block_chain,
128 self.input_notes,
129 )
130 }
131}
132
133impl Serializable for TransactionInputs {
134 fn write_into<W: ByteWriter>(&self, target: &mut W) {
135 self.account.write_into(target);
136 self.account_seed.write_into(target);
137 self.block_header.write_into(target);
138 self.block_chain.write_into(target);
139 self.input_notes.write_into(target);
140 }
141}
142
143impl Deserializable for TransactionInputs {
144 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
145 let account = Account::read_from(source)?;
146 let account_seed = source.read()?;
147 let block_header = BlockHeader::read_from(source)?;
148 let block_chain = ChainMmr::read_from(source)?;
149 let input_notes = InputNotes::read_from(source)?;
150 Self::new(account, account_seed, block_header, block_chain, input_notes)
151 .map_err(|err| DeserializationError::InvalidValue(format!("{}", err)))
152 }
153}
154
155pub trait ToInputNoteCommitments {
165 fn nullifier(&self) -> Nullifier;
166 fn note_commitment(&self) -> Option<Digest>;
167}
168
169#[derive(Debug, Clone)]
178pub struct InputNotes<T> {
179 notes: Vec<T>,
180 commitment: Digest,
181}
182
183impl<T: ToInputNoteCommitments> InputNotes<T> {
184 pub fn new(notes: Vec<T>) -> Result<Self, TransactionInputError> {
193 if notes.len() > MAX_INPUT_NOTES_PER_TX {
194 return Err(TransactionInputError::TooManyInputNotes(notes.len()));
195 }
196
197 let mut seen_notes = BTreeSet::new();
198 for note in notes.iter() {
199 if !seen_notes.insert(note.nullifier().inner()) {
200 return Err(TransactionInputError::DuplicateInputNote(note.nullifier()));
201 }
202 }
203
204 let commitment = build_input_note_commitment(¬es);
205
206 Ok(Self { notes, commitment })
207 }
208
209 pub fn new_unchecked(notes: Vec<T>) -> Self {
218 let commitment = build_input_note_commitment(¬es);
219 Self { notes, commitment }
220 }
221
222 pub fn commitment(&self) -> Digest {
234 self.commitment
235 }
236
237 pub fn num_notes(&self) -> usize {
239 self.notes.len()
240 }
241
242 pub fn is_empty(&self) -> bool {
244 self.notes.is_empty()
245 }
246
247 pub fn get_note(&self, idx: usize) -> &T {
249 &self.notes[idx]
250 }
251
252 pub fn iter(&self) -> impl Iterator<Item = &T> {
257 self.notes.iter()
258 }
259
260 pub fn into_vec(self) -> Vec<T> {
265 self.notes
266 }
267}
268
269impl<T> IntoIterator for InputNotes<T> {
270 type Item = T;
271 type IntoIter = alloc::vec::IntoIter<Self::Item>;
272
273 fn into_iter(self) -> Self::IntoIter {
274 self.notes.into_iter()
275 }
276}
277
278impl<'a, T> IntoIterator for &'a InputNotes<T> {
279 type Item = &'a T;
280 type IntoIter = alloc::slice::Iter<'a, T>;
281
282 fn into_iter(self) -> alloc::slice::Iter<'a, T> {
283 self.notes.iter()
284 }
285}
286
287impl<T: PartialEq> PartialEq for InputNotes<T> {
288 fn eq(&self, other: &Self) -> bool {
289 self.notes == other.notes
290 }
291}
292
293impl<T: Eq> Eq for InputNotes<T> {}
294
295impl<T: ToInputNoteCommitments> Default for InputNotes<T> {
296 fn default() -> Self {
297 Self {
298 notes: Vec::new(),
299 commitment: build_input_note_commitment::<T>(&[]),
300 }
301 }
302}
303
304impl<T: Serializable> Serializable for InputNotes<T> {
308 fn write_into<W: ByteWriter>(&self, target: &mut W) {
309 assert!(self.notes.len() <= u16::MAX.into());
311 target.write_u16(self.notes.len() as u16);
312 target.write_many(&self.notes);
313 }
314}
315
316impl<T: Deserializable + ToInputNoteCommitments> Deserializable for InputNotes<T> {
317 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
318 let num_notes = source.read_u16()?;
319 let notes = source.read_many::<T>(num_notes.into())?;
320 Self::new(notes).map_err(|err| DeserializationError::InvalidValue(format!("{}", err)))
321 }
322}
323
324fn build_input_note_commitment<T: ToInputNoteCommitments>(notes: &[T]) -> Digest {
328 if notes.is_empty() {
330 return Digest::default();
331 }
332
333 let mut elements: Vec<Felt> = Vec::with_capacity(notes.len() * 2);
334 for commitment_data in notes {
335 let nullifier = commitment_data.nullifier();
336 let empty_word_or_note_commitment = &commitment_data
337 .note_commitment()
338 .map_or(Word::default(), |note_id| note_id.into());
339
340 elements.extend_from_slice(nullifier.as_elements());
341 elements.extend_from_slice(empty_word_or_note_commitment);
342 }
343 Hasher::hash_elements(&elements)
344}
345
346const AUTHENTICATED: u8 = 0;
350const UNAUTHENTICATED: u8 = 1;
351
352#[derive(Debug, Clone, PartialEq, Eq)]
354pub enum InputNote {
355 Authenticated { note: Note, proof: NoteInclusionProof },
357
358 Unauthenticated { note: Note },
361}
362
363impl InputNote {
364 pub fn authenticated(note: Note, proof: NoteInclusionProof) -> Self {
369 Self::Authenticated { note, proof }
370 }
371
372 pub fn unauthenticated(note: Note) -> Self {
374 Self::Unauthenticated { note }
375 }
376
377 pub fn id(&self) -> NoteId {
382 self.note().id()
383 }
384
385 pub fn note(&self) -> &Note {
387 match self {
388 Self::Authenticated { note, .. } => note,
389 Self::Unauthenticated { note } => note,
390 }
391 }
392
393 pub fn proof(&self) -> Option<&NoteInclusionProof> {
395 match self {
396 Self::Authenticated { proof, .. } => Some(proof),
397 Self::Unauthenticated { .. } => None,
398 }
399 }
400
401 pub fn location(&self) -> Option<&NoteLocation> {
403 self.proof().map(|proof| proof.location())
404 }
405}
406
407fn validate_is_in_block(
409 note: &Note,
410 proof: &NoteInclusionProof,
411 block_header: &BlockHeader,
412) -> Result<(), TransactionInputError> {
413 let note_index = proof.location().node_index_in_block().into();
414 let note_commitment = note.commitment();
415 proof
416 .note_path()
417 .verify(note_index, note_commitment, &block_header.note_root())
418 .map_err(|_| {
419 TransactionInputError::InputNoteNotInBlock(note.id(), proof.location().block_num())
420 })
421}
422
423impl ToInputNoteCommitments for InputNote {
424 fn nullifier(&self) -> Nullifier {
425 self.note().nullifier()
426 }
427
428 fn note_commitment(&self) -> Option<Digest> {
429 match self {
430 InputNote::Authenticated { .. } => None,
431 InputNote::Unauthenticated { note } => Some(note.commitment()),
432 }
433 }
434}
435
436impl ToInputNoteCommitments for &InputNote {
437 fn nullifier(&self) -> Nullifier {
438 (*self).nullifier()
439 }
440
441 fn note_commitment(&self) -> Option<Digest> {
442 (*self).note_commitment()
443 }
444}
445
446impl Serializable for InputNote {
450 fn write_into<W: ByteWriter>(&self, target: &mut W) {
451 match self {
452 Self::Authenticated { note, proof } => {
453 target.write(AUTHENTICATED);
454 target.write(note);
455 target.write(proof);
456 },
457 Self::Unauthenticated { note } => {
458 target.write(UNAUTHENTICATED);
459 target.write(note);
460 },
461 }
462 }
463}
464
465impl Deserializable for InputNote {
466 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
467 match source.read_u8()? {
468 AUTHENTICATED => {
469 let note = Note::read_from(source)?;
470 let proof = NoteInclusionProof::read_from(source)?;
471 Ok(Self::Authenticated { note, proof })
472 },
473 UNAUTHENTICATED => {
474 let note = Note::read_from(source)?;
475 Ok(Self::Unauthenticated { note })
476 },
477 v => Err(DeserializationError::InvalidValue(format!("invalid input note type: {v}"))),
478 }
479 }
480}
481
482pub fn validate_account_seed(
487 account: &Account,
488 block_header: &BlockHeader,
489 block_chain: &ChainMmr,
490 account_seed: Option<Word>,
491) -> Result<(), TransactionInputError> {
492 match (account.is_new(), account_seed) {
493 (true, Some(seed)) => {
494 let anchor_block_number = BlockNumber::from_epoch(account.id().anchor_epoch());
495
496 let anchor_block_commitment = if block_header.block_num() == anchor_block_number {
497 block_header.commitment()
498 } else {
499 let anchor_block_header =
500 block_chain.get_block(anchor_block_number).ok_or_else(|| {
501 TransactionInputError::AnchorBlockHeaderNotProvidedForNewAccount(
502 account.id().anchor_epoch(),
503 )
504 })?;
505 anchor_block_header.commitment()
506 };
507
508 let anchor = AccountIdAnchor::new(anchor_block_number, anchor_block_commitment)
509 .map_err(TransactionInputError::InvalidAccountIdSeed)?;
510
511 let account_id = AccountId::new(
512 seed,
513 anchor,
514 account.id().version(),
515 account.code().commitment(),
516 account.storage().commitment(),
517 )
518 .map_err(TransactionInputError::InvalidAccountIdSeed)?;
519
520 if account_id != account.id() {
521 return Err(TransactionInputError::InconsistentAccountSeed {
522 expected: account.id(),
523 actual: account_id,
524 });
525 }
526
527 Ok(())
528 },
529 (true, None) => Err(TransactionInputError::AccountSeedNotProvidedForNewAccount),
530 (false, Some(_)) => Err(TransactionInputError::AccountSeedProvidedForExistingAccount),
531 (false, None) => Ok(()),
532 }
533}