miden_objects/transaction/
proven_tx.rs

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