1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use super::{InputNote, ToInputNoteCommitments};
5use crate::account::delta::AccountUpdateDetails;
6use crate::asset::FungibleAsset;
7use crate::block::BlockNumber;
8use crate::note::NoteHeader;
9use crate::transaction::{
10 AccountId,
11 InputNotes,
12 Nullifier,
13 OutputNote,
14 OutputNotes,
15 TransactionId,
16};
17use crate::utils::serde::{
18 ByteReader,
19 ByteWriter,
20 Deserializable,
21 DeserializationError,
22 Serializable,
23};
24use crate::vm::ExecutionProof;
25use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, ProvenTransactionError, Word};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ProvenTransaction {
41 id: TransactionId,
43
44 account_update: TxAccountUpdate,
46
47 input_notes: InputNotes<InputNoteCommitment>,
49
50 output_notes: OutputNotes,
53
54 ref_block_num: BlockNumber,
56
57 ref_block_commitment: Word,
59
60 fee: FungibleAsset,
62
63 expiration_block_num: BlockNumber,
65
66 proof: ExecutionProof,
68}
69
70impl ProvenTransaction {
71 pub fn id(&self) -> TransactionId {
73 self.id
74 }
75
76 pub fn account_id(&self) -> AccountId {
78 self.account_update.account_id()
79 }
80
81 pub fn account_update(&self) -> &TxAccountUpdate {
83 &self.account_update
84 }
85
86 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
88 &self.input_notes
89 }
90
91 pub fn output_notes(&self) -> &OutputNotes {
93 &self.output_notes
94 }
95
96 pub fn proof(&self) -> &ExecutionProof {
98 &self.proof
99 }
100
101 pub fn ref_block_num(&self) -> BlockNumber {
103 self.ref_block_num
104 }
105
106 pub fn ref_block_commitment(&self) -> Word {
108 self.ref_block_commitment
109 }
110
111 pub fn fee(&self) -> FungibleAsset {
113 self.fee
114 }
115
116 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
118 self.input_notes.iter().filter_map(|note| note.header())
119 }
120
121 pub fn expiration_block_num(&self) -> BlockNumber {
123 self.expiration_block_num
124 }
125
126 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
130 self.input_notes.iter().map(InputNoteCommitment::nullifier)
131 }
132
133 fn validate(self) -> Result<Self, ProvenTransactionError> {
157 if self.account_id().is_onchain() {
159 self.account_update.validate()?;
160
161 if self.account_update.initial_state_commitment()
164 == self.account_update.final_state_commitment()
165 && self.input_notes.commitment() == EMPTY_WORD
166 {
167 return Err(ProvenTransactionError::EmptyTransaction);
168 }
169
170 let is_new_account = self.account_update.initial_state_commitment() == Word::empty();
171 match self.account_update.details() {
172 AccountUpdateDetails::Private => {
173 return Err(ProvenTransactionError::OnChainAccountMissingDetails(
174 self.account_id(),
175 ));
176 },
177 AccountUpdateDetails::New(account) => {
178 if !is_new_account {
179 return Err(
180 ProvenTransactionError::ExistingOnChainAccountRequiresDeltaDetails(
181 self.account_id(),
182 ),
183 );
184 }
185 if account.id() != self.account_id() {
186 return Err(ProvenTransactionError::AccountIdMismatch {
187 tx_account_id: self.account_id(),
188 details_account_id: account.id(),
189 });
190 }
191 if account.commitment() != self.account_update.final_state_commitment() {
192 return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
193 tx_final_commitment: self.account_update.final_state_commitment(),
194 details_commitment: account.commitment(),
195 });
196 }
197 },
198 AccountUpdateDetails::Delta(_) => {
199 if is_new_account {
200 return Err(ProvenTransactionError::NewOnChainAccountRequiresFullDetails(
201 self.account_id(),
202 ));
203 }
204 },
205 }
206 } else if !self.account_update.is_private() {
207 return Err(ProvenTransactionError::PrivateAccountWithDetails(self.account_id()));
208 }
209
210 Ok(self)
211 }
212}
213
214impl Serializable for ProvenTransaction {
215 fn write_into<W: ByteWriter>(&self, target: &mut W) {
216 self.account_update.write_into(target);
217 self.input_notes.write_into(target);
218 self.output_notes.write_into(target);
219 self.ref_block_num.write_into(target);
220 self.ref_block_commitment.write_into(target);
221 self.fee.write_into(target);
222 self.expiration_block_num.write_into(target);
223 self.proof.write_into(target);
224 }
225}
226
227impl Deserializable for ProvenTransaction {
228 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
229 let account_update = TxAccountUpdate::read_from(source)?;
230
231 let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
232 let output_notes = OutputNotes::read_from(source)?;
233
234 let ref_block_num = BlockNumber::read_from(source)?;
235 let ref_block_commitment = Word::read_from(source)?;
236 let fee = FungibleAsset::read_from(source)?;
237 let expiration_block_num = BlockNumber::read_from(source)?;
238 let proof = ExecutionProof::read_from(source)?;
239
240 let id = TransactionId::new(
241 account_update.initial_state_commitment(),
242 account_update.final_state_commitment(),
243 input_notes.commitment(),
244 output_notes.commitment(),
245 );
246
247 let proven_transaction = Self {
248 id,
249 account_update,
250 input_notes,
251 output_notes,
252 ref_block_num,
253 ref_block_commitment,
254 fee,
255 expiration_block_num,
256 proof,
257 };
258
259 proven_transaction
260 .validate()
261 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
262 }
263}
264
265#[derive(Clone, Debug)]
270pub struct ProvenTransactionBuilder {
271 account_id: AccountId,
273
274 initial_account_commitment: Word,
276
277 final_account_commitment: Word,
279
280 account_delta_commitment: Word,
282
283 account_update_details: AccountUpdateDetails,
285
286 input_notes: Vec<InputNoteCommitment>,
288
289 output_notes: Vec<OutputNote>,
291
292 ref_block_num: BlockNumber,
294
295 ref_block_commitment: Word,
297
298 fee: FungibleAsset,
300
301 expiration_block_num: BlockNumber,
303
304 proof: ExecutionProof,
306}
307
308impl ProvenTransactionBuilder {
309 #[allow(clippy::too_many_arguments)]
314 pub fn new(
315 account_id: AccountId,
316 initial_account_commitment: Word,
317 final_account_commitment: Word,
318 account_delta_commitment: Word,
319 ref_block_num: BlockNumber,
320 ref_block_commitment: Word,
321 fee: FungibleAsset,
322 expiration_block_num: BlockNumber,
323 proof: ExecutionProof,
324 ) -> Self {
325 Self {
326 account_id,
327 initial_account_commitment,
328 final_account_commitment,
329 account_delta_commitment,
330 account_update_details: AccountUpdateDetails::Private,
331 input_notes: Vec::new(),
332 output_notes: Vec::new(),
333 ref_block_num,
334 ref_block_commitment,
335 fee,
336 expiration_block_num,
337 proof,
338 }
339 }
340
341 pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
346 self.account_update_details = details;
347 self
348 }
349
350 pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
352 where
353 I: IntoIterator<Item = T>,
354 T: Into<InputNoteCommitment>,
355 {
356 self.input_notes.extend(notes.into_iter().map(|note| note.into()));
357 self
358 }
359
360 pub fn add_output_notes<T>(mut self, notes: T) -> Self
362 where
363 T: IntoIterator<Item = OutputNote>,
364 {
365 self.output_notes.extend(notes);
366 self
367 }
368
369 pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
396 let input_notes =
397 InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
398 let output_notes = OutputNotes::new(self.output_notes)
399 .map_err(ProvenTransactionError::OutputNotesError)?;
400 let id = TransactionId::new(
401 self.initial_account_commitment,
402 self.final_account_commitment,
403 input_notes.commitment(),
404 output_notes.commitment(),
405 );
406 let account_update = TxAccountUpdate::new(
407 self.account_id,
408 self.initial_account_commitment,
409 self.final_account_commitment,
410 self.account_delta_commitment,
411 self.account_update_details,
412 );
413
414 let proven_transaction = ProvenTransaction {
415 id,
416 account_update,
417 input_notes,
418 output_notes,
419 ref_block_num: self.ref_block_num,
420 ref_block_commitment: self.ref_block_commitment,
421 fee: self.fee,
422 expiration_block_num: self.expiration_block_num,
423 proof: self.proof,
424 };
425
426 proven_transaction.validate()
427 }
428}
429
430#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct TxAccountUpdate {
436 account_id: AccountId,
438
439 init_state_commitment: Word,
443
444 final_state_commitment: Word,
446
447 account_delta_commitment: Word,
457
458 details: AccountUpdateDetails,
462}
463
464impl TxAccountUpdate {
465 pub const fn new(
467 account_id: AccountId,
468 init_state_commitment: Word,
469 final_state_commitment: Word,
470 account_delta_commitment: Word,
471 details: AccountUpdateDetails,
472 ) -> Self {
473 Self {
474 account_id,
475 init_state_commitment,
476 final_state_commitment,
477 account_delta_commitment,
478 details,
479 }
480 }
481
482 pub fn account_id(&self) -> AccountId {
484 self.account_id
485 }
486
487 pub fn initial_state_commitment(&self) -> Word {
489 self.init_state_commitment
490 }
491
492 pub fn final_state_commitment(&self) -> Word {
494 self.final_state_commitment
495 }
496
497 pub fn account_delta_commitment(&self) -> Word {
499 self.account_delta_commitment
500 }
501
502 pub fn details(&self) -> &AccountUpdateDetails {
507 &self.details
508 }
509
510 pub fn is_private(&self) -> bool {
512 self.details.is_private()
513 }
514
515 pub fn validate(&self) -> Result<(), ProvenTransactionError> {
519 let account_update_size = self.details().get_size_hint();
520 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
521 Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
522 account_id: self.account_id(),
523 update_size: account_update_size,
524 })
525 } else {
526 Ok(())
527 }
528 }
529}
530
531impl Serializable for TxAccountUpdate {
532 fn write_into<W: ByteWriter>(&self, target: &mut W) {
533 self.account_id.write_into(target);
534 self.init_state_commitment.write_into(target);
535 self.final_state_commitment.write_into(target);
536 self.account_delta_commitment.write_into(target);
537 self.details.write_into(target);
538 }
539}
540
541impl Deserializable for TxAccountUpdate {
542 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
543 Ok(Self {
544 account_id: AccountId::read_from(source)?,
545 init_state_commitment: Word::read_from(source)?,
546 final_state_commitment: Word::read_from(source)?,
547 account_delta_commitment: Word::read_from(source)?,
548 details: AccountUpdateDetails::read_from(source)?,
549 })
550 }
551}
552
553#[derive(Debug, Clone, PartialEq, Eq)]
562pub struct InputNoteCommitment {
563 nullifier: Nullifier,
564 header: Option<NoteHeader>,
565}
566
567impl InputNoteCommitment {
568 pub fn nullifier(&self) -> Nullifier {
570 self.nullifier
571 }
572
573 pub fn header(&self) -> Option<&NoteHeader> {
578 self.header.as_ref()
579 }
580
581 pub fn is_authenticated(&self) -> bool {
587 self.header.is_none()
588 }
589}
590
591impl From<InputNote> for InputNoteCommitment {
592 fn from(note: InputNote) -> Self {
593 Self::from(¬e)
594 }
595}
596
597impl From<&InputNote> for InputNoteCommitment {
598 fn from(note: &InputNote) -> Self {
599 match note {
600 InputNote::Authenticated { note, .. } => Self {
601 nullifier: note.nullifier(),
602 header: None,
603 },
604 InputNote::Unauthenticated { note } => Self {
605 nullifier: note.nullifier(),
606 header: Some(*note.header()),
607 },
608 }
609 }
610}
611
612impl From<Nullifier> for InputNoteCommitment {
613 fn from(nullifier: Nullifier) -> Self {
614 Self { nullifier, header: None }
615 }
616}
617
618impl ToInputNoteCommitments for InputNoteCommitment {
619 fn nullifier(&self) -> Nullifier {
620 self.nullifier
621 }
622
623 fn note_commitment(&self) -> Option<Word> {
624 self.header.map(|header| header.commitment())
625 }
626}
627
628impl Serializable for InputNoteCommitment {
632 fn write_into<W: ByteWriter>(&self, target: &mut W) {
633 self.nullifier.write_into(target);
634 self.header.write_into(target);
635 }
636}
637
638impl Deserializable for InputNoteCommitment {
639 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
640 let nullifier = Nullifier::read_from(source)?;
641 let header = <Option<NoteHeader>>::read_from(source)?;
642
643 Ok(Self { nullifier, header })
644 }
645}
646
647#[cfg(test)]
651mod tests {
652 use alloc::collections::BTreeMap;
653
654 use anyhow::Context;
655 use miden_core::utils::Deserializable;
656 use miden_verifier::ExecutionProof;
657 use winter_air::proof::Proof;
658 use winter_rand_utils::rand_value;
659
660 use super::ProvenTransaction;
661 use crate::account::delta::AccountUpdateDetails;
662 use crate::account::{
663 AccountDelta,
664 AccountId,
665 AccountIdVersion,
666 AccountStorageDelta,
667 AccountStorageMode,
668 AccountType,
669 AccountVaultDelta,
670 StorageMapDelta,
671 };
672 use crate::asset::FungibleAsset;
673 use crate::block::BlockNumber;
674 use crate::testing::account_id::{
675 ACCOUNT_ID_PRIVATE_SENDER,
676 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
677 };
678 use crate::transaction::{ProvenTransactionBuilder, TxAccountUpdate};
679 use crate::utils::Serializable;
680 use crate::{
681 ACCOUNT_UPDATE_MAX_SIZE,
682 EMPTY_WORD,
683 LexicographicWord,
684 ONE,
685 ProvenTransactionError,
686 Word,
687 };
688
689 fn check_if_sync<T: Sync>() {}
690 fn check_if_send<T: Send>() {}
691
692 #[test]
695 fn test_proven_transaction_is_sync() {
696 check_if_sync::<ProvenTransaction>();
697 }
698
699 #[test]
702 fn test_proven_transaction_is_send() {
703 check_if_send::<ProvenTransaction>();
704 }
705
706 #[test]
707 fn account_update_size_limit_not_exceeded() {
708 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
710 let storage_delta = AccountStorageDelta::from_iters(
711 [1, 2, 3, 4],
712 [(2, Word::from([1, 1, 1, 1u32])), (3, Word::from([1, 1, 0, 1u32]))],
713 [],
714 );
715 let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE)
716 .unwrap();
717 let details = AccountUpdateDetails::Delta(delta);
718 TxAccountUpdate::new(
719 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
720 EMPTY_WORD,
721 EMPTY_WORD,
722 EMPTY_WORD,
723 details,
724 )
725 .validate()
726 .unwrap();
727 }
728
729 #[test]
730 fn account_update_size_limit_exceeded() {
731 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
732 let mut map = BTreeMap::new();
733 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
737 for _ in 0..required_entries {
738 map.insert(LexicographicWord::new(rand_value::<Word>()), rand_value::<Word>());
739 }
740 let storage_delta = StorageMapDelta::new(map);
741
742 let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
744 let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE)
745 .unwrap();
746 let details = AccountUpdateDetails::Delta(delta);
747 let details_size = details.get_size_hint();
748
749 let err = TxAccountUpdate::new(
750 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
751 EMPTY_WORD,
752 EMPTY_WORD,
753 EMPTY_WORD,
754 details,
755 )
756 .validate()
757 .unwrap_err();
758
759 assert!(
760 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
761 );
762 }
763
764 #[test]
765 fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
766 let account_id = AccountId::dummy(
767 [1; 15],
768 AccountIdVersion::Version0,
769 AccountType::FungibleFaucet,
770 AccountStorageMode::Private,
771 );
772 let initial_account_commitment =
773 [2; 32].try_into().expect("failed to create initial account commitment");
774 let final_account_commitment =
775 [3; 32].try_into().expect("failed to create final account commitment");
776 let account_delta_commitment =
777 [4; 32].try_into().expect("failed to create account delta commitment");
778 let ref_block_num = BlockNumber::from(1);
779 let ref_block_commitment = Word::empty();
780 let expiration_block_num = BlockNumber::from(2);
781 let proof = ExecutionProof::new(Proof::new_dummy(), Default::default());
782
783 let tx = ProvenTransactionBuilder::new(
784 account_id,
785 initial_account_commitment,
786 final_account_commitment,
787 account_delta_commitment,
788 ref_block_num,
789 ref_block_commitment,
790 FungibleAsset::mock(42).unwrap_fungible(),
791 expiration_block_num,
792 proof,
793 )
794 .build()
795 .context("failed to build proven transaction")?;
796
797 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
798
799 assert_eq!(tx, deserialized);
800
801 Ok(())
802 }
803}