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