1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use super::{InputNote, ToInputNoteCommitments};
5use crate::account::{Account, AccountUpdateDetails};
6use crate::block::BlockNumber;
7use crate::errors::ProvenTransactionError;
8use crate::note::{NoteHeader, NoteId};
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, 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 expiration_block_num: BlockNumber,
62
63 proof: ExecutionProof,
65}
66
67impl ProvenTransaction {
68 pub fn new(
88 account_update: TxAccountUpdate,
89 input_notes: impl IntoIterator<Item = impl Into<InputNoteCommitment>>,
90 output_notes: impl IntoIterator<Item = impl Into<OutputNote>>,
91 ref_block_num: BlockNumber,
92 ref_block_commitment: Word,
93 expiration_block_num: BlockNumber,
94 proof: ExecutionProof,
95 ) -> Result<Self, ProvenTransactionError> {
96 let input_notes: Vec<InputNoteCommitment> =
97 input_notes.into_iter().map(Into::into).collect();
98 let output_notes: Vec<OutputNote> = output_notes.into_iter().map(Into::into).collect();
99
100 let input_notes =
101 InputNotes::new(input_notes).map_err(ProvenTransactionError::InputNotesError)?;
102 let output_notes =
103 OutputNotes::new(output_notes).map_err(ProvenTransactionError::OutputNotesError)?;
104
105 Self::from_parts(
106 account_update,
107 input_notes,
108 output_notes,
109 ref_block_num,
110 ref_block_commitment,
111 expiration_block_num,
112 proof,
113 )
114 }
115
116 pub fn id(&self) -> TransactionId {
121 self.id
122 }
123
124 pub fn account_id(&self) -> AccountId {
126 self.account_update.account_id()
127 }
128
129 pub fn account_update(&self) -> &TxAccountUpdate {
131 &self.account_update
132 }
133
134 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
136 &self.input_notes
137 }
138
139 pub fn output_notes(&self) -> &OutputNotes {
141 &self.output_notes
142 }
143
144 pub fn proof(&self) -> &ExecutionProof {
146 &self.proof
147 }
148
149 pub fn ref_block_num(&self) -> BlockNumber {
151 self.ref_block_num
152 }
153
154 pub fn ref_block_commitment(&self) -> Word {
156 self.ref_block_commitment
157 }
158
159 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
161 self.input_notes.iter().filter_map(|note| note.header())
162 }
163
164 pub fn expiration_block_num(&self) -> BlockNumber {
166 self.expiration_block_num
167 }
168
169 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
173 self.input_notes.iter().map(InputNoteCommitment::nullifier)
174 }
175
176 fn from_parts(
194 account_update: TxAccountUpdate,
195 input_notes: InputNotes<InputNoteCommitment>,
196 output_notes: OutputNotes,
197 ref_block_num: BlockNumber,
198 ref_block_commitment: Word,
199 expiration_block_num: BlockNumber,
200 proof: ExecutionProof,
201 ) -> Result<Self, ProvenTransactionError> {
202 if account_update.initial_state_commitment() == account_update.final_state_commitment()
205 && input_notes.commitment().is_empty()
206 {
207 return Err(ProvenTransactionError::EmptyTransaction);
208 }
209
210 for input_note in input_notes.iter().filter_map(InputNoteCommitment::header) {
215 if output_notes.iter().any(|output_note| output_note.id() == input_note.id()) {
216 return Err(ProvenTransactionError::NoteCreatedAndConsumed(input_note.id()));
217 }
218 }
219
220 let id = TransactionId::new(
221 account_update.initial_state_commitment(),
222 account_update.final_state_commitment(),
223 input_notes.commitment(),
224 output_notes.commitment(),
225 );
226
227 Ok(Self {
228 id,
229 account_update,
230 input_notes,
231 output_notes,
232 ref_block_num,
233 ref_block_commitment,
234 expiration_block_num,
235 proof,
236 })
237 }
238}
239
240impl Serializable for ProvenTransaction {
241 fn write_into<W: ByteWriter>(&self, target: &mut W) {
242 self.account_update.write_into(target);
243 self.input_notes.write_into(target);
244 self.output_notes.write_into(target);
245 self.ref_block_num.write_into(target);
246 self.ref_block_commitment.write_into(target);
247 self.expiration_block_num.write_into(target);
248 self.proof.write_into(target);
249 }
250}
251
252impl Deserializable for ProvenTransaction {
253 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
254 let account_update = TxAccountUpdate::read_from(source)?;
255
256 let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
257 let output_notes = OutputNotes::read_from(source)?;
258
259 let ref_block_num = BlockNumber::read_from(source)?;
260 let ref_block_commitment = Word::read_from(source)?;
261 let expiration_block_num = BlockNumber::read_from(source)?;
262 let proof = ExecutionProof::read_from(source)?;
263
264 Self::from_parts(
265 account_update,
266 input_notes,
267 output_notes,
268 ref_block_num,
269 ref_block_commitment,
270 expiration_block_num,
271 proof,
272 )
273 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
274 }
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct TxAccountUpdate {
283 account_id: AccountId,
285
286 init_state_commitment: Word,
290
291 final_state_commitment: Word,
293
294 account_patch_commitment: Word,
302
303 details: AccountUpdateDetails,
307}
308
309impl TxAccountUpdate {
310 pub fn new(
325 account_id: AccountId,
326 init_state_commitment: Word,
327 final_state_commitment: Word,
328 account_patch_commitment: Word,
329 details: AccountUpdateDetails,
330 ) -> Result<Self, ProvenTransactionError> {
331 let account_update = Self {
332 account_id,
333 init_state_commitment,
334 final_state_commitment,
335 account_patch_commitment,
336 details,
337 };
338
339 let account_update_size = account_update.details.get_size_hint();
340 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
341 return Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
342 account_id,
343 update_size: account_update_size,
344 });
345 }
346
347 if account_id.is_private() {
348 if account_update.details.is_private() {
349 return Ok(account_update);
350 } else {
351 return Err(ProvenTransactionError::PrivateAccountWithDetails(account_id));
352 }
353 }
354
355 match account_update.details() {
356 AccountUpdateDetails::Private => {
357 return Err(ProvenTransactionError::PublicStateAccountMissingDetails(
358 account_update.account_id(),
359 ));
360 },
361 AccountUpdateDetails::Public(patch) => {
362 if patch.id() != account_id {
363 return Err(ProvenTransactionError::AccountIdMismatch {
364 tx_account_id: account_id,
365 details_account_id: patch.id(),
366 });
367 }
368
369 let actual_patch_commitment = patch.to_commitment();
370 if account_patch_commitment != actual_patch_commitment {
371 return Err(ProvenTransactionError::AccountPatchCommitmentMismatch {
372 expected_patch_commitment: account_patch_commitment,
373 actual_patch_commitment,
374 });
375 }
376
377 let is_new_account = account_update.initial_state_commitment().is_empty();
378 if is_new_account {
379 let account = Account::try_from(patch).map_err(|err| {
382 ProvenTransactionError::NewPublicStateAccountRequiresFullStatePatch {
383 id: patch.id(),
384 source: err,
385 }
386 })?;
387
388 if account.to_commitment() != account_update.final_state_commitment {
389 return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
390 tx_final_commitment: account_update.final_state_commitment,
391 details_commitment: account.to_commitment(),
392 });
393 }
394 }
395 },
396 }
397
398 Ok(account_update)
399 }
400
401 pub fn account_id(&self) -> AccountId {
403 self.account_id
404 }
405
406 pub fn initial_state_commitment(&self) -> Word {
408 self.init_state_commitment
409 }
410
411 pub fn final_state_commitment(&self) -> Word {
413 self.final_state_commitment
414 }
415
416 pub fn account_patch_commitment(&self) -> Word {
419 self.account_patch_commitment
420 }
421
422 pub fn details(&self) -> &AccountUpdateDetails {
427 &self.details
428 }
429
430 pub fn is_private(&self) -> bool {
432 self.details.is_private()
433 }
434}
435
436impl Serializable for TxAccountUpdate {
437 fn write_into<W: ByteWriter>(&self, target: &mut W) {
438 self.account_id.write_into(target);
439 self.init_state_commitment.write_into(target);
440 self.final_state_commitment.write_into(target);
441 self.account_patch_commitment.write_into(target);
442 self.details.write_into(target);
443 }
444}
445
446impl Deserializable for TxAccountUpdate {
447 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
448 let account_id = AccountId::read_from(source)?;
449 let init_state_commitment = Word::read_from(source)?;
450 let final_state_commitment = Word::read_from(source)?;
451 let account_patch_commitment = Word::read_from(source)?;
452 let details = AccountUpdateDetails::read_from(source)?;
453
454 Self::new(
455 account_id,
456 init_state_commitment,
457 final_state_commitment,
458 account_patch_commitment,
459 details,
460 )
461 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
462 }
463}
464
465#[derive(Debug, Clone, PartialEq, Eq)]
474pub struct InputNoteCommitment {
475 nullifier: Nullifier,
476 header: Option<NoteHeader>,
477}
478
479impl InputNoteCommitment {
480 pub fn from_parts_unchecked(nullifier: Nullifier, header: Option<NoteHeader>) -> Self {
487 Self { nullifier, header }
488 }
489
490 pub fn nullifier(&self) -> Nullifier {
492 self.nullifier
493 }
494
495 pub fn header(&self) -> Option<&NoteHeader> {
500 self.header.as_ref()
501 }
502
503 pub fn is_authenticated(&self) -> bool {
509 self.header.is_none()
510 }
511}
512
513impl From<InputNote> for InputNoteCommitment {
514 fn from(note: InputNote) -> Self {
515 Self::from(¬e)
516 }
517}
518
519impl From<&InputNote> for InputNoteCommitment {
520 fn from(note: &InputNote) -> Self {
521 match note {
522 InputNote::Authenticated { note, .. } => Self {
523 nullifier: note.nullifier(),
524 header: None,
525 },
526 InputNote::Unauthenticated { note } => Self {
527 nullifier: note.nullifier(),
528 header: Some(*note.header()),
529 },
530 }
531 }
532}
533
534impl From<Nullifier> for InputNoteCommitment {
535 fn from(nullifier: Nullifier) -> Self {
536 Self { nullifier, header: None }
537 }
538}
539
540impl ToInputNoteCommitments for InputNoteCommitment {
541 fn nullifier(&self) -> Nullifier {
542 self.nullifier
543 }
544
545 fn note_id(&self) -> Option<NoteId> {
546 self.header.as_ref().map(NoteHeader::id)
547 }
548}
549
550impl Serializable for InputNoteCommitment {
554 fn write_into<W: ByteWriter>(&self, target: &mut W) {
555 self.nullifier.write_into(target);
556 self.header.write_into(target);
557 }
558}
559
560impl Deserializable for InputNoteCommitment {
561 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
562 let nullifier = Nullifier::read_from(source)?;
563 let header = <Option<NoteHeader>>::read_from(source)?;
564
565 Ok(Self::from_parts_unchecked(nullifier, header))
566 }
567}
568
569#[cfg(test)]
573mod tests {
574 use alloc::collections::BTreeMap;
575 use alloc::vec::Vec;
576
577 use anyhow::Context;
578 use assert_matches::assert_matches;
579 use miden_crypto::rand::test_utils::rand_value;
580 use miden_verifier::ExecutionProof;
581
582 use super::ProvenTransaction;
583 use crate::account::{
584 Account,
585 AccountId,
586 AccountPatch,
587 AccountStoragePatch,
588 AccountType,
589 AccountUpdateDetails,
590 AccountVaultPatch,
591 StorageMapKey,
592 StorageMapPatch,
593 StorageMapPatchEntries,
594 StorageSlotName,
595 };
596 use crate::block::BlockNumber;
597 use crate::errors::ProvenTransactionError;
598 use crate::testing::account_id::{
599 ACCOUNT_ID_PRIVATE_SENDER,
600 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
601 };
602 use crate::testing::add_component::AddComponent;
603 use crate::testing::noop_auth_component::NoopAuthComponent;
604 use crate::transaction::{InputNoteCommitment, OutputNote, TxAccountUpdate};
605 use crate::utils::serde::{Deserializable, Serializable};
606 use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, Felt, Word};
607
608 fn check_if_sync<T: Sync>() {}
609 fn check_if_send<T: Send>() {}
610
611 #[test]
614 fn test_proven_transaction_is_sync() {
615 check_if_sync::<ProvenTransaction>();
616 }
617
618 #[test]
621 fn test_proven_transaction_is_send() {
622 check_if_send::<ProvenTransaction>();
623 }
624
625 #[test]
626 fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
627 let account = Account::builder([9; 32])
629 .account_type(AccountType::Public)
630 .with_component(NoopAuthComponent)
631 .with_component(AddComponent)
632 .build_existing()?;
633 let patch = AccountPatch::try_from(account.clone())?;
634 let patch_commitment = patch.to_commitment();
635
636 let details = AccountUpdateDetails::Public(patch);
637
638 TxAccountUpdate::new(
639 account.id(),
640 account.to_commitment(),
641 account.to_commitment(),
642 patch_commitment,
643 details,
644 )?;
645
646 Ok(())
647 }
648
649 #[test]
650 fn account_update_size_limit_exceeded() {
651 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
652 let mut map = BTreeMap::new();
653 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
657 for _ in 0..required_entries {
658 map.insert(StorageMapKey::from_raw(rand_value()), rand_value::<Word>());
659 }
660 let storage_patch = StorageMapPatch::Update {
661 entries: StorageMapPatchEntries::from_raw(map),
662 };
663
664 let storage_patch =
666 AccountStoragePatch::from_iters([], [], [(StorageSlotName::mock(4), storage_patch)]);
667 let patch = AccountPatch::new(
668 account_id,
669 storage_patch,
670 AccountVaultPatch::default(),
671 None,
672 Some(Felt::from(2u32)),
673 )
674 .unwrap();
675 let details = AccountUpdateDetails::Public(patch);
676 let details_size = details.get_size_hint();
677
678 let err = TxAccountUpdate::new(
679 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
680 EMPTY_WORD,
681 EMPTY_WORD,
682 EMPTY_WORD,
683 details,
684 )
685 .unwrap_err();
686
687 assert!(
688 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
689 );
690 }
691
692 #[test]
695 fn account_update_id_mismatch_between_account_id_and_patch() -> anyhow::Result<()> {
696 let patch_account = Account::builder([9; 32])
697 .account_type(AccountType::Public)
698 .with_component(NoopAuthComponent)
699 .with_component(AddComponent)
700 .build_existing()?;
701 let patch = AccountPatch::try_from(patch_account.clone())?;
702
703 let other_account_id =
704 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)?;
705 assert_ne!(patch_account.id(), other_account_id);
706
707 let err = TxAccountUpdate::new(
708 other_account_id,
709 patch_account.to_commitment(),
710 patch_account.to_commitment(),
711 Word::empty(),
712 AccountUpdateDetails::Public(patch),
713 )
714 .unwrap_err();
715
716 assert_matches!(
717 err,
718 ProvenTransactionError::AccountIdMismatch {
719 tx_account_id,
720 details_account_id,
721 } => {
722 assert_eq!(tx_account_id, other_account_id);
723 assert_eq!(details_account_id, patch_account.id());
724 }
725 );
726
727 Ok(())
728 }
729
730 #[test]
731 fn account_patch_commitment_mismatch() -> anyhow::Result<()> {
732 let account = Account::builder([9; 32])
733 .account_type(AccountType::Public)
734 .with_component(NoopAuthComponent)
735 .with_component(AddComponent)
736 .build_existing()?;
737 let patch = AccountPatch::try_from(account.clone())?;
738 let actual_patch_commitment = patch.to_commitment();
739 let wrong_commitment = EMPTY_WORD;
741 assert_ne!(wrong_commitment, actual_patch_commitment);
742 let err = TxAccountUpdate::new(
743 account.id(),
744 account.to_commitment(),
745 account.to_commitment(),
746 wrong_commitment,
747 AccountUpdateDetails::Public(patch),
748 )
749 .unwrap_err();
750 assert_matches!(
751 err,
752 ProvenTransactionError::AccountPatchCommitmentMismatch {
753 expected_patch_commitment,
754 actual_patch_commitment: returned_actual,
755 } if expected_patch_commitment == wrong_commitment
756 && returned_actual == actual_patch_commitment
757 );
758 Ok(())
759 }
760 #[test]
761 fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
762 let account_id =
763 AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
764 let initial_account_commitment =
765 [2; 32].try_into().expect("failed to create initial account commitment");
766 let final_account_commitment =
767 [3; 32].try_into().expect("failed to create final account commitment");
768 let account_patch_commitment =
769 [4; 32].try_into().expect("failed to create account patch commitment");
770 let ref_block_num = BlockNumber::from(1);
771 let ref_block_commitment = Word::empty();
772 let expiration_block_num = BlockNumber::from(2);
773 let proof = ExecutionProof::new_dummy();
774
775 let account_update = TxAccountUpdate::new(
776 account_id,
777 initial_account_commitment,
778 final_account_commitment,
779 account_patch_commitment,
780 AccountUpdateDetails::Private,
781 )
782 .context("failed to build account update")?;
783
784 let tx = ProvenTransaction::new(
785 account_update,
786 Vec::<InputNoteCommitment>::new(),
787 Vec::<OutputNote>::new(),
788 ref_block_num,
789 ref_block_commitment,
790 expiration_block_num,
791 proof,
792 )
793 .context("failed to build proven transaction")?;
794
795 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
796
797 assert_eq!(tx, deserialized);
798
799 Ok(())
800 }
801}