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#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ProvenTransaction {
43 id: TransactionId,
45
46 account_update: TxAccountUpdate,
48
49 input_notes: InputNotes<InputNoteCommitment>,
51
52 output_notes: OutputNotes,
55
56 ref_block_num: BlockNumber,
58
59 ref_block_commitment: Word,
61
62 fee: FungibleAsset,
64
65 expiration_block_num: BlockNumber,
67
68 proof: ExecutionProof,
70}
71
72impl ProvenTransaction {
73 pub fn id(&self) -> TransactionId {
75 self.id
76 }
77
78 pub fn account_id(&self) -> AccountId {
80 self.account_update.account_id()
81 }
82
83 pub fn account_update(&self) -> &TxAccountUpdate {
85 &self.account_update
86 }
87
88 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
90 &self.input_notes
91 }
92
93 pub fn output_notes(&self) -> &OutputNotes {
95 &self.output_notes
96 }
97
98 pub fn proof(&self) -> &ExecutionProof {
100 &self.proof
101 }
102
103 pub fn ref_block_num(&self) -> BlockNumber {
105 self.ref_block_num
106 }
107
108 pub fn ref_block_commitment(&self) -> Word {
110 self.ref_block_commitment
111 }
112
113 pub fn fee(&self) -> FungibleAsset {
115 self.fee
116 }
117
118 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
120 self.input_notes.iter().filter_map(|note| note.header())
121 }
122
123 pub fn expiration_block_num(&self) -> BlockNumber {
125 self.expiration_block_num
126 }
127
128 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
132 self.input_notes.iter().map(InputNoteCommitment::nullifier)
133 }
134
135 fn validate(mut self) -> Result<Self, ProvenTransactionError> {
148 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 AccountUpdateDetails::Private => (),
161 AccountUpdateDetails::Delta(post_fee_account_delta) => {
162 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 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#[derive(Clone, Debug)]
245pub struct ProvenTransactionBuilder {
246 account_id: AccountId,
248
249 initial_account_commitment: Word,
251
252 final_account_commitment: Word,
254
255 account_delta_commitment: Word,
257
258 account_update_details: AccountUpdateDetails,
260
261 input_notes: Vec<InputNoteCommitment>,
263
264 output_notes: Vec<OutputNote>,
266
267 ref_block_num: BlockNumber,
269
270 ref_block_commitment: Word,
272
273 fee: FungibleAsset,
275
276 expiration_block_num: BlockNumber,
278
279 proof: ExecutionProof,
281}
282
283impl ProvenTransactionBuilder {
284 #[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 pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
321 self.account_update_details = details;
322 self
323 }
324
325 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct TxAccountUpdate {
411 account_id: AccountId,
413
414 init_state_commitment: Word,
418
419 final_state_commitment: Word,
421
422 account_delta_commitment: Word,
432
433 details: AccountUpdateDetails,
437}
438
439impl TxAccountUpdate {
440 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 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 pub fn account_id(&self) -> AccountId {
525 self.account_id
526 }
527
528 pub fn initial_state_commitment(&self) -> Word {
530 self.init_state_commitment
531 }
532
533 pub fn final_state_commitment(&self) -> Word {
535 self.final_state_commitment
536 }
537
538 pub fn account_delta_commitment(&self) -> Word {
540 self.account_delta_commitment
541 }
542
543 pub fn details(&self) -> &AccountUpdateDetails {
548 &self.details
549 }
550
551 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#[derive(Debug, Clone, PartialEq, Eq)]
595pub struct InputNoteCommitment {
596 nullifier: Nullifier,
597 header: Option<NoteHeader>,
598}
599
600impl InputNoteCommitment {
601 pub fn nullifier(&self) -> Nullifier {
603 self.nullifier
604 }
605
606 pub fn header(&self) -> Option<&NoteHeader> {
611 self.header.as_ref()
612 }
613
614 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(¬e)
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
661impl 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#[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 #[test]
730 fn test_proven_transaction_is_sync() {
731 check_if_sync::<ProvenTransaction>();
732 }
733
734 #[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 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 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 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}