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#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ProvenTransaction {
44 id: TransactionId,
46
47 account_update: TxAccountUpdate,
49
50 input_notes: InputNotes<InputNoteCommitment>,
52
53 output_notes: OutputNotes,
56
57 ref_block_num: BlockNumber,
59
60 ref_block_commitment: Word,
62
63 fee: FungibleAsset,
65
66 expiration_block_num: BlockNumber,
68
69 proof: ExecutionProof,
71}
72
73impl ProvenTransaction {
74 pub fn id(&self) -> TransactionId {
76 self.id
77 }
78
79 pub fn account_id(&self) -> AccountId {
81 self.account_update.account_id()
82 }
83
84 pub fn account_update(&self) -> &TxAccountUpdate {
86 &self.account_update
87 }
88
89 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
91 &self.input_notes
92 }
93
94 pub fn output_notes(&self) -> &OutputNotes {
96 &self.output_notes
97 }
98
99 pub fn proof(&self) -> &ExecutionProof {
101 &self.proof
102 }
103
104 pub fn ref_block_num(&self) -> BlockNumber {
106 self.ref_block_num
107 }
108
109 pub fn ref_block_commitment(&self) -> Word {
111 self.ref_block_commitment
112 }
113
114 pub fn fee(&self) -> FungibleAsset {
116 self.fee
117 }
118
119 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
121 self.input_notes.iter().filter_map(|note| note.header())
122 }
123
124 pub fn expiration_block_num(&self) -> BlockNumber {
126 self.expiration_block_num
127 }
128
129 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
133 self.input_notes.iter().map(InputNoteCommitment::nullifier)
134 }
135
136 fn validate(mut self) -> Result<Self, ProvenTransactionError> {
149 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 AccountUpdateDetails::Private => (),
162 AccountUpdateDetails::Delta(post_fee_account_delta) => {
163 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 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#[derive(Clone, Debug)]
246pub struct ProvenTransactionBuilder {
247 account_id: AccountId,
249
250 initial_account_commitment: Word,
252
253 final_account_commitment: Word,
255
256 account_delta_commitment: Word,
258
259 account_update_details: AccountUpdateDetails,
261
262 input_notes: Vec<InputNoteCommitment>,
264
265 output_notes: Vec<OutputNote>,
267
268 ref_block_num: BlockNumber,
270
271 ref_block_commitment: Word,
273
274 fee: FungibleAsset,
276
277 expiration_block_num: BlockNumber,
279
280 proof: ExecutionProof,
282}
283
284impl ProvenTransactionBuilder {
285 #[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 pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
322 self.account_update_details = details;
323 self
324 }
325
326 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
411pub struct TxAccountUpdate {
412 account_id: AccountId,
414
415 init_state_commitment: Word,
419
420 final_state_commitment: Word,
422
423 account_delta_commitment: Word,
433
434 details: AccountUpdateDetails,
438}
439
440impl TxAccountUpdate {
441 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 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 pub fn account_id(&self) -> AccountId {
526 self.account_id
527 }
528
529 pub fn initial_state_commitment(&self) -> Word {
531 self.init_state_commitment
532 }
533
534 pub fn final_state_commitment(&self) -> Word {
536 self.final_state_commitment
537 }
538
539 pub fn account_delta_commitment(&self) -> Word {
541 self.account_delta_commitment
542 }
543
544 pub fn details(&self) -> &AccountUpdateDetails {
549 &self.details
550 }
551
552 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#[derive(Debug, Clone, PartialEq, Eq)]
596pub struct InputNoteCommitment {
597 nullifier: Nullifier,
598 header: Option<NoteHeader>,
599}
600
601impl InputNoteCommitment {
602 pub fn nullifier(&self) -> Nullifier {
604 self.nullifier
605 }
606
607 pub fn header(&self) -> Option<&NoteHeader> {
612 self.header.as_ref()
613 }
614
615 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(¬e)
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
662impl 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#[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 #[test]
726 fn test_proven_transaction_is_sync() {
727 check_if_sync::<ProvenTransaction>();
728 }
729
730 #[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 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 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 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}