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