Skip to main content

miden_protocol/transaction/
proven_tx.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use super::{InputNote, ToInputNoteCommitments};
5use crate::Word;
6use crate::account::{AccountUpdateDetails, validate_new_public_account};
7use crate::block::BlockNumber;
8use crate::errors::ProvenTransactionError;
9use crate::note::{NoteHeader, NoteId};
10use crate::transaction::{
11    AccountId,
12    InputNotes,
13    Nullifier,
14    OutputNote,
15    OutputNotes,
16    TransactionId,
17};
18use crate::utils::serde::{
19    ByteReader,
20    ByteWriter,
21    Deserializable,
22    DeserializationError,
23    Serializable,
24};
25use crate::vm::ExecutionProof;
26
27// PROVEN TRANSACTION
28// ================================================================================================
29
30/// Result of executing and proving a transaction. Contains all the data required to verify that a
31/// transaction was executed correctly.
32///
33/// A proven transaction must not be empty. A transaction is empty if the account state is unchanged
34/// or the number of input notes is zero. This check prevents proving a transaction once and
35/// submitting it to the network many times. Output notes are not considered because they can be
36/// empty (i.e. contain no assets). Otherwise, a transaction with no account state change, no input
37/// notes and one such empty output note could be resubmitted many times to the network and fill up
38/// block space which is a form of DOS attack.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ProvenTransaction {
41    /// A unique identifier for the transaction, see [TransactionId] for additional details.
42    id: TransactionId,
43
44    /// Account update data.
45    account_update: TxAccountUpdate,
46
47    /// Committed details of all notes consumed by the transaction.
48    input_notes: InputNotes<InputNoteCommitment>,
49
50    /// Notes created by the transaction. For private notes, this will contain only note headers,
51    /// while for public notes this will also contain full note details.
52    output_notes: OutputNotes,
53
54    /// [`BlockNumber`] of the transaction's reference block.
55    ref_block_num: BlockNumber,
56
57    /// The block commitment of the transaction's reference block.
58    ref_block_commitment: Word,
59
60    /// The block number by which the transaction will expire, as defined by the executed scripts.
61    expiration_block_num: BlockNumber,
62
63    /// A STARK proof that attests to the correct execution of the transaction.
64    proof: ExecutionProof,
65}
66
67impl ProvenTransaction {
68    // CONSTRUCTOR
69    // --------------------------------------------------------------------------------------------
70
71    /// Creates a new [ProvenTransaction] from the specified components.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if:
76    /// - The total number of input notes is greater than
77    ///   [`MAX_INPUT_NOTES_PER_TX`](crate::constants::MAX_INPUT_NOTES_PER_TX).
78    /// - The vector of input notes contains duplicates.
79    /// - The total number of output notes is greater than
80    ///   [`MAX_OUTPUT_NOTES_PER_TX`](crate::constants::MAX_OUTPUT_NOTES_PER_TX).
81    /// - The vector of output notes contains duplicates.
82    /// - The set of input and output notes contains the same note.
83    /// - The transaction is empty, which is the case if the account state is unchanged or the
84    ///   number of input notes is zero.
85    /// - The commitment computed on the actual account delta contained in [`TxAccountUpdate`] does
86    ///   not match its declared account delta commitment.
87    pub fn new(
88        account_update: TxAccountUpdate,
89        input_notes: impl IntoIterator<Item = impl Into<InputNoteCommitment>>,
90        output_notes: impl IntoIterator<Item = impl Into<OutputNote>>,
91        ref_block_num: BlockNumber,
92        ref_block_commitment: Word,
93        expiration_block_num: BlockNumber,
94        proof: ExecutionProof,
95    ) -> Result<Self, ProvenTransactionError> {
96        let input_notes: Vec<InputNoteCommitment> =
97            input_notes.into_iter().map(Into::into).collect();
98        let output_notes: Vec<OutputNote> = output_notes.into_iter().map(Into::into).collect();
99
100        let input_notes =
101            InputNotes::new(input_notes).map_err(ProvenTransactionError::InputNotesError)?;
102        let output_notes =
103            OutputNotes::new(output_notes).map_err(ProvenTransactionError::OutputNotesError)?;
104
105        Self::from_parts(
106            account_update,
107            input_notes,
108            output_notes,
109            ref_block_num,
110            ref_block_commitment,
111            expiration_block_num,
112            proof,
113        )
114    }
115
116    // PUBLIC ACCESSORS
117    // --------------------------------------------------------------------------------------------
118
119    /// Returns unique identifier of this transaction.
120    pub fn id(&self) -> TransactionId {
121        self.id
122    }
123
124    /// Returns ID of the account against which this transaction was executed.
125    pub fn account_id(&self) -> AccountId {
126        self.account_update.account_id()
127    }
128
129    /// Returns the account update details.
130    pub fn account_update(&self) -> &TxAccountUpdate {
131        &self.account_update
132    }
133
134    /// Returns a reference to the notes consumed by the transaction.
135    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
136        &self.input_notes
137    }
138
139    /// Returns a reference to the notes produced by the transaction.
140    pub fn output_notes(&self) -> &OutputNotes {
141        &self.output_notes
142    }
143
144    /// Returns the proof of the transaction.
145    pub fn proof(&self) -> &ExecutionProof {
146        &self.proof
147    }
148
149    /// Returns the number of the reference block the transaction was executed against.
150    pub fn ref_block_num(&self) -> BlockNumber {
151        self.ref_block_num
152    }
153
154    /// Returns the commitment of the block transaction was executed against.
155    pub fn ref_block_commitment(&self) -> Word {
156        self.ref_block_commitment
157    }
158
159    /// Returns an iterator of the headers of unauthenticated input notes in this transaction.
160    pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
161        self.input_notes.iter().filter_map(|note| note.header())
162    }
163
164    /// Returns the block number at which the transaction will expire.
165    pub fn expiration_block_num(&self) -> BlockNumber {
166        self.expiration_block_num
167    }
168
169    /// Returns an iterator over the nullifiers of all input notes in this transaction.
170    ///
171    /// This includes both authenticated and unauthenticated notes.
172    pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
173        self.input_notes.iter().map(InputNoteCommitment::nullifier)
174    }
175
176    // HELPER METHODS
177    // --------------------------------------------------------------------------------------------
178
179    /// Creates a [`ProvenTransaction`] from its raw parts, enforcing all invariants.
180    ///
181    /// Both [`ProvenTransaction::new`] and [`ProvenTransaction::read_from`] funnel through this
182    /// constructor so that every invariant is checked on both the creation and deserialization
183    /// paths.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if:
188    /// - The transaction is empty (account state unchanged and no input notes).
189    /// - The same note ID appears as both an unauthenticated input and an output (circular
190    ///   dependency, see <https://github.com/0xMiden/protocol/issues/2796>).
191    /// - The commitment computed on the actual account delta does not match its declared account
192    ///   delta commitment.
193    fn from_parts(
194        account_update: TxAccountUpdate,
195        input_notes: InputNotes<InputNoteCommitment>,
196        output_notes: OutputNotes,
197        ref_block_num: BlockNumber,
198        ref_block_commitment: Word,
199        expiration_block_num: BlockNumber,
200        proof: ExecutionProof,
201    ) -> Result<Self, ProvenTransactionError> {
202        // Check that either the account state was changed or at least one note was consumed,
203        // otherwise this transaction is considered empty.
204        if account_update.initial_state_commitment() == account_update.final_state_commitment()
205            && input_notes.commitment().is_empty()
206        {
207            return Err(ProvenTransactionError::EmptyTransaction);
208        }
209
210        // Disallow creating and consuming notes with the same ID in a transaction. This is a
211        // circular dependency that can be abused (see https://github.com/0xMiden/protocol/issues/2796).
212        // This is only relevant for unauthenticated notes (notes with a header), since only these
213        // can be erased at batch or block level. Authenticated notes don't exhibit this issue.
214        for input_note in input_notes.iter().filter_map(InputNoteCommitment::header) {
215            if output_notes.iter().any(|output_note| output_note.id() == input_note.id()) {
216                return Err(ProvenTransactionError::NoteCreatedAndConsumed(input_note.id()));
217            }
218        }
219
220        let id = TransactionId::new(
221            account_update.initial_state_commitment(),
222            account_update.final_state_commitment(),
223            input_notes.commitment(),
224            output_notes.commitment(),
225        );
226
227        Ok(Self {
228            id,
229            account_update,
230            input_notes,
231            output_notes,
232            ref_block_num,
233            ref_block_commitment,
234            expiration_block_num,
235            proof,
236        })
237    }
238}
239
240impl Serializable for ProvenTransaction {
241    fn write_into<W: ByteWriter>(&self, target: &mut W) {
242        self.account_update.write_into(target);
243        self.input_notes.write_into(target);
244        self.output_notes.write_into(target);
245        self.ref_block_num.write_into(target);
246        self.ref_block_commitment.write_into(target);
247        self.expiration_block_num.write_into(target);
248        self.proof.write_into(target);
249    }
250}
251
252impl Deserializable for ProvenTransaction {
253    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
254        let account_update = TxAccountUpdate::read_from(source)?;
255
256        let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
257        let output_notes = OutputNotes::read_from(source)?;
258
259        let ref_block_num = BlockNumber::read_from(source)?;
260        let ref_block_commitment = Word::read_from(source)?;
261        let expiration_block_num = BlockNumber::read_from(source)?;
262        let proof = ExecutionProof::read_from(source)?;
263
264        Self::from_parts(
265            account_update,
266            input_notes,
267            output_notes,
268            ref_block_num,
269            ref_block_commitment,
270            expiration_block_num,
271            proof,
272        )
273        .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
274    }
275}
276
277// TRANSACTION ACCOUNT UPDATE
278// ================================================================================================
279
280/// Describes the changes made to the account state resulting from a transaction execution.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct TxAccountUpdate {
283    /// ID of the account updated by a transaction.
284    account_id: AccountId,
285
286    /// The commitment of the account before the transaction was executed.
287    ///
288    /// Set to `Word::empty()` for new accounts.
289    init_state_commitment: Word,
290
291    /// The commitment of the account state after the transaction was executed.
292    final_state_commitment: Word,
293
294    /// The commitment to the [`AccountPatch`](crate::account::AccountPatch) resulting from the
295    /// execution of the transaction, as computed by the transaction kernel in the epilogue. This
296    /// commitment is always set regardless of whether the account is public or private.
297    /// - When `details` is [`AccountUpdateDetails::Public`], it must equal the commitment of the
298    ///   patch carried in that variant.
299    /// - When `details` is [`AccountUpdateDetails::Private`], the patch itself is not transmitted
300    ///   and the commitment is validated implicitly as part of transaction verification.
301    account_patch_commitment: Word,
302
303    /// A description of the changes to the account that produces the post-transaction state when
304    /// applied to the pre-transaction state. For private accounts this is set to
305    /// [`AccountUpdateDetails::Private`].
306    details: AccountUpdateDetails,
307}
308
309impl TxAccountUpdate {
310    /// Returns a new [TxAccountUpdate] instantiated from the specified components.
311    ///
312    /// Returns an error if:
313    /// - The size of the serialized account update exceeds [`crate::ACCOUNT_UPDATE_MAX_SIZE`].
314    /// - The transaction was executed against an account with public state and its account ID does
315    ///   not match the ID of the patch in the account update.
316    /// - The transaction was executed against a _new_ account with public state and its commitment
317    ///   does not match the final state commitment of the account update.
318    /// - The transaction creates a _new_ account with public state and the update is of type
319    ///   [`AccountUpdateDetails::Public`] but the account patch is not a full state patch.
320    /// - The transaction was executed against a private account and the account update is _not_ of
321    ///   type [`AccountUpdateDetails::Private`].
322    /// - The transaction was executed against an account with public state and the update is of
323    ///   type [`AccountUpdateDetails::Private`].
324    pub fn new(
325        account_id: AccountId,
326        init_state_commitment: Word,
327        final_state_commitment: Word,
328        account_patch_commitment: Word,
329        details: AccountUpdateDetails,
330    ) -> Result<Self, ProvenTransactionError> {
331        let account_update = Self {
332            account_id,
333            init_state_commitment,
334            final_state_commitment,
335            account_patch_commitment,
336            details,
337        };
338
339        account_update.details.validate_size(account_id)?;
340
341        let Some(patch) = account_update.details.validate_for_account(account_id)? else {
342            return Ok(account_update);
343        };
344
345        let actual_patch_commitment = patch.to_commitment();
346        if account_patch_commitment != actual_patch_commitment {
347            return Err(ProvenTransactionError::AccountPatchCommitmentMismatch {
348                expected_patch_commitment: account_patch_commitment,
349                actual_patch_commitment,
350            });
351        }
352
353        if account_update.initial_state_commitment().is_empty() {
354            validate_new_public_account(patch, account_update.final_state_commitment)?;
355        }
356
357        Ok(account_update)
358    }
359
360    /// Returns the ID of the updated account.
361    pub fn account_id(&self) -> AccountId {
362        self.account_id
363    }
364
365    /// Returns the commitment of the account before the transaction was executed.
366    pub fn initial_state_commitment(&self) -> Word {
367        self.init_state_commitment
368    }
369
370    /// Returns the commitment of the account after the transaction was executed.
371    pub fn final_state_commitment(&self) -> Word {
372        self.final_state_commitment
373    }
374
375    /// Returns the commitment to the [`AccountPatch`](crate::account::AccountPatch) resulting from
376    /// the execution of the transaction.
377    pub fn account_patch_commitment(&self) -> Word {
378        self.account_patch_commitment
379    }
380
381    /// Returns the description of the updates for public accounts.
382    ///
383    /// These descriptions can be used to build the new account state from the previous account
384    /// state.
385    pub fn details(&self) -> &AccountUpdateDetails {
386        &self.details
387    }
388
389    /// Returns `true` if the account update details are for a private account.
390    pub fn is_private(&self) -> bool {
391        self.details.is_private()
392    }
393}
394
395impl Serializable for TxAccountUpdate {
396    fn write_into<W: ByteWriter>(&self, target: &mut W) {
397        self.account_id.write_into(target);
398        self.init_state_commitment.write_into(target);
399        self.final_state_commitment.write_into(target);
400        self.account_patch_commitment.write_into(target);
401        self.details.write_into(target);
402    }
403}
404
405impl Deserializable for TxAccountUpdate {
406    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
407        let account_id = AccountId::read_from(source)?;
408        let init_state_commitment = Word::read_from(source)?;
409        let final_state_commitment = Word::read_from(source)?;
410        let account_patch_commitment = Word::read_from(source)?;
411        let details = AccountUpdateDetails::read_from(source)?;
412
413        Self::new(
414            account_id,
415            init_state_commitment,
416            final_state_commitment,
417            account_patch_commitment,
418            details,
419        )
420        .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
421    }
422}
423
424// INPUT NOTE COMMITMENT
425// ================================================================================================
426
427/// The commitment to an input note.
428///
429/// For notes authenticated by the transaction kernel, the commitment consists only of the note's
430/// nullifier. For notes whose authentication is delayed to batch/block kernels, the commitment
431/// also includes full note header (i.e., note ID and metadata).
432#[derive(Debug, Clone, PartialEq, Eq)]
433pub struct InputNoteCommitment {
434    nullifier: Nullifier,
435    header: Option<NoteHeader>,
436}
437
438impl InputNoteCommitment {
439    /// Returns a new [InputNoteCommitment] instantiated from the provided nullifier and optional
440    /// note header.
441    ///
442    /// Note: this method does not validate that the provided nullifier and header are consistent
443    /// with each other (i.e., it does not check that the nullifier was derived from the note
444    /// referenced by the header).
445    pub fn from_parts_unchecked(nullifier: Nullifier, header: Option<NoteHeader>) -> Self {
446        Self { nullifier, header }
447    }
448
449    /// Returns the nullifier of the input note committed to by this commitment.
450    pub fn nullifier(&self) -> Nullifier {
451        self.nullifier
452    }
453
454    /// Returns the header of the input committed to by this commitment.
455    ///
456    /// Note headers are present only for notes whose presence in the change has not yet been
457    /// authenticated.
458    pub fn header(&self) -> Option<&NoteHeader> {
459        self.header.as_ref()
460    }
461
462    /// Returns true if this commitment is for a note whose presence in the chain has been
463    /// authenticated.
464    ///
465    /// Authenticated notes are represented solely by their nullifiers and are missing the note
466    /// header.
467    pub fn is_authenticated(&self) -> bool {
468        self.header.is_none()
469    }
470}
471
472impl From<InputNote> for InputNoteCommitment {
473    fn from(note: InputNote) -> Self {
474        Self::from(&note)
475    }
476}
477
478impl From<&InputNote> for InputNoteCommitment {
479    fn from(note: &InputNote) -> Self {
480        match note {
481            InputNote::Authenticated { note, .. } => Self {
482                nullifier: note.nullifier(),
483                header: None,
484            },
485            InputNote::Unauthenticated { note } => Self {
486                nullifier: note.nullifier(),
487                header: Some(*note.header()),
488            },
489        }
490    }
491}
492
493impl From<Nullifier> for InputNoteCommitment {
494    fn from(nullifier: Nullifier) -> Self {
495        Self { nullifier, header: None }
496    }
497}
498
499impl ToInputNoteCommitments for InputNoteCommitment {
500    fn nullifier(&self) -> Nullifier {
501        self.nullifier
502    }
503
504    fn note_id(&self) -> Option<NoteId> {
505        self.header.as_ref().map(NoteHeader::id)
506    }
507}
508
509// SERIALIZATION
510// ------------------------------------------------------------------------------------------------
511
512impl Serializable for InputNoteCommitment {
513    fn write_into<W: ByteWriter>(&self, target: &mut W) {
514        self.nullifier.write_into(target);
515        self.header.write_into(target);
516    }
517}
518
519impl Deserializable for InputNoteCommitment {
520    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
521        let nullifier = Nullifier::read_from(source)?;
522        let header = <Option<NoteHeader>>::read_from(source)?;
523
524        Ok(Self::from_parts_unchecked(nullifier, header))
525    }
526}
527
528// TESTS
529// ================================================================================================
530
531#[cfg(test)]
532mod tests {
533    use alloc::collections::BTreeMap;
534    use alloc::vec::Vec;
535
536    use anyhow::Context;
537    use assert_matches::assert_matches;
538    use miden_crypto::rand::test_utils::rand_value;
539
540    use super::ProvenTransaction;
541    use crate::account::{
542        Account,
543        AccountId,
544        AccountPatch,
545        AccountStoragePatch,
546        AccountType,
547        AccountUpdateDetails,
548        AccountVaultPatch,
549        StorageMapKey,
550        StorageMapPatch,
551        StorageMapPatchEntries,
552        StorageSlotName,
553    };
554    use crate::block::BlockNumber;
555    use crate::errors::ProvenTransactionError;
556    use crate::testing::account_id::{
557        ACCOUNT_ID_PRIVATE_SENDER,
558        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
559    };
560    use crate::testing::add_component::AddComponent;
561    use crate::testing::noop_auth_component::NoopAuthComponent;
562    use crate::transaction::{InputNoteCommitment, OutputNote, TxAccountUpdate};
563    use crate::utils::serde::{Deserializable, Serializable};
564    use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, Felt, Word};
565
566    fn check_if_sync<T: Sync>() {}
567    fn check_if_send<T: Send>() {}
568
569    /// [ProvenTransaction] being Sync is part of its public API and changing it is backwards
570    /// incompatible.
571    #[test]
572    fn test_proven_transaction_is_sync() {
573        check_if_sync::<ProvenTransaction>();
574    }
575
576    /// [ProvenTransaction] being Send is part of its public API and changing it is backwards
577    /// incompatible.
578    #[test]
579    fn test_proven_transaction_is_send() {
580        check_if_send::<ProvenTransaction>();
581    }
582
583    #[test]
584    fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
585        // A small account's delta does not exceed the limit.
586        let account = Account::builder([9; 32])
587            .account_type(AccountType::Public)
588            .with_component(NoopAuthComponent)
589            .with_component(AddComponent)
590            .build_existing()?;
591        let patch = AccountPatch::try_from(account.clone())?;
592        let patch_commitment = patch.to_commitment();
593
594        let details = AccountUpdateDetails::Public(patch);
595
596        TxAccountUpdate::new(
597            account.id(),
598            account.to_commitment(),
599            account.to_commitment(),
600            patch_commitment,
601            details,
602        )?;
603
604        Ok(())
605    }
606
607    #[test]
608    fn account_update_size_limit_exceeded() {
609        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
610        let mut map = BTreeMap::new();
611        // The number of entries in the map required to exceed the limit.
612        // We divide by each entry's size which consists of a key (digest) and a value (word), both
613        // 32 bytes in size.
614        let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
615        for _ in 0..required_entries {
616            map.insert(StorageMapKey::from_raw(rand_value()), rand_value::<Word>());
617        }
618        let storage_patch = StorageMapPatch::Update {
619            entries: StorageMapPatchEntries::from_raw(map),
620        };
621
622        // A patch that exceeds the limit returns an error.
623        let storage_patch =
624            AccountStoragePatch::from_iters([], [], [(StorageSlotName::mock(4), storage_patch)]);
625        let patch = AccountPatch::new(
626            account_id,
627            storage_patch,
628            AccountVaultPatch::default(),
629            None,
630            Some(Felt::from(2u32)),
631        )
632        .unwrap();
633        let details = AccountUpdateDetails::Public(patch);
634        let details_size = details.get_size_hint();
635
636        let err = TxAccountUpdate::new(
637            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
638            EMPTY_WORD,
639            EMPTY_WORD,
640            EMPTY_WORD,
641            details,
642        )
643        .unwrap_err();
644
645        assert!(
646            matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
647        );
648    }
649
650    /// Building a [`TxAccountUpdate`] for a public account fails if the account ID in the patch
651    /// does not match the account ID passed to the constructor.
652    #[test]
653    fn account_update_id_mismatch_between_account_id_and_patch() -> anyhow::Result<()> {
654        let patch_account = Account::builder([9; 32])
655            .account_type(AccountType::Public)
656            .with_component(NoopAuthComponent)
657            .with_component(AddComponent)
658            .build_existing()?;
659        let patch = AccountPatch::try_from(patch_account.clone())?;
660
661        let other_account_id =
662            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)?;
663        assert_ne!(patch_account.id(), other_account_id);
664
665        let err = TxAccountUpdate::new(
666            other_account_id,
667            patch_account.to_commitment(),
668            patch_account.to_commitment(),
669            Word::empty(),
670            AccountUpdateDetails::Public(patch),
671        )
672        .unwrap_err();
673
674        assert_matches!(
675            err,
676            ProvenTransactionError::AccountIdMismatch {
677                tx_account_id,
678                details_account_id,
679            } => {
680                assert_eq!(tx_account_id, other_account_id);
681                assert_eq!(details_account_id, patch_account.id());
682            }
683        );
684
685        Ok(())
686    }
687
688    #[test]
689    fn account_patch_commitment_mismatch() -> anyhow::Result<()> {
690        let account = Account::builder([9; 32])
691            .account_type(AccountType::Public)
692            .with_component(NoopAuthComponent)
693            .with_component(AddComponent)
694            .build_existing()?;
695        let patch = AccountPatch::try_from(account.clone())?;
696        let actual_patch_commitment = patch.to_commitment();
697        // EMPTY_WORD is all-zeros and differs from a real Rescue hash.
698        let wrong_commitment = EMPTY_WORD;
699        assert_ne!(wrong_commitment, actual_patch_commitment);
700        let err = TxAccountUpdate::new(
701            account.id(),
702            account.to_commitment(),
703            account.to_commitment(),
704            wrong_commitment,
705            AccountUpdateDetails::Public(patch),
706        )
707        .unwrap_err();
708        assert_matches!(
709            err,
710            ProvenTransactionError::AccountPatchCommitmentMismatch {
711                expected_patch_commitment,
712                actual_patch_commitment: returned_actual,
713            } if expected_patch_commitment == wrong_commitment
714                && returned_actual == actual_patch_commitment
715        );
716        Ok(())
717    }
718    #[test]
719    fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
720        let account_id =
721            AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
722        let initial_account_commitment =
723            [2; 32].try_into().expect("failed to create initial account commitment");
724        let final_account_commitment =
725            [3; 32].try_into().expect("failed to create final account commitment");
726        let account_patch_commitment =
727            [4; 32].try_into().expect("failed to create account patch commitment");
728        let ref_block_num = BlockNumber::from(1);
729        let ref_block_commitment = Word::empty();
730        let expiration_block_num = BlockNumber::from(2);
731        let proof = crate::testing::dummy_execution_proof();
732
733        let account_update = TxAccountUpdate::new(
734            account_id,
735            initial_account_commitment,
736            final_account_commitment,
737            account_patch_commitment,
738            AccountUpdateDetails::Private,
739        )
740        .context("failed to build account update")?;
741
742        let tx = ProvenTransaction::new(
743            account_update,
744            Vec::<InputNoteCommitment>::new(),
745            Vec::<OutputNote>::new(),
746            ref_block_num,
747            ref_block_commitment,
748            expiration_block_num,
749            proof,
750        )
751        .context("failed to build proven transaction")?;
752
753        let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
754
755        assert_eq!(tx, deserialized);
756
757        Ok(())
758    }
759}