Skip to main content

miden_protocol/transaction/
proven_tx.rs

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