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 account::{Account, AccountId, AccountIdAnchor},
7 block::BlockNumber,
8 note::{Note, NoteId, NoteInclusionProof, NoteLocation, Nullifier},
9 utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
10 TransactionInputError, MAX_INPUT_NOTES_PER_TX,
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_root() {
55 return Err(TransactionInputError::InconsistentChainRoot {
56 expected: block_header.chain_root(),
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_hash(&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 commitment(&self) -> Digest {
221 self.commitment
222 }
223
224 pub fn num_notes(&self) -> usize {
226 self.notes.len()
227 }
228
229 pub fn is_empty(&self) -> bool {
231 self.notes.is_empty()
232 }
233
234 pub fn get_note(&self, idx: usize) -> &T {
236 &self.notes[idx]
237 }
238
239 pub fn iter(&self) -> impl Iterator<Item = &T> {
244 self.notes.iter()
245 }
246
247 pub fn into_vec(self) -> Vec<T> {
252 self.notes
253 }
254}
255
256impl<T> IntoIterator for InputNotes<T> {
257 type Item = T;
258 type IntoIter = alloc::vec::IntoIter<Self::Item>;
259
260 fn into_iter(self) -> Self::IntoIter {
261 self.notes.into_iter()
262 }
263}
264
265impl<'a, T> IntoIterator for &'a InputNotes<T> {
266 type Item = &'a T;
267 type IntoIter = alloc::slice::Iter<'a, T>;
268
269 fn into_iter(self) -> alloc::slice::Iter<'a, T> {
270 self.notes.iter()
271 }
272}
273
274impl<T: PartialEq> PartialEq for InputNotes<T> {
275 fn eq(&self, other: &Self) -> bool {
276 self.notes == other.notes
277 }
278}
279
280impl<T: Eq> Eq for InputNotes<T> {}
281
282impl<T: ToInputNoteCommitments> Default for InputNotes<T> {
283 fn default() -> Self {
284 Self {
285 notes: Vec::new(),
286 commitment: build_input_note_commitment::<T>(&[]),
287 }
288 }
289}
290
291impl<T: Serializable> Serializable for InputNotes<T> {
295 fn write_into<W: ByteWriter>(&self, target: &mut W) {
296 assert!(self.notes.len() <= u16::MAX.into());
298 target.write_u16(self.notes.len() as u16);
299 target.write_many(&self.notes);
300 }
301}
302
303impl<T: Deserializable + ToInputNoteCommitments> Deserializable for InputNotes<T> {
304 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
305 let num_notes = source.read_u16()?;
306 let notes = source.read_many::<T>(num_notes.into())?;
307 Self::new(notes).map_err(|err| DeserializationError::InvalidValue(format!("{}", err)))
308 }
309}
310
311fn build_input_note_commitment<T: ToInputNoteCommitments>(notes: &[T]) -> Digest {
315 if notes.is_empty() {
317 return Digest::default();
318 }
319
320 let mut elements: Vec<Felt> = Vec::with_capacity(notes.len() * 2);
321 for commitment_data in notes {
322 let nullifier = commitment_data.nullifier();
323 let zero_or_note_hash =
324 &commitment_data.note_hash().map_or(Word::default(), |note_id| note_id.into());
325
326 elements.extend_from_slice(nullifier.as_elements());
327 elements.extend_from_slice(zero_or_note_hash);
328 }
329 Hasher::hash_elements(&elements)
330}
331
332const AUTHENTICATED: u8 = 0;
336const UNAUTHENTICATED: u8 = 1;
337
338#[derive(Debug, Clone, PartialEq, Eq)]
340pub enum InputNote {
341 Authenticated { note: Note, proof: NoteInclusionProof },
343
344 Unauthenticated { note: Note },
347}
348
349impl InputNote {
350 pub fn authenticated(note: Note, proof: NoteInclusionProof) -> Self {
355 Self::Authenticated { note, proof }
356 }
357
358 pub fn unauthenticated(note: Note) -> Self {
360 Self::Unauthenticated { note }
361 }
362
363 pub fn id(&self) -> NoteId {
368 self.note().id()
369 }
370
371 pub fn note(&self) -> &Note {
373 match self {
374 Self::Authenticated { note, .. } => note,
375 Self::Unauthenticated { note } => note,
376 }
377 }
378
379 pub fn proof(&self) -> Option<&NoteInclusionProof> {
381 match self {
382 Self::Authenticated { proof, .. } => Some(proof),
383 Self::Unauthenticated { .. } => None,
384 }
385 }
386
387 pub fn location(&self) -> Option<&NoteLocation> {
389 self.proof().map(|proof| proof.location())
390 }
391}
392
393fn validate_is_in_block(
395 note: &Note,
396 proof: &NoteInclusionProof,
397 block_header: &BlockHeader,
398) -> Result<(), TransactionInputError> {
399 let note_index = proof.location().node_index_in_block().into();
400 let note_hash = note.hash();
401 proof
402 .note_path()
403 .verify(note_index, note_hash, &block_header.note_root())
404 .map_err(|_| {
405 TransactionInputError::InputNoteNotInBlock(note.id(), proof.location().block_num())
406 })
407}
408
409impl ToInputNoteCommitments for InputNote {
410 fn nullifier(&self) -> Nullifier {
411 self.note().nullifier()
412 }
413
414 fn note_hash(&self) -> Option<Digest> {
415 match self {
416 InputNote::Authenticated { .. } => None,
417 InputNote::Unauthenticated { note } => Some(note.hash()),
418 }
419 }
420}
421
422impl ToInputNoteCommitments for &InputNote {
423 fn nullifier(&self) -> Nullifier {
424 (*self).nullifier()
425 }
426
427 fn note_hash(&self) -> Option<Digest> {
428 (*self).note_hash()
429 }
430}
431
432impl Serializable for InputNote {
436 fn write_into<W: ByteWriter>(&self, target: &mut W) {
437 match self {
438 Self::Authenticated { note, proof } => {
439 target.write(AUTHENTICATED);
440 target.write(note);
441 target.write(proof);
442 },
443 Self::Unauthenticated { note } => {
444 target.write(UNAUTHENTICATED);
445 target.write(note);
446 },
447 }
448 }
449}
450
451impl Deserializable for InputNote {
452 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
453 match source.read_u8()? {
454 AUTHENTICATED => {
455 let note = Note::read_from(source)?;
456 let proof = NoteInclusionProof::read_from(source)?;
457 Ok(Self::Authenticated { note, proof })
458 },
459 UNAUTHENTICATED => {
460 let note = Note::read_from(source)?;
461 Ok(Self::Unauthenticated { note })
462 },
463 v => Err(DeserializationError::InvalidValue(format!("invalid input note type: {v}"))),
464 }
465 }
466}
467
468pub fn validate_account_seed(
473 account: &Account,
474 block_header: &BlockHeader,
475 block_chain: &ChainMmr,
476 account_seed: Option<Word>,
477) -> Result<(), TransactionInputError> {
478 match (account.is_new(), account_seed) {
479 (true, Some(seed)) => {
480 let anchor_block_number = BlockNumber::from_epoch(account.id().anchor_epoch());
481
482 let anchor_block_hash = if block_header.block_num() == anchor_block_number {
483 block_header.hash()
484 } else {
485 let anchor_block_header =
486 block_chain.get_block(anchor_block_number).ok_or_else(|| {
487 TransactionInputError::AnchorBlockHeaderNotProvidedForNewAccount(
488 account.id().anchor_epoch(),
489 )
490 })?;
491 anchor_block_header.hash()
492 };
493
494 let anchor = AccountIdAnchor::new(anchor_block_number, anchor_block_hash)
495 .map_err(TransactionInputError::InvalidAccountIdSeed)?;
496
497 let account_id = AccountId::new(
498 seed,
499 anchor,
500 account.id().version(),
501 account.code().commitment(),
502 account.storage().commitment(),
503 )
504 .map_err(TransactionInputError::InvalidAccountIdSeed)?;
505
506 if account_id != account.id() {
507 return Err(TransactionInputError::InconsistentAccountSeed {
508 expected: account.id(),
509 actual: account_id,
510 });
511 }
512
513 Ok(())
514 },
515 (true, None) => Err(TransactionInputError::AccountSeedNotProvidedForNewAccount),
516 (false, Some(_)) => Err(TransactionInputError::AccountSeedProvidedForExistingAccount),
517 (false, None) => Ok(()),
518 }
519}