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    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// 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_root() {
55            return Err(TransactionInputError::InconsistentChainRoot {
56                expected: block_header.chain_root(),
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 hash, which allows for delayed note authentication.
164pub trait ToInputNoteCommitments {
165    fn nullifier(&self) -> Nullifier;
166    fn note_hash(&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    // PUBLIC ACCESSORS
210    // --------------------------------------------------------------------------------------------
211
212    /// Returns a sequential hash of nullifiers for all notes.
213    ///
214    /// For non empty lists the commitment is defined as:
215    ///
216    /// > hash(nullifier_0 || noteid0_or_zero || nullifier_1 || noteid1_or_zero || .. || nullifier_n
217    /// > || noteidn_or_zero)
218    ///
219    /// Otherwise defined as ZERO for empty lists.
220    pub fn commitment(&self) -> Digest {
221        self.commitment
222    }
223
224    /// Returns total number of input notes.
225    pub fn num_notes(&self) -> usize {
226        self.notes.len()
227    }
228
229    /// Returns true if this [InputNotes] does not contain any notes.
230    pub fn is_empty(&self) -> bool {
231        self.notes.is_empty()
232    }
233
234    /// Returns a reference to the note located at the specified index.
235    pub fn get_note(&self, idx: usize) -> &T {
236        &self.notes[idx]
237    }
238
239    // ITERATORS
240    // --------------------------------------------------------------------------------------------
241
242    /// Returns an iterator over notes in this [InputNotes].
243    pub fn iter(&self) -> impl Iterator<Item = &T> {
244        self.notes.iter()
245    }
246
247    // CONVERSIONS
248    // --------------------------------------------------------------------------------------------
249
250    /// Converts self into a vector of input notes.
251    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
291// SERIALIZATION
292// ------------------------------------------------------------------------------------------------
293
294impl<T: Serializable> Serializable for InputNotes<T> {
295    fn write_into<W: ByteWriter>(&self, target: &mut W) {
296        // assert is OK here because we enforce max number of notes in the constructor
297        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
311// HELPER FUNCTIONS
312// ------------------------------------------------------------------------------------------------
313
314fn build_input_note_commitment<T: ToInputNoteCommitments>(notes: &[T]) -> Digest {
315    // Note: This implementation must be kept in sync with the kernel's `process_input_notes_data`
316    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
332// INPUT NOTE
333// ================================================================================================
334
335const AUTHENTICATED: u8 = 0;
336const UNAUTHENTICATED: u8 = 1;
337
338/// An input note for a transaction.
339#[derive(Debug, Clone, PartialEq, Eq)]
340pub enum InputNote {
341    /// Input notes whose existences in the chain is verified by the transaction kernel.
342    Authenticated { note: Note, proof: NoteInclusionProof },
343
344    /// Input notes whose existence in the chain is not verified by the transaction kernel, but
345    /// instead is delegated to the rollup kernels.
346    Unauthenticated { note: Note },
347}
348
349impl InputNote {
350    // CONSTRUCTORS
351    // -------------------------------------------------------------------------------------------
352
353    /// Returns an authenticated [InputNote].
354    pub fn authenticated(note: Note, proof: NoteInclusionProof) -> Self {
355        Self::Authenticated { note, proof }
356    }
357
358    /// Returns an unauthenticated [InputNote].
359    pub fn unauthenticated(note: Note) -> Self {
360        Self::Unauthenticated { note }
361    }
362
363    // ACCESSORS
364    // -------------------------------------------------------------------------------------------
365
366    /// Returns the ID of the note.
367    pub fn id(&self) -> NoteId {
368        self.note().id()
369    }
370
371    /// Returns a reference to the underlying note.
372    pub fn note(&self) -> &Note {
373        match self {
374            Self::Authenticated { note, .. } => note,
375            Self::Unauthenticated { note } => note,
376        }
377    }
378
379    /// Returns a reference to the inclusion proof of the note.
380    pub fn proof(&self) -> Option<&NoteInclusionProof> {
381        match self {
382            Self::Authenticated { proof, .. } => Some(proof),
383            Self::Unauthenticated { .. } => None,
384        }
385    }
386
387    /// Returns a reference to the location of the note.
388    pub fn location(&self) -> Option<&NoteLocation> {
389        self.proof().map(|proof| proof.location())
390    }
391}
392
393/// Validates whether the provided note belongs to the note tree of the specified block.
394fn 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
432// SERIALIZATION
433// ------------------------------------------------------------------------------------------------
434
435impl 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
468// INPUT NOTE
469// ================================================================================================
470
471/// Validates that the provided seed is valid for this account.
472pub 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}