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::account::{Account, AccountUpdateDetails};
6use crate::block::BlockNumber;
7use crate::errors::ProvenTransactionError;
8use crate::note::{NoteHeader, NoteId};
9use crate::transaction::{
10    AccountId,
11    InputNotes,
12    Nullifier,
13    OutputNote,
14    OutputNotes,
15    TransactionId,
16};
17use crate::utils::serde::{
18    ByteReader,
19    ByteWriter,
20    Deserializable,
21    DeserializationError,
22    Serializable,
23};
24use crate::vm::ExecutionProof;
25use crate::{ACCOUNT_UPDATE_MAX_SIZE, Word};
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 [`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        let account_update_size = account_update.details.get_size_hint();
340        if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
341            return Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
342                account_id,
343                update_size: account_update_size,
344            });
345        }
346
347        if account_id.is_private() {
348            if account_update.details.is_private() {
349                return Ok(account_update);
350            } else {
351                return Err(ProvenTransactionError::PrivateAccountWithDetails(account_id));
352            }
353        }
354
355        match account_update.details() {
356            AccountUpdateDetails::Private => {
357                return Err(ProvenTransactionError::PublicStateAccountMissingDetails(
358                    account_update.account_id(),
359                ));
360            },
361            AccountUpdateDetails::Public(patch) => {
362                if patch.id() != account_id {
363                    return Err(ProvenTransactionError::AccountIdMismatch {
364                        tx_account_id: account_id,
365                        details_account_id: patch.id(),
366                    });
367                }
368
369                let actual_patch_commitment = patch.to_commitment();
370                if account_patch_commitment != actual_patch_commitment {
371                    return Err(ProvenTransactionError::AccountPatchCommitmentMismatch {
372                        expected_patch_commitment: account_patch_commitment,
373                        actual_patch_commitment,
374                    });
375                }
376
377                let is_new_account = account_update.initial_state_commitment().is_empty();
378                if is_new_account {
379                    // Validate that for new accounts, the full account state can be constructed
380                    // from the patch. This will fail if it is not such a full state patch.
381                    let account = Account::try_from(patch).map_err(|err| {
382                        ProvenTransactionError::NewPublicStateAccountRequiresFullStatePatch {
383                            id: patch.id(),
384                            source: err,
385                        }
386                    })?;
387
388                    if account.to_commitment() != account_update.final_state_commitment {
389                        return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
390                            tx_final_commitment: account_update.final_state_commitment,
391                            details_commitment: account.to_commitment(),
392                        });
393                    }
394                }
395            },
396        }
397
398        Ok(account_update)
399    }
400
401    /// Returns the ID of the updated account.
402    pub fn account_id(&self) -> AccountId {
403        self.account_id
404    }
405
406    /// Returns the commitment of the account before the transaction was executed.
407    pub fn initial_state_commitment(&self) -> Word {
408        self.init_state_commitment
409    }
410
411    /// Returns the commitment of the account after the transaction was executed.
412    pub fn final_state_commitment(&self) -> Word {
413        self.final_state_commitment
414    }
415
416    /// Returns the commitment to the [`AccountPatch`](crate::account::AccountPatch) resulting from
417    /// the execution of the transaction.
418    pub fn account_patch_commitment(&self) -> Word {
419        self.account_patch_commitment
420    }
421
422    /// Returns the description of the updates for public accounts.
423    ///
424    /// These descriptions can be used to build the new account state from the previous account
425    /// state.
426    pub fn details(&self) -> &AccountUpdateDetails {
427        &self.details
428    }
429
430    /// Returns `true` if the account update details are for a private account.
431    pub fn is_private(&self) -> bool {
432        self.details.is_private()
433    }
434}
435
436impl Serializable for TxAccountUpdate {
437    fn write_into<W: ByteWriter>(&self, target: &mut W) {
438        self.account_id.write_into(target);
439        self.init_state_commitment.write_into(target);
440        self.final_state_commitment.write_into(target);
441        self.account_patch_commitment.write_into(target);
442        self.details.write_into(target);
443    }
444}
445
446impl Deserializable for TxAccountUpdate {
447    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
448        let account_id = AccountId::read_from(source)?;
449        let init_state_commitment = Word::read_from(source)?;
450        let final_state_commitment = Word::read_from(source)?;
451        let account_patch_commitment = Word::read_from(source)?;
452        let details = AccountUpdateDetails::read_from(source)?;
453
454        Self::new(
455            account_id,
456            init_state_commitment,
457            final_state_commitment,
458            account_patch_commitment,
459            details,
460        )
461        .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
462    }
463}
464
465// INPUT NOTE COMMITMENT
466// ================================================================================================
467
468/// The commitment to an input note.
469///
470/// For notes authenticated by the transaction kernel, the commitment consists only of the note's
471/// nullifier. For notes whose authentication is delayed to batch/block kernels, the commitment
472/// also includes full note header (i.e., note ID and metadata).
473#[derive(Debug, Clone, PartialEq, Eq)]
474pub struct InputNoteCommitment {
475    nullifier: Nullifier,
476    header: Option<NoteHeader>,
477}
478
479impl InputNoteCommitment {
480    /// Returns a new [InputNoteCommitment] instantiated from the provided nullifier and optional
481    /// note header.
482    ///
483    /// Note: this method does not validate that the provided nullifier and header are consistent
484    /// with each other (i.e., it does not check that the nullifier was derived from the note
485    /// referenced by the header).
486    pub fn from_parts_unchecked(nullifier: Nullifier, header: Option<NoteHeader>) -> Self {
487        Self { nullifier, header }
488    }
489
490    /// Returns the nullifier of the input note committed to by this commitment.
491    pub fn nullifier(&self) -> Nullifier {
492        self.nullifier
493    }
494
495    /// Returns the header of the input committed to by this commitment.
496    ///
497    /// Note headers are present only for notes whose presence in the change has not yet been
498    /// authenticated.
499    pub fn header(&self) -> Option<&NoteHeader> {
500        self.header.as_ref()
501    }
502
503    /// Returns true if this commitment is for a note whose presence in the chain has been
504    /// authenticated.
505    ///
506    /// Authenticated notes are represented solely by their nullifiers and are missing the note
507    /// header.
508    pub fn is_authenticated(&self) -> bool {
509        self.header.is_none()
510    }
511}
512
513impl From<InputNote> for InputNoteCommitment {
514    fn from(note: InputNote) -> Self {
515        Self::from(&note)
516    }
517}
518
519impl From<&InputNote> for InputNoteCommitment {
520    fn from(note: &InputNote) -> Self {
521        match note {
522            InputNote::Authenticated { note, .. } => Self {
523                nullifier: note.nullifier(),
524                header: None,
525            },
526            InputNote::Unauthenticated { note } => Self {
527                nullifier: note.nullifier(),
528                header: Some(*note.header()),
529            },
530        }
531    }
532}
533
534impl From<Nullifier> for InputNoteCommitment {
535    fn from(nullifier: Nullifier) -> Self {
536        Self { nullifier, header: None }
537    }
538}
539
540impl ToInputNoteCommitments for InputNoteCommitment {
541    fn nullifier(&self) -> Nullifier {
542        self.nullifier
543    }
544
545    fn note_id(&self) -> Option<NoteId> {
546        self.header.as_ref().map(NoteHeader::id)
547    }
548}
549
550// SERIALIZATION
551// ------------------------------------------------------------------------------------------------
552
553impl Serializable for InputNoteCommitment {
554    fn write_into<W: ByteWriter>(&self, target: &mut W) {
555        self.nullifier.write_into(target);
556        self.header.write_into(target);
557    }
558}
559
560impl Deserializable for InputNoteCommitment {
561    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
562        let nullifier = Nullifier::read_from(source)?;
563        let header = <Option<NoteHeader>>::read_from(source)?;
564
565        Ok(Self::from_parts_unchecked(nullifier, header))
566    }
567}
568
569// TESTS
570// ================================================================================================
571
572#[cfg(test)]
573mod tests {
574    use alloc::collections::BTreeMap;
575    use alloc::vec::Vec;
576
577    use anyhow::Context;
578    use assert_matches::assert_matches;
579    use miden_crypto::rand::test_utils::rand_value;
580    use miden_verifier::ExecutionProof;
581
582    use super::ProvenTransaction;
583    use crate::account::{
584        Account,
585        AccountId,
586        AccountPatch,
587        AccountStoragePatch,
588        AccountType,
589        AccountUpdateDetails,
590        AccountVaultPatch,
591        StorageMapKey,
592        StorageMapPatch,
593        StorageMapPatchEntries,
594        StorageSlotName,
595    };
596    use crate::block::BlockNumber;
597    use crate::errors::ProvenTransactionError;
598    use crate::testing::account_id::{
599        ACCOUNT_ID_PRIVATE_SENDER,
600        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
601    };
602    use crate::testing::add_component::AddComponent;
603    use crate::testing::noop_auth_component::NoopAuthComponent;
604    use crate::transaction::{InputNoteCommitment, OutputNote, TxAccountUpdate};
605    use crate::utils::serde::{Deserializable, Serializable};
606    use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, Felt, Word};
607
608    fn check_if_sync<T: Sync>() {}
609    fn check_if_send<T: Send>() {}
610
611    /// [ProvenTransaction] being Sync is part of its public API and changing it is backwards
612    /// incompatible.
613    #[test]
614    fn test_proven_transaction_is_sync() {
615        check_if_sync::<ProvenTransaction>();
616    }
617
618    /// [ProvenTransaction] being Send is part of its public API and changing it is backwards
619    /// incompatible.
620    #[test]
621    fn test_proven_transaction_is_send() {
622        check_if_send::<ProvenTransaction>();
623    }
624
625    #[test]
626    fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
627        // A small account's delta does not exceed the limit.
628        let account = Account::builder([9; 32])
629            .account_type(AccountType::Public)
630            .with_component(NoopAuthComponent)
631            .with_component(AddComponent)
632            .build_existing()?;
633        let patch = AccountPatch::try_from(account.clone())?;
634        let patch_commitment = patch.to_commitment();
635
636        let details = AccountUpdateDetails::Public(patch);
637
638        TxAccountUpdate::new(
639            account.id(),
640            account.to_commitment(),
641            account.to_commitment(),
642            patch_commitment,
643            details,
644        )?;
645
646        Ok(())
647    }
648
649    #[test]
650    fn account_update_size_limit_exceeded() {
651        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
652        let mut map = BTreeMap::new();
653        // The number of entries in the map required to exceed the limit.
654        // We divide by each entry's size which consists of a key (digest) and a value (word), both
655        // 32 bytes in size.
656        let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
657        for _ in 0..required_entries {
658            map.insert(StorageMapKey::from_raw(rand_value()), rand_value::<Word>());
659        }
660        let storage_patch = StorageMapPatch::Update {
661            entries: StorageMapPatchEntries::from_raw(map),
662        };
663
664        // A patch that exceeds the limit returns an error.
665        let storage_patch =
666            AccountStoragePatch::from_iters([], [], [(StorageSlotName::mock(4), storage_patch)]);
667        let patch = AccountPatch::new(
668            account_id,
669            storage_patch,
670            AccountVaultPatch::default(),
671            None,
672            Some(Felt::from(2u32)),
673        )
674        .unwrap();
675        let details = AccountUpdateDetails::Public(patch);
676        let details_size = details.get_size_hint();
677
678        let err = TxAccountUpdate::new(
679            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
680            EMPTY_WORD,
681            EMPTY_WORD,
682            EMPTY_WORD,
683            details,
684        )
685        .unwrap_err();
686
687        assert!(
688            matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
689        );
690    }
691
692    /// Building a [`TxAccountUpdate`] for a public account fails if the account ID in the patch
693    /// does not match the account ID passed to the constructor.
694    #[test]
695    fn account_update_id_mismatch_between_account_id_and_patch() -> anyhow::Result<()> {
696        let patch_account = Account::builder([9; 32])
697            .account_type(AccountType::Public)
698            .with_component(NoopAuthComponent)
699            .with_component(AddComponent)
700            .build_existing()?;
701        let patch = AccountPatch::try_from(patch_account.clone())?;
702
703        let other_account_id =
704            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)?;
705        assert_ne!(patch_account.id(), other_account_id);
706
707        let err = TxAccountUpdate::new(
708            other_account_id,
709            patch_account.to_commitment(),
710            patch_account.to_commitment(),
711            Word::empty(),
712            AccountUpdateDetails::Public(patch),
713        )
714        .unwrap_err();
715
716        assert_matches!(
717            err,
718            ProvenTransactionError::AccountIdMismatch {
719                tx_account_id,
720                details_account_id,
721            } => {
722                assert_eq!(tx_account_id, other_account_id);
723                assert_eq!(details_account_id, patch_account.id());
724            }
725        );
726
727        Ok(())
728    }
729
730    #[test]
731    fn account_patch_commitment_mismatch() -> anyhow::Result<()> {
732        let account = Account::builder([9; 32])
733            .account_type(AccountType::Public)
734            .with_component(NoopAuthComponent)
735            .with_component(AddComponent)
736            .build_existing()?;
737        let patch = AccountPatch::try_from(account.clone())?;
738        let actual_patch_commitment = patch.to_commitment();
739        // EMPTY_WORD is all-zeros and differs from a real Rescue hash.
740        let wrong_commitment = EMPTY_WORD;
741        assert_ne!(wrong_commitment, actual_patch_commitment);
742        let err = TxAccountUpdate::new(
743            account.id(),
744            account.to_commitment(),
745            account.to_commitment(),
746            wrong_commitment,
747            AccountUpdateDetails::Public(patch),
748        )
749        .unwrap_err();
750        assert_matches!(
751            err,
752            ProvenTransactionError::AccountPatchCommitmentMismatch {
753                expected_patch_commitment,
754                actual_patch_commitment: returned_actual,
755            } if expected_patch_commitment == wrong_commitment
756                && returned_actual == actual_patch_commitment
757        );
758        Ok(())
759    }
760    #[test]
761    fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
762        let account_id =
763            AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
764        let initial_account_commitment =
765            [2; 32].try_into().expect("failed to create initial account commitment");
766        let final_account_commitment =
767            [3; 32].try_into().expect("failed to create final account commitment");
768        let account_patch_commitment =
769            [4; 32].try_into().expect("failed to create account patch commitment");
770        let ref_block_num = BlockNumber::from(1);
771        let ref_block_commitment = Word::empty();
772        let expiration_block_num = BlockNumber::from(2);
773        let proof = ExecutionProof::new_dummy();
774
775        let account_update = TxAccountUpdate::new(
776            account_id,
777            initial_account_commitment,
778            final_account_commitment,
779            account_patch_commitment,
780            AccountUpdateDetails::Private,
781        )
782        .context("failed to build account update")?;
783
784        let tx = ProvenTransaction::new(
785            account_update,
786            Vec::<InputNoteCommitment>::new(),
787            Vec::<OutputNote>::new(),
788            ref_block_num,
789            ref_block_commitment,
790            expiration_block_num,
791            proof,
792        )
793        .context("failed to build proven transaction")?;
794
795        let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
796
797        assert_eq!(tx, deserialized);
798
799        Ok(())
800    }
801}