Skip to main content

miden_protocol/transaction/
proven_tx.rs

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