miden_objects/transaction/
proven_tx.rs

1use alloc::{string::ToString, vec::Vec};
2
3use miden_verifier::ExecutionProof;
4
5use super::{InputNote, ToInputNoteCommitments};
6use crate::{
7    account::delta::AccountUpdateDetails,
8    block::BlockNumber,
9    note::NoteHeader,
10    transaction::{
11        AccountId, Digest, InputNotes, Nullifier, OutputNote, OutputNotes, TransactionId,
12    },
13    utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
14    ProvenTransactionError, ACCOUNT_UPDATE_MAX_SIZE,
15};
16
17// PROVEN TRANSACTION
18// ================================================================================================
19
20/// Result of executing and proving a transaction. Contains all the data required to verify that a
21/// transaction was executed correctly.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ProvenTransaction {
24    /// A unique identifier for the transaction, see [TransactionId] for additional details.
25    id: TransactionId,
26
27    /// Account update data.
28    account_update: TxAccountUpdate,
29
30    /// Committed details of all notes consumed by the transaction.
31    input_notes: InputNotes<InputNoteCommitment>,
32
33    /// Notes created by the transaction. For private notes, this will contain only note headers,
34    /// while for public notes this will also contain full note details.
35    output_notes: OutputNotes,
36
37    /// The block hash of the last known block at the time the transaction was executed.
38    block_ref: Digest,
39
40    /// The block number by which the transaction will expire, as defined by the executed scripts.
41    expiration_block_num: BlockNumber,
42
43    /// A STARK proof that attests to the correct execution of the transaction.
44    proof: ExecutionProof,
45}
46
47impl ProvenTransaction {
48    /// Returns unique identifier of this transaction.
49    pub fn id(&self) -> TransactionId {
50        self.id
51    }
52
53    /// Returns ID of the account against which this transaction was executed.
54    pub fn account_id(&self) -> AccountId {
55        self.account_update.account_id()
56    }
57
58    /// Returns the account update details.
59    pub fn account_update(&self) -> &TxAccountUpdate {
60        &self.account_update
61    }
62
63    /// Returns a reference to the notes consumed by the transaction.
64    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
65        &self.input_notes
66    }
67
68    /// Returns a reference to the notes produced by the transaction.
69    pub fn output_notes(&self) -> &OutputNotes {
70        &self.output_notes
71    }
72
73    /// Returns the proof of the transaction.
74    pub fn proof(&self) -> &ExecutionProof {
75        &self.proof
76    }
77
78    /// Returns the block reference the transaction was executed against.
79    pub fn block_ref(&self) -> Digest {
80        self.block_ref
81    }
82
83    /// Returns an iterator of the headers of unauthenticated input notes in this transaction.
84    pub fn get_unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
85        self.input_notes.iter().filter_map(|note| note.header())
86    }
87
88    /// Returns the block number at which the transaction will expire.
89    pub fn expiration_block_num(&self) -> BlockNumber {
90        self.expiration_block_num
91    }
92
93    /// Returns an iterator over the nullifiers of all input notes in this transaction.
94    ///
95    /// This includes both authenticated and unauthenticated notes.
96    pub fn get_nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
97        self.input_notes.iter().map(InputNoteCommitment::nullifier)
98    }
99
100    // HELPER METHODS
101    // --------------------------------------------------------------------------------------------
102
103    fn validate(self) -> Result<Self, ProvenTransactionError> {
104        if self.account_id().is_public() {
105            self.account_update.validate()?;
106
107            let is_new_account = self.account_update.init_state_hash() == Digest::default();
108            match self.account_update.details() {
109                AccountUpdateDetails::Private => {
110                    return Err(ProvenTransactionError::OnChainAccountMissingDetails(
111                        self.account_id(),
112                    ))
113                },
114                AccountUpdateDetails::New(ref account) => {
115                    if !is_new_account {
116                        return Err(
117                            ProvenTransactionError::ExistingOnChainAccountRequiresDeltaDetails(
118                                self.account_id(),
119                            ),
120                        );
121                    }
122                    if account.id() != self.account_id() {
123                        return Err(ProvenTransactionError::AccountIdMismatch {
124                            tx_account_id: self.account_id(),
125                            details_account_id: account.id(),
126                        });
127                    }
128                    if account.hash() != self.account_update.final_state_hash() {
129                        return Err(ProvenTransactionError::AccountFinalHashMismatch {
130                            tx_final_hash: self.account_update.final_state_hash(),
131                            details_hash: account.hash(),
132                        });
133                    }
134                },
135                AccountUpdateDetails::Delta(_) => {
136                    if is_new_account {
137                        return Err(ProvenTransactionError::NewOnChainAccountRequiresFullDetails(
138                            self.account_id(),
139                        ));
140                    }
141                },
142            }
143        } else if !self.account_update.is_private() {
144            return Err(ProvenTransactionError::OffChainAccountWithDetails(self.account_id()));
145        }
146
147        Ok(self)
148    }
149}
150
151impl Serializable for ProvenTransaction {
152    fn write_into<W: ByteWriter>(&self, target: &mut W) {
153        self.account_update.write_into(target);
154        self.input_notes.write_into(target);
155        self.output_notes.write_into(target);
156        self.block_ref.write_into(target);
157        self.expiration_block_num.write_into(target);
158        self.proof.write_into(target);
159    }
160}
161
162impl Deserializable for ProvenTransaction {
163    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
164        let account_update = TxAccountUpdate::read_from(source)?;
165
166        let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
167        let output_notes = OutputNotes::read_from(source)?;
168
169        let block_ref = Digest::read_from(source)?;
170        let expiration_block_num = BlockNumber::read_from(source)?;
171        let proof = ExecutionProof::read_from(source)?;
172
173        let id = TransactionId::new(
174            account_update.init_state_hash(),
175            account_update.final_state_hash(),
176            input_notes.commitment(),
177            output_notes.commitment(),
178        );
179
180        let proven_transaction = Self {
181            id,
182            account_update,
183            input_notes,
184            output_notes,
185            block_ref,
186            expiration_block_num,
187            proof,
188        };
189
190        proven_transaction
191            .validate()
192            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
193    }
194}
195
196// PROVEN TRANSACTION BUILDER
197// ================================================================================================
198
199/// Builder for a proven transaction.
200#[derive(Clone, Debug)]
201pub struct ProvenTransactionBuilder {
202    /// ID of the account that the transaction was executed against.
203    account_id: AccountId,
204
205    /// The hash of the account before the transaction was executed.
206    initial_account_hash: Digest,
207
208    /// The hash of the account after the transaction was executed.
209    final_account_hash: Digest,
210
211    /// State changes to the account due to the transaction.
212    account_update_details: AccountUpdateDetails,
213
214    /// List of [InputNoteCommitment]s of all consumed notes by the transaction.
215    input_notes: Vec<InputNoteCommitment>,
216
217    /// List of [OutputNote]s of all notes created by the transaction.
218    output_notes: Vec<OutputNote>,
219
220    /// Block [Digest] of the transaction's reference block.
221    block_ref: Digest,
222
223    /// The block number by which the transaction will expire, as defined by the executed scripts.
224    expiration_block_num: BlockNumber,
225
226    /// A STARK proof that attests to the correct execution of the transaction.
227    proof: ExecutionProof,
228}
229
230impl ProvenTransactionBuilder {
231    // CONSTRUCTOR
232    // --------------------------------------------------------------------------------------------
233
234    /// Returns a [ProvenTransactionBuilder] used to build a [ProvenTransaction].
235    pub fn new(
236        account_id: AccountId,
237        initial_account_hash: Digest,
238        final_account_hash: Digest,
239        block_ref: Digest,
240        expiration_block_num: BlockNumber,
241        proof: ExecutionProof,
242    ) -> Self {
243        Self {
244            account_id,
245            initial_account_hash,
246            final_account_hash,
247            account_update_details: AccountUpdateDetails::Private,
248            input_notes: Vec::new(),
249            output_notes: Vec::new(),
250            block_ref,
251            expiration_block_num,
252            proof,
253        }
254    }
255
256    // PUBLIC ACCESSORS
257    // --------------------------------------------------------------------------------------------
258
259    /// Sets the account's update details.
260    pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
261        self.account_update_details = details;
262        self
263    }
264
265    /// Add notes consumed by the transaction.
266    pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
267    where
268        I: IntoIterator<Item = T>,
269        T: Into<InputNoteCommitment>,
270    {
271        self.input_notes.extend(notes.into_iter().map(|note| note.into()));
272        self
273    }
274
275    /// Add notes produced by the transaction.
276    pub fn add_output_notes<T>(mut self, notes: T) -> Self
277    where
278        T: IntoIterator<Item = OutputNote>,
279    {
280        self.output_notes.extend(notes);
281        self
282    }
283
284    /// Builds the [ProvenTransaction].
285    ///
286    /// # Errors
287    ///
288    /// An error will be returned if an on-chain account is used without provided on-chain detail.
289    /// Or if the account details, i.e. account ID and final hash, don't match the transaction.
290    pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
291        let input_notes =
292            InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
293        let output_notes = OutputNotes::new(self.output_notes)
294            .map_err(ProvenTransactionError::OutputNotesError)?;
295        let id = TransactionId::new(
296            self.initial_account_hash,
297            self.final_account_hash,
298            input_notes.commitment(),
299            output_notes.commitment(),
300        );
301        let account_update = TxAccountUpdate::new(
302            self.account_id,
303            self.initial_account_hash,
304            self.final_account_hash,
305            self.account_update_details,
306        );
307
308        let proven_transaction = ProvenTransaction {
309            id,
310            account_update,
311            input_notes,
312            output_notes,
313            block_ref: self.block_ref,
314            expiration_block_num: self.expiration_block_num,
315            proof: self.proof,
316        };
317
318        proven_transaction.validate()
319    }
320}
321
322// TRANSACTION ACCOUNT UPDATE
323// ================================================================================================
324
325/// Describes the changes made to the account state resulting from a transaction execution.
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct TxAccountUpdate {
328    /// ID of the account updated by a transaction.
329    account_id: AccountId,
330
331    /// The hash of the account before a transaction was executed.
332    ///
333    /// Set to `Digest::default()` for new accounts.
334    init_state_hash: Digest,
335
336    /// The hash of the account state after a transaction was executed.
337    final_state_hash: Digest,
338
339    /// A set of changes which can be applied the account's state prior to the transaction to
340    /// get the account state after the transaction. For private accounts this is set to
341    /// [AccountUpdateDetails::Private].
342    details: AccountUpdateDetails,
343}
344
345impl TxAccountUpdate {
346    /// Returns a new [TxAccountUpdate] instantiated from the specified components.
347    pub const fn new(
348        account_id: AccountId,
349        init_state_hash: Digest,
350        final_state_hash: Digest,
351        details: AccountUpdateDetails,
352    ) -> Self {
353        Self {
354            account_id,
355            init_state_hash,
356            final_state_hash,
357            details,
358        }
359    }
360
361    /// Returns the ID of the updated account.
362    pub fn account_id(&self) -> AccountId {
363        self.account_id
364    }
365
366    /// Returns the hash of the account's initial state.
367    pub fn init_state_hash(&self) -> Digest {
368        self.init_state_hash
369    }
370
371    /// Returns the hash of the account's after a transaction was executed.
372    pub fn final_state_hash(&self) -> Digest {
373        self.final_state_hash
374    }
375
376    /// Returns the description of the updates for public accounts.
377    ///
378    /// These descriptions can be used to build the new account state from the previous account
379    /// state.
380    pub fn details(&self) -> &AccountUpdateDetails {
381        &self.details
382    }
383
384    /// Returns `true` if the account update details are for a private account.
385    pub fn is_private(&self) -> bool {
386        self.details.is_private()
387    }
388
389    /// Validates the following properties of the account update:
390    ///
391    /// - The size of the serialized account update does not exceed [`ACCOUNT_UPDATE_MAX_SIZE`].
392    pub fn validate(&self) -> Result<(), ProvenTransactionError> {
393        let account_update_size = self.details().get_size_hint();
394        if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
395            Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
396                account_id: self.account_id(),
397                update_size: account_update_size,
398            })
399        } else {
400            Ok(())
401        }
402    }
403}
404
405impl Serializable for TxAccountUpdate {
406    fn write_into<W: ByteWriter>(&self, target: &mut W) {
407        self.account_id.write_into(target);
408        self.init_state_hash.write_into(target);
409        self.final_state_hash.write_into(target);
410        self.details.write_into(target);
411    }
412}
413
414impl Deserializable for TxAccountUpdate {
415    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
416        Ok(Self {
417            account_id: AccountId::read_from(source)?,
418            init_state_hash: Digest::read_from(source)?,
419            final_state_hash: Digest::read_from(source)?,
420            details: AccountUpdateDetails::read_from(source)?,
421        })
422    }
423}
424
425// INPUT NOTE COMMITMENT
426// ================================================================================================
427
428/// The commitment to an input note.
429///
430/// For notes authenticated by the transaction kernel, the commitment consists only of the note's
431/// nullifier. For notes whose authentication is delayed to batch/block kernels, the commitment
432/// also includes full note header (i.e., note ID and metadata).
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct InputNoteCommitment {
435    nullifier: Nullifier,
436    header: Option<NoteHeader>,
437}
438
439impl InputNoteCommitment {
440    /// Returns the nullifier of the input note committed to by this commitment.
441    pub fn nullifier(&self) -> Nullifier {
442        self.nullifier
443    }
444
445    /// Returns the header of the input committed to by this commitment.
446    ///
447    /// Note headers are present only for notes whose presence in the change has not yet been
448    /// authenticated.
449    pub fn header(&self) -> Option<&NoteHeader> {
450        self.header.as_ref()
451    }
452
453    /// Returns true if this commitment is for a note whose presence in the chain has been
454    /// authenticated.
455    ///
456    /// Authenticated notes are represented solely by their nullifiers and are missing the note
457    /// header.
458    pub fn is_authenticated(&self) -> bool {
459        self.header.is_none()
460    }
461}
462
463impl From<InputNote> for InputNoteCommitment {
464    fn from(note: InputNote) -> Self {
465        Self::from(&note)
466    }
467}
468
469impl From<&InputNote> for InputNoteCommitment {
470    fn from(note: &InputNote) -> Self {
471        match note {
472            InputNote::Authenticated { note, .. } => Self {
473                nullifier: note.nullifier(),
474                header: None,
475            },
476            InputNote::Unauthenticated { note } => Self {
477                nullifier: note.nullifier(),
478                header: Some(*note.header()),
479            },
480        }
481    }
482}
483
484impl From<Nullifier> for InputNoteCommitment {
485    fn from(nullifier: Nullifier) -> Self {
486        Self { nullifier, header: None }
487    }
488}
489
490impl ToInputNoteCommitments for InputNoteCommitment {
491    fn nullifier(&self) -> Nullifier {
492        self.nullifier
493    }
494
495    fn note_hash(&self) -> Option<Digest> {
496        self.header.map(|header| header.hash())
497    }
498}
499
500// SERIALIZATION
501// ------------------------------------------------------------------------------------------------
502
503impl Serializable for InputNoteCommitment {
504    fn write_into<W: ByteWriter>(&self, target: &mut W) {
505        self.nullifier.write_into(target);
506        self.header.write_into(target);
507    }
508}
509
510impl Deserializable for InputNoteCommitment {
511    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
512        let nullifier = Nullifier::read_from(source)?;
513        let header = <Option<NoteHeader>>::read_from(source)?;
514
515        Ok(Self { nullifier, header })
516    }
517}
518
519// TESTS
520// ================================================================================================
521
522#[cfg(test)]
523mod tests {
524    use alloc::collections::BTreeMap;
525
526    use winter_rand_utils::rand_array;
527
528    use super::ProvenTransaction;
529    use crate::{
530        account::{
531            delta::AccountUpdateDetails, AccountDelta, AccountId, AccountStorageDelta,
532            AccountVaultDelta, StorageMapDelta,
533        },
534        testing::account_id::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN,
535        transaction::TxAccountUpdate,
536        utils::Serializable,
537        Digest, ProvenTransactionError, ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, ONE, ZERO,
538    };
539
540    fn check_if_sync<T: Sync>() {}
541    fn check_if_send<T: Send>() {}
542
543    /// [ProvenTransaction] being Sync is part of its public API and changing it is backwards
544    /// incompatible.
545    #[test]
546    fn test_proven_transaction_is_sync() {
547        check_if_sync::<ProvenTransaction>();
548    }
549
550    /// [ProvenTransaction] being Send is part of its public API and changing it is backwards
551    /// incompatible.
552    #[test]
553    fn test_proven_transaction_is_send() {
554        check_if_send::<ProvenTransaction>();
555    }
556
557    #[test]
558    fn account_update_size_limit_not_exceeded() {
559        // A small delta does not exceed the limit.
560        let storage_delta = AccountStorageDelta::from_iters(
561            [1, 2, 3, 4],
562            [(2, [ONE, ONE, ONE, ONE]), (3, [ONE, ONE, ZERO, ONE])],
563            [],
564        );
565        let delta =
566            AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
567        let details = AccountUpdateDetails::Delta(delta);
568        TxAccountUpdate::new(
569            AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(),
570            Digest::new(EMPTY_WORD),
571            Digest::new(EMPTY_WORD),
572            details,
573        )
574        .validate()
575        .unwrap();
576    }
577
578    #[test]
579    fn account_update_size_limit_exceeded() {
580        let mut map = BTreeMap::new();
581        // The number of entries in the map required to exceed the limit.
582        // We divide by each entry's size which consists of a key (digest) and a value (word), both
583        // 32 bytes in size.
584        let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
585        for _ in 0..required_entries {
586            map.insert(Digest::new(rand_array()), rand_array());
587        }
588        let storage_delta = StorageMapDelta::new(map);
589
590        // A delta that exceeds the limit returns an error.
591        let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
592        let delta =
593            AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
594        let details = AccountUpdateDetails::Delta(delta);
595        let details_size = details.get_size_hint();
596
597        let err = TxAccountUpdate::new(
598            AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(),
599            Digest::new(EMPTY_WORD),
600            Digest::new(EMPTY_WORD),
601            details,
602        )
603        .validate()
604        .unwrap_err();
605
606        assert!(
607            matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
608        );
609    }
610}