1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use miden_core::deferred::PrecompileWitness;
5
6use super::{InputNote, ToInputNoteCommitments};
7use crate::Word;
8use crate::account::{AccountUpdateDetails, validate_new_public_account};
9use crate::block::BlockNumber;
10use crate::errors::ProvenTransactionError;
11use crate::note::{NoteHeader, NoteId};
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, PrecompileStatus};
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 expiration_block_num: BlockNumber,
64
65 proof: ExecutionProof,
67}
68
69impl ProvenTransaction {
70 pub fn new(
90 account_update: TxAccountUpdate,
91 input_notes: impl IntoIterator<Item = impl Into<InputNoteCommitment>>,
92 output_notes: impl IntoIterator<Item = impl Into<OutputNote>>,
93 ref_block_num: BlockNumber,
94 ref_block_commitment: Word,
95 expiration_block_num: BlockNumber,
96 proof: ExecutionProof,
97 ) -> Result<Self, ProvenTransactionError> {
98 let input_notes: Vec<InputNoteCommitment> =
99 input_notes.into_iter().map(Into::into).collect();
100 let output_notes: Vec<OutputNote> = output_notes.into_iter().map(Into::into).collect();
101
102 let input_notes =
103 InputNotes::new(input_notes).map_err(ProvenTransactionError::InputNotesError)?;
104 let output_notes =
105 OutputNotes::new(output_notes).map_err(ProvenTransactionError::OutputNotesError)?;
106
107 Self::from_parts(
108 account_update,
109 input_notes,
110 output_notes,
111 ref_block_num,
112 ref_block_commitment,
113 expiration_block_num,
114 proof,
115 )
116 }
117
118 pub fn id(&self) -> TransactionId {
123 self.id
124 }
125
126 pub fn account_id(&self) -> AccountId {
128 self.account_update.account_id()
129 }
130
131 pub fn account_update(&self) -> &TxAccountUpdate {
133 &self.account_update
134 }
135
136 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
138 &self.input_notes
139 }
140
141 pub fn output_notes(&self) -> &OutputNotes {
143 &self.output_notes
144 }
145
146 pub fn proof(&self) -> &ExecutionProof {
148 &self.proof
149 }
150
151 pub fn precompile_witness(&self) -> Option<&PrecompileWitness> {
157 match self.proof.precompile() {
158 PrecompileStatus::Deferred(witness) => Some(witness),
159 PrecompileStatus::Empty | PrecompileStatus::Proven(_) => None,
160 }
161 }
162
163 pub fn ref_block_num(&self) -> BlockNumber {
165 self.ref_block_num
166 }
167
168 pub fn ref_block_commitment(&self) -> Word {
170 self.ref_block_commitment
171 }
172
173 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
175 self.input_notes.iter().filter_map(|note| note.header())
176 }
177
178 pub fn expiration_block_num(&self) -> BlockNumber {
180 self.expiration_block_num
181 }
182
183 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
187 self.input_notes.iter().map(InputNoteCommitment::nullifier)
188 }
189
190 fn from_parts(
208 account_update: TxAccountUpdate,
209 input_notes: InputNotes<InputNoteCommitment>,
210 output_notes: OutputNotes,
211 ref_block_num: BlockNumber,
212 ref_block_commitment: Word,
213 expiration_block_num: BlockNumber,
214 proof: ExecutionProof,
215 ) -> Result<Self, ProvenTransactionError> {
216 if account_update.initial_state_commitment() == account_update.final_state_commitment()
219 && input_notes.commitment().is_empty()
220 {
221 return Err(ProvenTransactionError::EmptyTransaction);
222 }
223
224 for input_note in input_notes.iter().filter_map(InputNoteCommitment::header) {
229 if output_notes.iter().any(|output_note| output_note.id() == input_note.id()) {
230 return Err(ProvenTransactionError::NoteCreatedAndConsumed(input_note.id()));
231 }
232 }
233
234 let id = TransactionId::new(
235 account_update.initial_state_commitment(),
236 account_update.final_state_commitment(),
237 input_notes.commitment(),
238 output_notes.commitment(),
239 );
240
241 Ok(Self {
242 id,
243 account_update,
244 input_notes,
245 output_notes,
246 ref_block_num,
247 ref_block_commitment,
248 expiration_block_num,
249 proof,
250 })
251 }
252}
253
254impl Serializable for ProvenTransaction {
255 fn write_into<W: ByteWriter>(&self, target: &mut W) {
256 self.account_update.write_into(target);
257 self.input_notes.write_into(target);
258 self.output_notes.write_into(target);
259 self.ref_block_num.write_into(target);
260 self.ref_block_commitment.write_into(target);
261 self.expiration_block_num.write_into(target);
262 self.proof.write_into(target);
263 }
264}
265
266impl Deserializable for ProvenTransaction {
267 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
268 let account_update = TxAccountUpdate::read_from(source)?;
269
270 let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
271 let output_notes = OutputNotes::read_from(source)?;
272
273 let ref_block_num = BlockNumber::read_from(source)?;
274 let ref_block_commitment = Word::read_from(source)?;
275 let expiration_block_num = BlockNumber::read_from(source)?;
276 let proof = ExecutionProof::read_from(source)?;
277
278 Self::from_parts(
279 account_update,
280 input_notes,
281 output_notes,
282 ref_block_num,
283 ref_block_commitment,
284 expiration_block_num,
285 proof,
286 )
287 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct TxAccountUpdate {
297 account_id: AccountId,
299
300 init_state_commitment: Word,
304
305 final_state_commitment: Word,
307
308 account_patch_commitment: Word,
316
317 details: AccountUpdateDetails,
321}
322
323impl TxAccountUpdate {
324 pub fn new(
339 account_id: AccountId,
340 init_state_commitment: Word,
341 final_state_commitment: Word,
342 account_patch_commitment: Word,
343 details: AccountUpdateDetails,
344 ) -> Result<Self, ProvenTransactionError> {
345 let account_update = Self {
346 account_id,
347 init_state_commitment,
348 final_state_commitment,
349 account_patch_commitment,
350 details,
351 };
352
353 account_update.details.validate_size(account_id)?;
354
355 let Some(patch) = account_update.details.validate_for_account(account_id)? else {
356 return Ok(account_update);
357 };
358
359 let actual_patch_commitment = patch.to_commitment();
360 if account_patch_commitment != actual_patch_commitment {
361 return Err(ProvenTransactionError::AccountPatchCommitmentMismatch {
362 expected_patch_commitment: account_patch_commitment,
363 actual_patch_commitment,
364 });
365 }
366
367 if account_update.initial_state_commitment().is_empty() {
368 validate_new_public_account(patch, account_update.final_state_commitment)?;
369 }
370
371 Ok(account_update)
372 }
373
374 pub fn account_id(&self) -> AccountId {
376 self.account_id
377 }
378
379 pub fn initial_state_commitment(&self) -> Word {
381 self.init_state_commitment
382 }
383
384 pub fn final_state_commitment(&self) -> Word {
386 self.final_state_commitment
387 }
388
389 pub fn account_patch_commitment(&self) -> Word {
392 self.account_patch_commitment
393 }
394
395 pub fn details(&self) -> &AccountUpdateDetails {
400 &self.details
401 }
402
403 pub fn is_private(&self) -> bool {
405 self.details.is_private()
406 }
407}
408
409impl Serializable for TxAccountUpdate {
410 fn write_into<W: ByteWriter>(&self, target: &mut W) {
411 self.account_id.write_into(target);
412 self.init_state_commitment.write_into(target);
413 self.final_state_commitment.write_into(target);
414 self.account_patch_commitment.write_into(target);
415 self.details.write_into(target);
416 }
417}
418
419impl Deserializable for TxAccountUpdate {
420 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
421 let account_id = AccountId::read_from(source)?;
422 let init_state_commitment = Word::read_from(source)?;
423 let final_state_commitment = Word::read_from(source)?;
424 let account_patch_commitment = Word::read_from(source)?;
425 let details = AccountUpdateDetails::read_from(source)?;
426
427 Self::new(
428 account_id,
429 init_state_commitment,
430 final_state_commitment,
431 account_patch_commitment,
432 details,
433 )
434 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
435 }
436}
437
438#[derive(Debug, Clone, PartialEq, Eq)]
447pub struct InputNoteCommitment {
448 nullifier: Nullifier,
449 header: Option<NoteHeader>,
450}
451
452impl InputNoteCommitment {
453 pub fn from_parts_unchecked(nullifier: Nullifier, header: Option<NoteHeader>) -> Self {
460 Self { nullifier, header }
461 }
462
463 pub fn nullifier(&self) -> Nullifier {
465 self.nullifier
466 }
467
468 pub fn header(&self) -> Option<&NoteHeader> {
473 self.header.as_ref()
474 }
475
476 pub fn is_authenticated(&self) -> bool {
482 self.header.is_none()
483 }
484}
485
486impl From<InputNote> for InputNoteCommitment {
487 fn from(note: InputNote) -> Self {
488 Self::from(¬e)
489 }
490}
491
492impl From<&InputNote> for InputNoteCommitment {
493 fn from(note: &InputNote) -> Self {
494 match note {
495 InputNote::Authenticated { note, .. } => Self {
496 nullifier: note.nullifier(),
497 header: None,
498 },
499 InputNote::Unauthenticated { note } => Self {
500 nullifier: note.nullifier(),
501 header: Some(*note.header()),
502 },
503 }
504 }
505}
506
507impl From<Nullifier> for InputNoteCommitment {
508 fn from(nullifier: Nullifier) -> Self {
509 Self { nullifier, header: None }
510 }
511}
512
513impl ToInputNoteCommitments for InputNoteCommitment {
514 fn nullifier(&self) -> Nullifier {
515 self.nullifier
516 }
517
518 fn note_id(&self) -> Option<NoteId> {
519 self.header.as_ref().map(NoteHeader::id)
520 }
521}
522
523impl Serializable for InputNoteCommitment {
527 fn write_into<W: ByteWriter>(&self, target: &mut W) {
528 self.nullifier.write_into(target);
529 self.header.write_into(target);
530 }
531}
532
533impl Deserializable for InputNoteCommitment {
534 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
535 let nullifier = Nullifier::read_from(source)?;
536 let header = <Option<NoteHeader>>::read_from(source)?;
537
538 Ok(Self::from_parts_unchecked(nullifier, header))
539 }
540}
541
542#[cfg(test)]
546mod tests {
547 use alloc::collections::BTreeMap;
548 use alloc::vec::Vec;
549
550 use anyhow::Context;
551 use assert_matches::assert_matches;
552 use miden_crypto::rand::test_utils::rand_value;
553
554 use super::ProvenTransaction;
555 use crate::account::{
556 Account,
557 AccountId,
558 AccountPatch,
559 AccountStoragePatch,
560 AccountType,
561 AccountUpdateDetails,
562 AccountVaultPatch,
563 StorageMapKey,
564 StorageMapPatch,
565 StorageMapPatchEntries,
566 StorageSlotName,
567 };
568 use crate::block::BlockNumber;
569 use crate::errors::ProvenTransactionError;
570 use crate::testing::account_id::{
571 ACCOUNT_ID_PRIVATE_SENDER,
572 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
573 };
574 use crate::testing::add_component::AddComponent;
575 use crate::testing::noop_auth_component::NoopAuthComponent;
576 use crate::transaction::{InputNoteCommitment, OutputNote, TxAccountUpdate};
577 use crate::utils::serde::{Deserializable, Serializable};
578 use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, Felt, Word};
579
580 fn check_if_sync<T: Sync>() {}
581 fn check_if_send<T: Send>() {}
582
583 #[test]
586 fn test_proven_transaction_is_sync() {
587 check_if_sync::<ProvenTransaction>();
588 }
589
590 #[test]
593 fn test_proven_transaction_is_send() {
594 check_if_send::<ProvenTransaction>();
595 }
596
597 #[test]
598 fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
599 let account = Account::builder([9; 32])
601 .account_type(AccountType::Public)
602 .with_component(NoopAuthComponent)
603 .with_component(AddComponent)
604 .build_existing()?;
605 let patch = AccountPatch::try_from(account.clone())?;
606 let patch_commitment = patch.to_commitment();
607
608 let details = AccountUpdateDetails::Public(patch);
609
610 TxAccountUpdate::new(
611 account.id(),
612 account.to_commitment(),
613 account.to_commitment(),
614 patch_commitment,
615 details,
616 )?;
617
618 Ok(())
619 }
620
621 #[test]
622 fn account_update_size_limit_exceeded() {
623 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
624 let mut map = BTreeMap::new();
625 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
629 for _ in 0..required_entries {
630 map.insert(StorageMapKey::from_raw(rand_value()), rand_value::<Word>());
631 }
632 let storage_patch = StorageMapPatch::Update {
633 entries: StorageMapPatchEntries::from_raw(map),
634 };
635
636 let storage_patch =
638 AccountStoragePatch::from_iters([], [], [(StorageSlotName::mock(4), storage_patch)]);
639 let patch = AccountPatch::new(
640 account_id,
641 storage_patch,
642 AccountVaultPatch::default(),
643 None,
644 Some(Felt::from(2u32)),
645 )
646 .unwrap();
647 let details = AccountUpdateDetails::Public(patch);
648 let details_size = details.get_size_hint();
649
650 let err = TxAccountUpdate::new(
651 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
652 EMPTY_WORD,
653 EMPTY_WORD,
654 EMPTY_WORD,
655 details,
656 )
657 .unwrap_err();
658
659 assert!(
660 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
661 );
662 }
663
664 #[test]
667 fn account_update_id_mismatch_between_account_id_and_patch() -> anyhow::Result<()> {
668 let patch_account = Account::builder([9; 32])
669 .account_type(AccountType::Public)
670 .with_component(NoopAuthComponent)
671 .with_component(AddComponent)
672 .build_existing()?;
673 let patch = AccountPatch::try_from(patch_account.clone())?;
674
675 let other_account_id =
676 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)?;
677 assert_ne!(patch_account.id(), other_account_id);
678
679 let err = TxAccountUpdate::new(
680 other_account_id,
681 patch_account.to_commitment(),
682 patch_account.to_commitment(),
683 Word::empty(),
684 AccountUpdateDetails::Public(patch),
685 )
686 .unwrap_err();
687
688 assert_matches!(
689 err,
690 ProvenTransactionError::AccountIdMismatch {
691 tx_account_id,
692 details_account_id,
693 } => {
694 assert_eq!(tx_account_id, other_account_id);
695 assert_eq!(details_account_id, patch_account.id());
696 }
697 );
698
699 Ok(())
700 }
701
702 #[test]
703 fn account_patch_commitment_mismatch() -> anyhow::Result<()> {
704 let account = Account::builder([9; 32])
705 .account_type(AccountType::Public)
706 .with_component(NoopAuthComponent)
707 .with_component(AddComponent)
708 .build_existing()?;
709 let patch = AccountPatch::try_from(account.clone())?;
710 let actual_patch_commitment = patch.to_commitment();
711 let wrong_commitment = EMPTY_WORD;
713 assert_ne!(wrong_commitment, actual_patch_commitment);
714 let err = TxAccountUpdate::new(
715 account.id(),
716 account.to_commitment(),
717 account.to_commitment(),
718 wrong_commitment,
719 AccountUpdateDetails::Public(patch),
720 )
721 .unwrap_err();
722 assert_matches!(
723 err,
724 ProvenTransactionError::AccountPatchCommitmentMismatch {
725 expected_patch_commitment,
726 actual_patch_commitment: returned_actual,
727 } if expected_patch_commitment == wrong_commitment
728 && returned_actual == actual_patch_commitment
729 );
730 Ok(())
731 }
732 #[test]
733 fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
734 let account_id =
735 AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
736 let initial_account_commitment =
737 [2; 32].try_into().expect("failed to create initial account commitment");
738 let final_account_commitment =
739 [3; 32].try_into().expect("failed to create final account commitment");
740 let account_patch_commitment =
741 [4; 32].try_into().expect("failed to create account patch commitment");
742 let ref_block_num = BlockNumber::from(1);
743 let ref_block_commitment = Word::empty();
744 let expiration_block_num = BlockNumber::from(2);
745 let proof = crate::testing::dummy_execution_proof();
746
747 let account_update = TxAccountUpdate::new(
748 account_id,
749 initial_account_commitment,
750 final_account_commitment,
751 account_patch_commitment,
752 AccountUpdateDetails::Private,
753 )
754 .context("failed to build account update")?;
755
756 let tx = ProvenTransaction::new(
757 account_update,
758 Vec::<InputNoteCommitment>::new(),
759 Vec::<OutputNote>::new(),
760 ref_block_num,
761 ref_block_commitment,
762 expiration_block_num,
763 proof,
764 )
765 .context("failed to build proven transaction")?;
766
767 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
768
769 assert_eq!(tx, deserialized);
770
771 Ok(())
772 }
773}