miden_objects/transaction/
proven_tx.rs

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