Skip to main content

miden_protocol/transaction/
proven_tx.rs

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