miden_objects/transaction/
proven_tx.rs

1use alloc::{string::ToString, vec::Vec};
2
3use super::{InputNote, ToInputNoteCommitments};
4use crate::{
5    ACCOUNT_UPDATE_MAX_SIZE, ProvenTransactionError,
6    account::delta::AccountUpdateDetails,
7    block::BlockNumber,
8    note::NoteHeader,
9    transaction::{
10        AccountId, Digest, InputNotes, Nullifier, OutputNote, OutputNotes, TransactionId,
11    },
12    utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
13    vm::ExecutionProof,
14};
15
16// PROVEN TRANSACTION
17// ================================================================================================
18
19/// Result of executing and proving a transaction. Contains all the data required to verify that a
20/// transaction was executed correctly.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ProvenTransaction {
23    /// A unique identifier for the transaction, see [TransactionId] for additional details.
24    id: TransactionId,
25
26    /// Account update data.
27    account_update: TxAccountUpdate,
28
29    /// Committed details of all notes consumed by the transaction.
30    input_notes: InputNotes<InputNoteCommitment>,
31
32    /// Notes created by the transaction. For private notes, this will contain only note headers,
33    /// while for public notes this will also contain full note details.
34    output_notes: OutputNotes,
35
36    /// [`BlockNumber`] of the transaction's reference block.
37    ///
38    /// This is not needed for proving the transaction, but it is useful for the node to lookup the
39    /// block.
40    ref_block_num: BlockNumber,
41
42    /// The block commitment of the transaction's reference block.
43    ref_block_commitment: Digest,
44
45    /// The block number by which the transaction will expire, as defined by the executed scripts.
46    expiration_block_num: BlockNumber,
47
48    /// A STARK proof that attests to the correct execution of the transaction.
49    proof: ExecutionProof,
50}
51
52impl ProvenTransaction {
53    /// Returns unique identifier of this transaction.
54    pub fn id(&self) -> TransactionId {
55        self.id
56    }
57
58    /// Returns ID of the account against which this transaction was executed.
59    pub fn account_id(&self) -> AccountId {
60        self.account_update.account_id()
61    }
62
63    /// Returns the account update details.
64    pub fn account_update(&self) -> &TxAccountUpdate {
65        &self.account_update
66    }
67
68    /// Returns a reference to the notes consumed by the transaction.
69    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
70        &self.input_notes
71    }
72
73    /// Returns a reference to the notes produced by the transaction.
74    pub fn output_notes(&self) -> &OutputNotes {
75        &self.output_notes
76    }
77
78    /// Returns the proof of the transaction.
79    pub fn proof(&self) -> &ExecutionProof {
80        &self.proof
81    }
82
83    /// Returns the number of the reference block the transaction was executed against.
84    pub fn ref_block_num(&self) -> BlockNumber {
85        self.ref_block_num
86    }
87
88    /// Returns the commitment of the block transaction was executed against.
89    pub fn ref_block_commitment(&self) -> Digest {
90        self.ref_block_commitment
91    }
92
93    /// Returns an iterator of the headers of unauthenticated input notes in this transaction.
94    pub fn get_unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
95        self.input_notes.iter().filter_map(|note| note.header())
96    }
97
98    /// Returns the block number at which the transaction will expire.
99    pub fn expiration_block_num(&self) -> BlockNumber {
100        self.expiration_block_num
101    }
102
103    /// Returns an iterator over the nullifiers of all input notes in this transaction.
104    ///
105    /// This includes both authenticated and unauthenticated notes.
106    pub fn get_nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
107        self.input_notes.iter().map(InputNoteCommitment::nullifier)
108    }
109
110    // HELPER METHODS
111    // --------------------------------------------------------------------------------------------
112
113    fn validate(self) -> Result<Self, ProvenTransactionError> {
114        if self.account_id().is_public() {
115            self.account_update.validate()?;
116
117            let is_new_account =
118                self.account_update.initial_state_commitment() == Digest::default();
119            match self.account_update.details() {
120                AccountUpdateDetails::Private => {
121                    return Err(ProvenTransactionError::PublicAccountMissingDetails(
122                        self.account_id(),
123                    ));
124                },
125                AccountUpdateDetails::New(account) => {
126                    if !is_new_account {
127                        return Err(
128                            ProvenTransactionError::ExistingPublicAccountRequiresDeltaDetails(
129                                self.account_id(),
130                            ),
131                        );
132                    }
133                    if account.id() != self.account_id() {
134                        return Err(ProvenTransactionError::AccountIdMismatch {
135                            tx_account_id: self.account_id(),
136                            details_account_id: account.id(),
137                        });
138                    }
139                    if account.commitment() != self.account_update.final_state_commitment() {
140                        return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
141                            tx_final_commitment: self.account_update.final_state_commitment(),
142                            details_commitment: account.commitment(),
143                        });
144                    }
145                },
146                AccountUpdateDetails::Delta(_) => {
147                    if is_new_account {
148                        return Err(ProvenTransactionError::NewPublicAccountRequiresFullDetails(
149                            self.account_id(),
150                        ));
151                    }
152                },
153            }
154        } else if !self.account_update.is_private() {
155            return Err(ProvenTransactionError::PrivateAccountWithDetails(self.account_id()));
156        }
157
158        Ok(self)
159    }
160}
161
162impl Serializable for ProvenTransaction {
163    fn write_into<W: ByteWriter>(&self, target: &mut W) {
164        self.account_update.write_into(target);
165        self.input_notes.write_into(target);
166        self.output_notes.write_into(target);
167        self.ref_block_num.write_into(target);
168        self.ref_block_commitment.write_into(target);
169        self.expiration_block_num.write_into(target);
170        self.proof.write_into(target);
171    }
172}
173
174impl Deserializable for ProvenTransaction {
175    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
176        let account_update = TxAccountUpdate::read_from(source)?;
177
178        let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
179        let output_notes = OutputNotes::read_from(source)?;
180
181        let ref_block_num = BlockNumber::read_from(source)?;
182        let ref_block_commitment = Digest::read_from(source)?;
183        let expiration_block_num = BlockNumber::read_from(source)?;
184        let proof = ExecutionProof::read_from(source)?;
185
186        let id = TransactionId::new(
187            account_update.initial_state_commitment(),
188            account_update.final_state_commitment(),
189            input_notes.commitment(),
190            output_notes.commitment(),
191        );
192
193        let proven_transaction = Self {
194            id,
195            account_update,
196            input_notes,
197            output_notes,
198            ref_block_num,
199            ref_block_commitment,
200            expiration_block_num,
201            proof,
202        };
203
204        proven_transaction
205            .validate()
206            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
207    }
208}
209
210// PROVEN TRANSACTION BUILDER
211// ================================================================================================
212
213/// Builder for a proven transaction.
214#[derive(Clone, Debug)]
215pub struct ProvenTransactionBuilder {
216    /// ID of the account that the transaction was executed against.
217    account_id: AccountId,
218
219    /// The commitment of the account before the transaction was executed.
220    initial_account_commitment: Digest,
221
222    /// The commitment of the account after the transaction was executed.
223    final_account_commitment: Digest,
224
225    /// State changes to the account due to the transaction.
226    account_update_details: AccountUpdateDetails,
227
228    /// List of [InputNoteCommitment]s of all consumed notes by the transaction.
229    input_notes: Vec<InputNoteCommitment>,
230
231    /// List of [OutputNote]s of all notes created by the transaction.
232    output_notes: Vec<OutputNote>,
233
234    /// [`BlockNumber`] of the transaction's reference block.
235    ref_block_num: BlockNumber,
236
237    /// Block [Digest] of the transaction's reference block.
238    ref_block_commitment: Digest,
239
240    /// The block number by which the transaction will expire, as defined by the executed scripts.
241    expiration_block_num: BlockNumber,
242
243    /// A STARK proof that attests to the correct execution of the transaction.
244    proof: ExecutionProof,
245}
246
247impl ProvenTransactionBuilder {
248    // CONSTRUCTOR
249    // --------------------------------------------------------------------------------------------
250
251    /// Returns a [ProvenTransactionBuilder] used to build a [ProvenTransaction].
252    pub fn new(
253        account_id: AccountId,
254        initial_account_commitment: Digest,
255        final_account_commitment: Digest,
256        ref_block_num: BlockNumber,
257        ref_block_commitment: Digest,
258        expiration_block_num: BlockNumber,
259        proof: ExecutionProof,
260    ) -> Self {
261        Self {
262            account_id,
263            initial_account_commitment,
264            final_account_commitment,
265            account_update_details: AccountUpdateDetails::Private,
266            input_notes: Vec::new(),
267            output_notes: Vec::new(),
268            ref_block_num,
269            ref_block_commitment,
270            expiration_block_num,
271            proof,
272        }
273    }
274
275    // PUBLIC ACCESSORS
276    // --------------------------------------------------------------------------------------------
277
278    /// Sets the account's update details.
279    pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
280        self.account_update_details = details;
281        self
282    }
283
284    /// Add notes consumed by the transaction.
285    pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
286    where
287        I: IntoIterator<Item = T>,
288        T: Into<InputNoteCommitment>,
289    {
290        self.input_notes.extend(notes.into_iter().map(|note| note.into()));
291        self
292    }
293
294    /// Add notes produced by the transaction.
295    pub fn add_output_notes<T>(mut self, notes: T) -> Self
296    where
297        T: IntoIterator<Item = OutputNote>,
298    {
299        self.output_notes.extend(notes);
300        self
301    }
302
303    /// Builds the [ProvenTransaction].
304    ///
305    /// # Errors
306    ///
307    /// An error will be returned if a public account is used without provided on-chain detail.
308    /// Or if the account details, i.e. account ID and final hash, don't match the transaction.
309    pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
310        let input_notes =
311            InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
312        let output_notes = OutputNotes::new(self.output_notes)
313            .map_err(ProvenTransactionError::OutputNotesError)?;
314        let id = TransactionId::new(
315            self.initial_account_commitment,
316            self.final_account_commitment,
317            input_notes.commitment(),
318            output_notes.commitment(),
319        );
320        let account_update = TxAccountUpdate::new(
321            self.account_id,
322            self.initial_account_commitment,
323            self.final_account_commitment,
324            self.account_update_details,
325        );
326
327        let proven_transaction = ProvenTransaction {
328            id,
329            account_update,
330            input_notes,
331            output_notes,
332            ref_block_num: self.ref_block_num,
333            ref_block_commitment: self.ref_block_commitment,
334            expiration_block_num: self.expiration_block_num,
335            proof: self.proof,
336        };
337
338        proven_transaction.validate()
339    }
340}
341
342// TRANSACTION ACCOUNT UPDATE
343// ================================================================================================
344
345/// Describes the changes made to the account state resulting from a transaction execution.
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct TxAccountUpdate {
348    /// ID of the account updated by a transaction.
349    account_id: AccountId,
350
351    /// The commitment of the account before a transaction was executed.
352    ///
353    /// Set to `Digest::default()` for new accounts.
354    init_state_commitment: Digest,
355
356    /// The commitment of the account state after a transaction was executed.
357    final_state_commitment: Digest,
358
359    /// A set of changes which can be applied the account's state prior to the transaction to
360    /// get the account state after the transaction. For private accounts this is set to
361    /// [AccountUpdateDetails::Private].
362    details: AccountUpdateDetails,
363}
364
365impl TxAccountUpdate {
366    /// Returns a new [TxAccountUpdate] instantiated from the specified components.
367    pub const fn new(
368        account_id: AccountId,
369        init_state_commitment: Digest,
370        final_state_commitment: Digest,
371        details: AccountUpdateDetails,
372    ) -> Self {
373        Self {
374            account_id,
375            init_state_commitment,
376            final_state_commitment,
377            details,
378        }
379    }
380
381    /// Returns the ID of the updated account.
382    pub fn account_id(&self) -> AccountId {
383        self.account_id
384    }
385
386    /// Returns the commitment of the account's initial state.
387    pub fn initial_state_commitment(&self) -> Digest {
388        self.init_state_commitment
389    }
390
391    /// Returns the commitment of the account's after a transaction was executed.
392    pub fn final_state_commitment(&self) -> Digest {
393        self.final_state_commitment
394    }
395
396    /// Returns the description of the updates for public accounts.
397    ///
398    /// These descriptions can be used to build the new account state from the previous account
399    /// state.
400    pub fn details(&self) -> &AccountUpdateDetails {
401        &self.details
402    }
403
404    /// Returns `true` if the account update details are for a private account.
405    pub fn is_private(&self) -> bool {
406        self.details.is_private()
407    }
408
409    /// Validates the following properties of the account update:
410    ///
411    /// - The size of the serialized account update does not exceed [`ACCOUNT_UPDATE_MAX_SIZE`].
412    pub fn validate(&self) -> Result<(), ProvenTransactionError> {
413        let account_update_size = self.details().get_size_hint();
414        if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
415            Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
416                account_id: self.account_id(),
417                update_size: account_update_size,
418            })
419        } else {
420            Ok(())
421        }
422    }
423}
424
425impl Serializable for TxAccountUpdate {
426    fn write_into<W: ByteWriter>(&self, target: &mut W) {
427        self.account_id.write_into(target);
428        self.init_state_commitment.write_into(target);
429        self.final_state_commitment.write_into(target);
430        self.details.write_into(target);
431    }
432}
433
434impl Deserializable for TxAccountUpdate {
435    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
436        Ok(Self {
437            account_id: AccountId::read_from(source)?,
438            init_state_commitment: Digest::read_from(source)?,
439            final_state_commitment: Digest::read_from(source)?,
440            details: AccountUpdateDetails::read_from(source)?,
441        })
442    }
443}
444
445// INPUT NOTE COMMITMENT
446// ================================================================================================
447
448/// The commitment to an input note.
449///
450/// For notes authenticated by the transaction kernel, the commitment consists only of the note's
451/// nullifier. For notes whose authentication is delayed to batch/block kernels, the commitment
452/// also includes full note header (i.e., note ID and metadata).
453#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct InputNoteCommitment {
455    nullifier: Nullifier,
456    header: Option<NoteHeader>,
457}
458
459impl InputNoteCommitment {
460    /// Returns the nullifier of the input note committed to by this commitment.
461    pub fn nullifier(&self) -> Nullifier {
462        self.nullifier
463    }
464
465    /// Returns the header of the input committed to by this commitment.
466    ///
467    /// Note headers are present only for notes whose presence in the change has not yet been
468    /// authenticated.
469    pub fn header(&self) -> Option<&NoteHeader> {
470        self.header.as_ref()
471    }
472
473    /// Returns true if this commitment is for a note whose presence in the chain has been
474    /// authenticated.
475    ///
476    /// Authenticated notes are represented solely by their nullifiers and are missing the note
477    /// header.
478    pub fn is_authenticated(&self) -> bool {
479        self.header.is_none()
480    }
481}
482
483impl From<InputNote> for InputNoteCommitment {
484    fn from(note: InputNote) -> Self {
485        Self::from(&note)
486    }
487}
488
489impl From<&InputNote> for InputNoteCommitment {
490    fn from(note: &InputNote) -> Self {
491        match note {
492            InputNote::Authenticated { note, .. } => Self {
493                nullifier: note.nullifier(),
494                header: None,
495            },
496            InputNote::Unauthenticated { note } => Self {
497                nullifier: note.nullifier(),
498                header: Some(*note.header()),
499            },
500        }
501    }
502}
503
504impl From<Nullifier> for InputNoteCommitment {
505    fn from(nullifier: Nullifier) -> Self {
506        Self { nullifier, header: None }
507    }
508}
509
510impl ToInputNoteCommitments for InputNoteCommitment {
511    fn nullifier(&self) -> Nullifier {
512        self.nullifier
513    }
514
515    fn note_commitment(&self) -> Option<Digest> {
516        self.header.map(|header| header.commitment())
517    }
518}
519
520// SERIALIZATION
521// ------------------------------------------------------------------------------------------------
522
523impl Serializable for InputNoteCommitment {
524    fn write_into<W: ByteWriter>(&self, target: &mut W) {
525        self.nullifier.write_into(target);
526        self.header.write_into(target);
527    }
528}
529
530impl Deserializable for InputNoteCommitment {
531    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
532        let nullifier = Nullifier::read_from(source)?;
533        let header = <Option<NoteHeader>>::read_from(source)?;
534
535        Ok(Self { nullifier, header })
536    }
537}
538
539// TESTS
540// ================================================================================================
541
542#[cfg(test)]
543mod tests {
544    use alloc::collections::BTreeMap;
545
546    use miden_verifier::ExecutionProof;
547    use vm_core::utils::Deserializable;
548    use winter_air::proof::Proof;
549    use winter_rand_utils::rand_array;
550
551    use super::ProvenTransaction;
552    use crate::{
553        ACCOUNT_UPDATE_MAX_SIZE, Digest, EMPTY_WORD, ONE, ProvenTransactionError, ZERO,
554        account::{
555            AccountDelta, AccountId, AccountIdVersion, AccountStorageDelta, AccountStorageMode,
556            AccountType, AccountVaultDelta, StorageMapDelta, delta::AccountUpdateDetails,
557        },
558        block::BlockNumber,
559        testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
560        transaction::{ProvenTransactionBuilder, TxAccountUpdate},
561        utils::Serializable,
562    };
563
564    fn check_if_sync<T: Sync>() {}
565    fn check_if_send<T: Send>() {}
566
567    /// [ProvenTransaction] being Sync is part of its public API and changing it is backwards
568    /// incompatible.
569    #[test]
570    fn test_proven_transaction_is_sync() {
571        check_if_sync::<ProvenTransaction>();
572    }
573
574    /// [ProvenTransaction] being Send is part of its public API and changing it is backwards
575    /// incompatible.
576    #[test]
577    fn test_proven_transaction_is_send() {
578        check_if_send::<ProvenTransaction>();
579    }
580
581    #[test]
582    fn account_update_size_limit_not_exceeded() {
583        // A small delta does not exceed the limit.
584        let storage_delta = AccountStorageDelta::from_iters(
585            [1, 2, 3, 4],
586            [(2, [ONE, ONE, ONE, ONE]), (3, [ONE, ONE, ZERO, ONE])],
587            [],
588        );
589        let delta =
590            AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
591        let details = AccountUpdateDetails::Delta(delta);
592        TxAccountUpdate::new(
593            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
594            Digest::new(EMPTY_WORD),
595            Digest::new(EMPTY_WORD),
596            details,
597        )
598        .validate()
599        .unwrap();
600    }
601
602    #[test]
603    fn account_update_size_limit_exceeded() {
604        let mut map = BTreeMap::new();
605        // The number of entries in the map required to exceed the limit.
606        // We divide by each entry's size which consists of a key (digest) and a value (word), both
607        // 32 bytes in size.
608        let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
609        for _ in 0..required_entries {
610            map.insert(Digest::new(rand_array()), rand_array());
611        }
612        let storage_delta = StorageMapDelta::new(map);
613
614        // A delta that exceeds the limit returns an error.
615        let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
616        let delta =
617            AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
618        let details = AccountUpdateDetails::Delta(delta);
619        let details_size = details.get_size_hint();
620
621        let err = TxAccountUpdate::new(
622            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
623            Digest::new(EMPTY_WORD),
624            Digest::new(EMPTY_WORD),
625            details,
626        )
627        .validate()
628        .unwrap_err();
629
630        assert!(
631            matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
632        );
633    }
634
635    #[test]
636    fn test_proven_tx_serde_roundtrip() {
637        let account_id = AccountId::dummy(
638            [1; 15],
639            AccountIdVersion::Version0,
640            AccountType::FungibleFaucet,
641            AccountStorageMode::Private,
642        );
643        let initial_account_commitment =
644            [2; 32].try_into().expect("failed to create initial account commitment");
645        let final_account_commitment =
646            [3; 32].try_into().expect("failed to create final account commitment");
647        let ref_block_num = BlockNumber::from(1);
648        let ref_block_commitment = Digest::default();
649        let expiration_block_num = BlockNumber::from(2);
650        let proof = ExecutionProof::new(Proof::new_dummy(), Default::default());
651
652        let tx = ProvenTransactionBuilder::new(
653            account_id,
654            initial_account_commitment,
655            final_account_commitment,
656            ref_block_num,
657            ref_block_commitment,
658            expiration_block_num,
659            proof,
660        )
661        .build()
662        .expect("failed to build proven transaction");
663
664        let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
665
666        assert_eq!(tx, deserialized);
667    }
668}