miden_objects/transaction/
inputs.rs

1use 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// TRANSACTION INPUTS
14// ================================================================================================
15
16/// Contains the data required to execute a transaction.
17#[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    // CONSTRUCTOR
28    // --------------------------------------------------------------------------------------------
29    /// Returns new [TransactionInputs] instantiated with the specified parameters.
30    ///
31    /// # Errors
32    /// Returns an error if:
33    /// - For a new account, account seed is not provided or the provided seed is invalid.
34    /// - For an existing account, account seed was provided.
35    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 the seed
43        validate_account_seed(&account, &block_header, &block_chain, account_seed)?;
44
45        // check the block_chain and block_header are consistent
46        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        // check the authentication paths of the input notes.
62        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    // PUBLIC ACCESSORS
88    // --------------------------------------------------------------------------------------------
89
90    /// Returns account against which the transaction is to be executed.
91    pub fn account(&self) -> &Account {
92        &self.account
93    }
94
95    /// For newly-created accounts, returns the account seed; for existing accounts, returns None.
96    pub fn account_seed(&self) -> Option<Word> {
97        self.account_seed
98    }
99
100    /// Returns block header for the block referenced by the transaction.
101    pub fn block_header(&self) -> &BlockHeader {
102        &self.block_header
103    }
104
105    /// Returns chain MMR containing authentication paths for all notes consumed by the
106    /// transaction.
107    pub fn block_chain(&self) -> &ChainMmr {
108        &self.block_chain
109    }
110
111    /// Returns the notes to be consumed in the transaction.
112    pub fn input_notes(&self) -> &InputNotes<InputNote> {
113        &self.input_notes
114    }
115
116    // CONVERSIONS
117    // --------------------------------------------------------------------------------------------
118
119    /// Consumes these transaction inputs and returns their underlying components.
120    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
155// TO INPUT NOTE COMMITMENT
156// ================================================================================================
157
158/// Specifies the data used by the transaction kernel to commit to a note.
159///
160/// The commitment is composed of:
161///
162/// - nullifier, which prevents double spend and provides unlinkability.
163/// - an optional note commitment, which allows for delayed note authentication.
164pub trait ToInputNoteCommitments {
165    fn nullifier(&self) -> Nullifier;
166    fn note_commitment(&self) -> Option<Digest>;
167}
168
169// INPUT NOTES
170// ================================================================================================
171
172/// Input notes for a transaction, empty if the transaction does not consume notes.
173///
174/// This structure is generic over `T`, so it can be used to create the input notes for transaction
175/// execution, which require the note's details to run the transaction kernel, and the input notes
176/// for proof verification, which require only the commitment data.
177#[derive(Debug, Clone)]
178pub struct InputNotes<T> {
179    notes: Vec<T>,
180    commitment: Digest,
181}
182
183impl<T: ToInputNoteCommitments> InputNotes<T> {
184    // CONSTRUCTOR
185    // --------------------------------------------------------------------------------------------
186    /// Returns new [InputNotes] instantiated from the provided vector of notes.
187    ///
188    /// # Errors
189    /// Returns an error if:
190    /// - The total number of notes is greater than [`MAX_INPUT_NOTES_PER_TX`].
191    /// - The vector of notes contains duplicates.
192    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(&notes);
205
206        Ok(Self { notes, commitment })
207    }
208
209    /// Returns new [`InputNotes`] instantiated from the provided vector of notes without checking
210    /// their validity.
211    ///
212    /// This is exposed for use in transaction batches, but should generally not be used.
213    ///
214    /// # Warning
215    ///
216    /// This does not run the checks from [`InputNotes::new`], so the latter should be preferred.
217    pub fn new_unchecked(notes: Vec<T>) -> Self {
218        let commitment = build_input_note_commitment(&notes);
219        Self { notes, commitment }
220    }
221
222    // PUBLIC ACCESSORS
223    // --------------------------------------------------------------------------------------------
224
225    /// Returns a sequential hash of nullifiers for all notes.
226    ///
227    /// For non empty lists the commitment is defined as:
228    ///
229    /// > hash(nullifier_0 || noteid0_or_zero || nullifier_1 || noteid1_or_zero || .. || nullifier_n
230    /// > || noteidn_or_zero)
231    ///
232    /// Otherwise defined as ZERO for empty lists.
233    pub fn commitment(&self) -> Digest {
234        self.commitment
235    }
236
237    /// Returns total number of input notes.
238    pub fn num_notes(&self) -> usize {
239        self.notes.len()
240    }
241
242    /// Returns true if this [InputNotes] does not contain any notes.
243    pub fn is_empty(&self) -> bool {
244        self.notes.is_empty()
245    }
246
247    /// Returns a reference to the note located at the specified index.
248    pub fn get_note(&self, idx: usize) -> &T {
249        &self.notes[idx]
250    }
251
252    // ITERATORS
253    // --------------------------------------------------------------------------------------------
254
255    /// Returns an iterator over notes in this [InputNotes].
256    pub fn iter(&self) -> impl Iterator<Item = &T> {
257        self.notes.iter()
258    }
259
260    // CONVERSIONS
261    // --------------------------------------------------------------------------------------------
262
263    /// Converts self into a vector of input notes.
264    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
304// SERIALIZATION
305// ------------------------------------------------------------------------------------------------
306
307impl<T: Serializable> Serializable for InputNotes<T> {
308    fn write_into<W: ByteWriter>(&self, target: &mut W) {
309        // assert is OK here because we enforce max number of notes in the constructor
310        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
324// HELPER FUNCTIONS
325// ------------------------------------------------------------------------------------------------
326
327fn build_input_note_commitment<T: ToInputNoteCommitments>(notes: &[T]) -> Digest {
328    // Note: This implementation must be kept in sync with the kernel's `process_input_notes_data`
329    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
346// INPUT NOTE
347// ================================================================================================
348
349const AUTHENTICATED: u8 = 0;
350const UNAUTHENTICATED: u8 = 1;
351
352/// An input note for a transaction.
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub enum InputNote {
355    /// Input notes whose existences in the chain is verified by the transaction kernel.
356    Authenticated { note: Note, proof: NoteInclusionProof },
357
358    /// Input notes whose existence in the chain is not verified by the transaction kernel, but
359    /// instead is delegated to the rollup kernels.
360    Unauthenticated { note: Note },
361}
362
363impl InputNote {
364    // CONSTRUCTORS
365    // -------------------------------------------------------------------------------------------
366
367    /// Returns an authenticated [InputNote].
368    pub fn authenticated(note: Note, proof: NoteInclusionProof) -> Self {
369        Self::Authenticated { note, proof }
370    }
371
372    /// Returns an unauthenticated [InputNote].
373    pub fn unauthenticated(note: Note) -> Self {
374        Self::Unauthenticated { note }
375    }
376
377    // ACCESSORS
378    // -------------------------------------------------------------------------------------------
379
380    /// Returns the ID of the note.
381    pub fn id(&self) -> NoteId {
382        self.note().id()
383    }
384
385    /// Returns a reference to the underlying note.
386    pub fn note(&self) -> &Note {
387        match self {
388            Self::Authenticated { note, .. } => note,
389            Self::Unauthenticated { note } => note,
390        }
391    }
392
393    /// Returns a reference to the inclusion proof of the note.
394    pub fn proof(&self) -> Option<&NoteInclusionProof> {
395        match self {
396            Self::Authenticated { proof, .. } => Some(proof),
397            Self::Unauthenticated { .. } => None,
398        }
399    }
400
401    /// Returns a reference to the location of the note.
402    pub fn location(&self) -> Option<&NoteLocation> {
403        self.proof().map(|proof| proof.location())
404    }
405}
406
407/// Validates whether the provided note belongs to the note tree of the specified block.
408fn 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
446// SERIALIZATION
447// ------------------------------------------------------------------------------------------------
448
449impl 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
482// INPUT NOTE
483// ================================================================================================
484
485/// Validates that the provided seed is valid for this account.
486pub 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}