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