1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use super::{InputNote, ToInputNoteCommitments};
5use crate::Word;
6use crate::account::{AccountUpdateDetails, validate_new_public_account};
7use crate::block::BlockNumber;
8use crate::errors::ProvenTransactionError;
9use crate::note::{NoteHeader, NoteId};
10use crate::transaction::{
11 AccountId,
12 InputNotes,
13 Nullifier,
14 OutputNote,
15 OutputNotes,
16 TransactionId,
17};
18use crate::utils::serde::{
19 ByteReader,
20 ByteWriter,
21 Deserializable,
22 DeserializationError,
23 Serializable,
24};
25use crate::vm::ExecutionProof;
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 account_update.details.validate_size(account_id)?;
340
341 let Some(patch) = account_update.details.validate_for_account(account_id)? else {
342 return Ok(account_update);
343 };
344
345 let actual_patch_commitment = patch.to_commitment();
346 if account_patch_commitment != actual_patch_commitment {
347 return Err(ProvenTransactionError::AccountPatchCommitmentMismatch {
348 expected_patch_commitment: account_patch_commitment,
349 actual_patch_commitment,
350 });
351 }
352
353 if account_update.initial_state_commitment().is_empty() {
354 validate_new_public_account(patch, account_update.final_state_commitment)?;
355 }
356
357 Ok(account_update)
358 }
359
360 pub fn account_id(&self) -> AccountId {
362 self.account_id
363 }
364
365 pub fn initial_state_commitment(&self) -> Word {
367 self.init_state_commitment
368 }
369
370 pub fn final_state_commitment(&self) -> Word {
372 self.final_state_commitment
373 }
374
375 pub fn account_patch_commitment(&self) -> Word {
378 self.account_patch_commitment
379 }
380
381 pub fn details(&self) -> &AccountUpdateDetails {
386 &self.details
387 }
388
389 pub fn is_private(&self) -> bool {
391 self.details.is_private()
392 }
393}
394
395impl Serializable for TxAccountUpdate {
396 fn write_into<W: ByteWriter>(&self, target: &mut W) {
397 self.account_id.write_into(target);
398 self.init_state_commitment.write_into(target);
399 self.final_state_commitment.write_into(target);
400 self.account_patch_commitment.write_into(target);
401 self.details.write_into(target);
402 }
403}
404
405impl Deserializable for TxAccountUpdate {
406 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
407 let account_id = AccountId::read_from(source)?;
408 let init_state_commitment = Word::read_from(source)?;
409 let final_state_commitment = Word::read_from(source)?;
410 let account_patch_commitment = Word::read_from(source)?;
411 let details = AccountUpdateDetails::read_from(source)?;
412
413 Self::new(
414 account_id,
415 init_state_commitment,
416 final_state_commitment,
417 account_patch_commitment,
418 details,
419 )
420 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
421 }
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
433pub struct InputNoteCommitment {
434 nullifier: Nullifier,
435 header: Option<NoteHeader>,
436}
437
438impl InputNoteCommitment {
439 pub fn from_parts_unchecked(nullifier: Nullifier, header: Option<NoteHeader>) -> Self {
446 Self { nullifier, header }
447 }
448
449 pub fn nullifier(&self) -> Nullifier {
451 self.nullifier
452 }
453
454 pub fn header(&self) -> Option<&NoteHeader> {
459 self.header.as_ref()
460 }
461
462 pub fn is_authenticated(&self) -> bool {
468 self.header.is_none()
469 }
470}
471
472impl From<InputNote> for InputNoteCommitment {
473 fn from(note: InputNote) -> Self {
474 Self::from(¬e)
475 }
476}
477
478impl From<&InputNote> for InputNoteCommitment {
479 fn from(note: &InputNote) -> Self {
480 match note {
481 InputNote::Authenticated { note, .. } => Self {
482 nullifier: note.nullifier(),
483 header: None,
484 },
485 InputNote::Unauthenticated { note } => Self {
486 nullifier: note.nullifier(),
487 header: Some(*note.header()),
488 },
489 }
490 }
491}
492
493impl From<Nullifier> for InputNoteCommitment {
494 fn from(nullifier: Nullifier) -> Self {
495 Self { nullifier, header: None }
496 }
497}
498
499impl ToInputNoteCommitments for InputNoteCommitment {
500 fn nullifier(&self) -> Nullifier {
501 self.nullifier
502 }
503
504 fn note_id(&self) -> Option<NoteId> {
505 self.header.as_ref().map(NoteHeader::id)
506 }
507}
508
509impl Serializable for InputNoteCommitment {
513 fn write_into<W: ByteWriter>(&self, target: &mut W) {
514 self.nullifier.write_into(target);
515 self.header.write_into(target);
516 }
517}
518
519impl Deserializable for InputNoteCommitment {
520 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
521 let nullifier = Nullifier::read_from(source)?;
522 let header = <Option<NoteHeader>>::read_from(source)?;
523
524 Ok(Self::from_parts_unchecked(nullifier, header))
525 }
526}
527
528#[cfg(test)]
532mod tests {
533 use alloc::collections::BTreeMap;
534 use alloc::vec::Vec;
535
536 use anyhow::Context;
537 use assert_matches::assert_matches;
538 use miden_crypto::rand::test_utils::rand_value;
539
540 use super::ProvenTransaction;
541 use crate::account::{
542 Account,
543 AccountId,
544 AccountPatch,
545 AccountStoragePatch,
546 AccountType,
547 AccountUpdateDetails,
548 AccountVaultPatch,
549 StorageMapKey,
550 StorageMapPatch,
551 StorageMapPatchEntries,
552 StorageSlotName,
553 };
554 use crate::block::BlockNumber;
555 use crate::errors::ProvenTransactionError;
556 use crate::testing::account_id::{
557 ACCOUNT_ID_PRIVATE_SENDER,
558 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
559 };
560 use crate::testing::add_component::AddComponent;
561 use crate::testing::noop_auth_component::NoopAuthComponent;
562 use crate::transaction::{InputNoteCommitment, OutputNote, TxAccountUpdate};
563 use crate::utils::serde::{Deserializable, Serializable};
564 use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, Felt, Word};
565
566 fn check_if_sync<T: Sync>() {}
567 fn check_if_send<T: Send>() {}
568
569 #[test]
572 fn test_proven_transaction_is_sync() {
573 check_if_sync::<ProvenTransaction>();
574 }
575
576 #[test]
579 fn test_proven_transaction_is_send() {
580 check_if_send::<ProvenTransaction>();
581 }
582
583 #[test]
584 fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
585 let account = Account::builder([9; 32])
587 .account_type(AccountType::Public)
588 .with_component(NoopAuthComponent)
589 .with_component(AddComponent)
590 .build_existing()?;
591 let patch = AccountPatch::try_from(account.clone())?;
592 let patch_commitment = patch.to_commitment();
593
594 let details = AccountUpdateDetails::Public(patch);
595
596 TxAccountUpdate::new(
597 account.id(),
598 account.to_commitment(),
599 account.to_commitment(),
600 patch_commitment,
601 details,
602 )?;
603
604 Ok(())
605 }
606
607 #[test]
608 fn account_update_size_limit_exceeded() {
609 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
610 let mut map = BTreeMap::new();
611 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
615 for _ in 0..required_entries {
616 map.insert(StorageMapKey::from_raw(rand_value()), rand_value::<Word>());
617 }
618 let storage_patch = StorageMapPatch::Update {
619 entries: StorageMapPatchEntries::from_raw(map),
620 };
621
622 let storage_patch =
624 AccountStoragePatch::from_iters([], [], [(StorageSlotName::mock(4), storage_patch)]);
625 let patch = AccountPatch::new(
626 account_id,
627 storage_patch,
628 AccountVaultPatch::default(),
629 None,
630 Some(Felt::from(2u32)),
631 )
632 .unwrap();
633 let details = AccountUpdateDetails::Public(patch);
634 let details_size = details.get_size_hint();
635
636 let err = TxAccountUpdate::new(
637 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
638 EMPTY_WORD,
639 EMPTY_WORD,
640 EMPTY_WORD,
641 details,
642 )
643 .unwrap_err();
644
645 assert!(
646 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
647 );
648 }
649
650 #[test]
653 fn account_update_id_mismatch_between_account_id_and_patch() -> anyhow::Result<()> {
654 let patch_account = Account::builder([9; 32])
655 .account_type(AccountType::Public)
656 .with_component(NoopAuthComponent)
657 .with_component(AddComponent)
658 .build_existing()?;
659 let patch = AccountPatch::try_from(patch_account.clone())?;
660
661 let other_account_id =
662 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)?;
663 assert_ne!(patch_account.id(), other_account_id);
664
665 let err = TxAccountUpdate::new(
666 other_account_id,
667 patch_account.to_commitment(),
668 patch_account.to_commitment(),
669 Word::empty(),
670 AccountUpdateDetails::Public(patch),
671 )
672 .unwrap_err();
673
674 assert_matches!(
675 err,
676 ProvenTransactionError::AccountIdMismatch {
677 tx_account_id,
678 details_account_id,
679 } => {
680 assert_eq!(tx_account_id, other_account_id);
681 assert_eq!(details_account_id, patch_account.id());
682 }
683 );
684
685 Ok(())
686 }
687
688 #[test]
689 fn account_patch_commitment_mismatch() -> anyhow::Result<()> {
690 let account = Account::builder([9; 32])
691 .account_type(AccountType::Public)
692 .with_component(NoopAuthComponent)
693 .with_component(AddComponent)
694 .build_existing()?;
695 let patch = AccountPatch::try_from(account.clone())?;
696 let actual_patch_commitment = patch.to_commitment();
697 let wrong_commitment = EMPTY_WORD;
699 assert_ne!(wrong_commitment, actual_patch_commitment);
700 let err = TxAccountUpdate::new(
701 account.id(),
702 account.to_commitment(),
703 account.to_commitment(),
704 wrong_commitment,
705 AccountUpdateDetails::Public(patch),
706 )
707 .unwrap_err();
708 assert_matches!(
709 err,
710 ProvenTransactionError::AccountPatchCommitmentMismatch {
711 expected_patch_commitment,
712 actual_patch_commitment: returned_actual,
713 } if expected_patch_commitment == wrong_commitment
714 && returned_actual == actual_patch_commitment
715 );
716 Ok(())
717 }
718 #[test]
719 fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
720 let account_id =
721 AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
722 let initial_account_commitment =
723 [2; 32].try_into().expect("failed to create initial account commitment");
724 let final_account_commitment =
725 [3; 32].try_into().expect("failed to create final account commitment");
726 let account_patch_commitment =
727 [4; 32].try_into().expect("failed to create account patch commitment");
728 let ref_block_num = BlockNumber::from(1);
729 let ref_block_commitment = Word::empty();
730 let expiration_block_num = BlockNumber::from(2);
731 let proof = crate::testing::dummy_execution_proof();
732
733 let account_update = TxAccountUpdate::new(
734 account_id,
735 initial_account_commitment,
736 final_account_commitment,
737 account_patch_commitment,
738 AccountUpdateDetails::Private,
739 )
740 .context("failed to build account update")?;
741
742 let tx = ProvenTransaction::new(
743 account_update,
744 Vec::<InputNoteCommitment>::new(),
745 Vec::<OutputNote>::new(),
746 ref_block_num,
747 ref_block_commitment,
748 expiration_block_num,
749 proof,
750 )
751 .context("failed to build proven transaction")?;
752
753 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
754
755 assert_eq!(tx, deserialized);
756
757 Ok(())
758 }
759}