1use alloc::boxed::Box;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use super::{InputNote, ToInputNoteCommitments};
6use crate::account::{Account, AccountUpdateDetails};
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;
26use crate::{ACCOUNT_UPDATE_MAX_SIZE, Word};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ProvenTransaction {
42 id: TransactionId,
44
45 account_update: TxAccountUpdate,
47
48 input_notes: InputNotes<InputNoteCommitment>,
50
51 output_notes: OutputNotes,
54
55 ref_block_num: BlockNumber,
57
58 ref_block_commitment: Word,
60
61 expiration_block_num: BlockNumber,
63
64 proof: ExecutionProof,
66}
67
68impl ProvenTransaction {
69 pub fn new(
89 account_update: TxAccountUpdate,
90 input_notes: impl IntoIterator<Item = impl Into<InputNoteCommitment>>,
91 output_notes: impl IntoIterator<Item = impl Into<OutputNote>>,
92 ref_block_num: BlockNumber,
93 ref_block_commitment: Word,
94 expiration_block_num: BlockNumber,
95 proof: ExecutionProof,
96 ) -> Result<Self, ProvenTransactionError> {
97 let input_notes: Vec<InputNoteCommitment> =
98 input_notes.into_iter().map(Into::into).collect();
99 let output_notes: Vec<OutputNote> = output_notes.into_iter().map(Into::into).collect();
100
101 let input_notes =
102 InputNotes::new(input_notes).map_err(ProvenTransactionError::InputNotesError)?;
103 let output_notes =
104 OutputNotes::new(output_notes).map_err(ProvenTransactionError::OutputNotesError)?;
105
106 for input_note in input_notes.iter().filter_map(InputNoteCommitment::header) {
111 if output_notes.iter().any(|output_note| output_note.id() == input_note.id()) {
112 return Err(ProvenTransactionError::NoteCreatedAndConsumed(input_note.id()));
113 }
114 }
115
116 let id = TransactionId::new(
117 account_update.initial_state_commitment(),
118 account_update.final_state_commitment(),
119 input_notes.commitment(),
120 output_notes.commitment(),
121 );
122
123 let proven_transaction = Self {
124 id,
125 account_update,
126 input_notes,
127 output_notes,
128 ref_block_num,
129 ref_block_commitment,
130 expiration_block_num,
131 proof,
132 };
133
134 proven_transaction.validate()
135 }
136
137 pub fn id(&self) -> TransactionId {
142 self.id
143 }
144
145 pub fn account_id(&self) -> AccountId {
147 self.account_update.account_id()
148 }
149
150 pub fn account_update(&self) -> &TxAccountUpdate {
152 &self.account_update
153 }
154
155 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
157 &self.input_notes
158 }
159
160 pub fn output_notes(&self) -> &OutputNotes {
162 &self.output_notes
163 }
164
165 pub fn proof(&self) -> &ExecutionProof {
167 &self.proof
168 }
169
170 pub fn ref_block_num(&self) -> BlockNumber {
172 self.ref_block_num
173 }
174
175 pub fn ref_block_commitment(&self) -> Word {
177 self.ref_block_commitment
178 }
179
180 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
182 self.input_notes.iter().filter_map(|note| note.header())
183 }
184
185 pub fn expiration_block_num(&self) -> BlockNumber {
187 self.expiration_block_num
188 }
189
190 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
194 self.input_notes.iter().map(InputNoteCommitment::nullifier)
195 }
196
197 fn validate(self) -> Result<Self, ProvenTransactionError> {
210 if self.account_update.initial_state_commitment()
213 == self.account_update.final_state_commitment()
214 && self.input_notes.commitment().is_empty()
215 {
216 return Err(ProvenTransactionError::EmptyTransaction);
217 }
218
219 match &self.account_update.details {
220 AccountUpdateDetails::Private => (),
223 AccountUpdateDetails::Public(account_patch) => {
224 let expected_commitment = self.account_update.account_patch_commitment;
225 let actual_commitment = account_patch.to_commitment();
226 if expected_commitment != actual_commitment {
227 return Err(ProvenTransactionError::AccountPatchCommitmentMismatch(Box::from(
228 format!(
229 "expected account patch commitment {expected_commitment} but found {actual_commitment}"
230 ),
231 )));
232 }
233 },
234 }
235
236 Ok(self)
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 let id = TransactionId::new(
265 account_update.initial_state_commitment(),
266 account_update.final_state_commitment(),
267 input_notes.commitment(),
268 output_notes.commitment(),
269 );
270
271 let proven_transaction = Self {
272 id,
273 account_update,
274 input_notes,
275 output_notes,
276 ref_block_num,
277 ref_block_commitment,
278 expiration_block_num,
279 proof,
280 };
281
282 proven_transaction
283 .validate()
284 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
285 }
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct TxAccountUpdate {
294 account_id: AccountId,
296
297 init_state_commitment: Word,
301
302 final_state_commitment: Word,
304
305 account_patch_commitment: Word,
313
314 details: AccountUpdateDetails,
318}
319
320impl TxAccountUpdate {
321 pub fn new(
336 account_id: AccountId,
337 init_state_commitment: Word,
338 final_state_commitment: Word,
339 account_patch_commitment: Word,
340 details: AccountUpdateDetails,
341 ) -> Result<Self, ProvenTransactionError> {
342 let account_update = Self {
343 account_id,
344 init_state_commitment,
345 final_state_commitment,
346 account_patch_commitment,
347 details,
348 };
349
350 let account_update_size = account_update.details.get_size_hint();
351 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
352 return Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
353 account_id,
354 update_size: account_update_size,
355 });
356 }
357
358 if account_id.is_private() {
359 if account_update.details.is_private() {
360 return Ok(account_update);
361 } else {
362 return Err(ProvenTransactionError::PrivateAccountWithDetails(account_id));
363 }
364 }
365
366 match account_update.details() {
367 AccountUpdateDetails::Private => {
368 return Err(ProvenTransactionError::PublicStateAccountMissingDetails(
369 account_update.account_id(),
370 ));
371 },
372 AccountUpdateDetails::Public(patch) => {
373 if patch.id() != account_id {
374 return Err(ProvenTransactionError::AccountIdMismatch {
375 tx_account_id: account_id,
376 details_account_id: patch.id(),
377 });
378 }
379
380 let is_new_account = account_update.initial_state_commitment().is_empty();
381 if is_new_account {
382 let account = Account::try_from(patch).map_err(|err| {
385 ProvenTransactionError::NewPublicStateAccountRequiresFullStatePatch {
386 id: patch.id(),
387 source: err,
388 }
389 })?;
390
391 if account.to_commitment() != account_update.final_state_commitment {
392 return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
393 tx_final_commitment: account_update.final_state_commitment,
394 details_commitment: account.to_commitment(),
395 });
396 }
397 }
398 },
399 }
400
401 Ok(account_update)
402 }
403
404 pub fn account_id(&self) -> AccountId {
406 self.account_id
407 }
408
409 pub fn initial_state_commitment(&self) -> Word {
411 self.init_state_commitment
412 }
413
414 pub fn final_state_commitment(&self) -> Word {
416 self.final_state_commitment
417 }
418
419 pub fn account_patch_commitment(&self) -> Word {
422 self.account_patch_commitment
423 }
424
425 pub fn details(&self) -> &AccountUpdateDetails {
430 &self.details
431 }
432
433 pub fn is_private(&self) -> bool {
435 self.details.is_private()
436 }
437}
438
439impl Serializable for TxAccountUpdate {
440 fn write_into<W: ByteWriter>(&self, target: &mut W) {
441 self.account_id.write_into(target);
442 self.init_state_commitment.write_into(target);
443 self.final_state_commitment.write_into(target);
444 self.account_patch_commitment.write_into(target);
445 self.details.write_into(target);
446 }
447}
448
449impl Deserializable for TxAccountUpdate {
450 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
451 let account_id = AccountId::read_from(source)?;
452 let init_state_commitment = Word::read_from(source)?;
453 let final_state_commitment = Word::read_from(source)?;
454 let account_patch_commitment = Word::read_from(source)?;
455 let details = AccountUpdateDetails::read_from(source)?;
456
457 Self::new(
458 account_id,
459 init_state_commitment,
460 final_state_commitment,
461 account_patch_commitment,
462 details,
463 )
464 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
465 }
466}
467
468#[derive(Debug, Clone, PartialEq, Eq)]
477pub struct InputNoteCommitment {
478 nullifier: Nullifier,
479 header: Option<NoteHeader>,
480}
481
482impl InputNoteCommitment {
483 pub fn from_parts_unchecked(nullifier: Nullifier, header: Option<NoteHeader>) -> Self {
490 Self { nullifier, header }
491 }
492
493 pub fn nullifier(&self) -> Nullifier {
495 self.nullifier
496 }
497
498 pub fn header(&self) -> Option<&NoteHeader> {
503 self.header.as_ref()
504 }
505
506 pub fn is_authenticated(&self) -> bool {
512 self.header.is_none()
513 }
514}
515
516impl From<InputNote> for InputNoteCommitment {
517 fn from(note: InputNote) -> Self {
518 Self::from(¬e)
519 }
520}
521
522impl From<&InputNote> for InputNoteCommitment {
523 fn from(note: &InputNote) -> Self {
524 match note {
525 InputNote::Authenticated { note, .. } => Self {
526 nullifier: note.nullifier(),
527 header: None,
528 },
529 InputNote::Unauthenticated { note } => Self {
530 nullifier: note.nullifier(),
531 header: Some(*note.header()),
532 },
533 }
534 }
535}
536
537impl From<Nullifier> for InputNoteCommitment {
538 fn from(nullifier: Nullifier) -> Self {
539 Self { nullifier, header: None }
540 }
541}
542
543impl ToInputNoteCommitments for InputNoteCommitment {
544 fn nullifier(&self) -> Nullifier {
545 self.nullifier
546 }
547
548 fn note_id(&self) -> Option<NoteId> {
549 self.header.as_ref().map(NoteHeader::id)
550 }
551}
552
553impl Serializable for InputNoteCommitment {
557 fn write_into<W: ByteWriter>(&self, target: &mut W) {
558 self.nullifier.write_into(target);
559 self.header.write_into(target);
560 }
561}
562
563impl Deserializable for InputNoteCommitment {
564 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
565 let nullifier = Nullifier::read_from(source)?;
566 let header = <Option<NoteHeader>>::read_from(source)?;
567
568 Ok(Self::from_parts_unchecked(nullifier, header))
569 }
570}
571
572#[cfg(test)]
576mod tests {
577 use alloc::collections::BTreeMap;
578 use alloc::vec::Vec;
579
580 use anyhow::Context;
581 use assert_matches::assert_matches;
582 use miden_crypto::rand::test_utils::rand_value;
583 use miden_verifier::ExecutionProof;
584
585 use super::ProvenTransaction;
586 use crate::account::{
587 Account,
588 AccountId,
589 AccountPatch,
590 AccountStoragePatch,
591 AccountType,
592 AccountUpdateDetails,
593 AccountVaultPatch,
594 StorageMapKey,
595 StorageMapPatch,
596 StorageMapPatchEntries,
597 StorageSlotName,
598 };
599 use crate::block::BlockNumber;
600 use crate::errors::ProvenTransactionError;
601 use crate::testing::account_id::{
602 ACCOUNT_ID_PRIVATE_SENDER,
603 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
604 };
605 use crate::testing::add_component::AddComponent;
606 use crate::testing::noop_auth_component::NoopAuthComponent;
607 use crate::transaction::{InputNoteCommitment, OutputNote, TxAccountUpdate};
608 use crate::utils::serde::{Deserializable, Serializable};
609 use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, Felt, Word};
610
611 fn check_if_sync<T: Sync>() {}
612 fn check_if_send<T: Send>() {}
613
614 #[test]
617 fn test_proven_transaction_is_sync() {
618 check_if_sync::<ProvenTransaction>();
619 }
620
621 #[test]
624 fn test_proven_transaction_is_send() {
625 check_if_send::<ProvenTransaction>();
626 }
627
628 #[test]
629 fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
630 let account = Account::builder([9; 32])
632 .account_type(AccountType::Public)
633 .with_auth_component(NoopAuthComponent)
634 .with_component(AddComponent)
635 .build_existing()?;
636 let patch = AccountPatch::try_from(account.clone())?;
637
638 let details = AccountUpdateDetails::Public(patch);
639
640 TxAccountUpdate::new(
641 account.id(),
642 account.to_commitment(),
643 account.to_commitment(),
644 Word::empty(),
645 details,
646 )?;
647
648 Ok(())
649 }
650
651 #[test]
652 fn account_update_size_limit_exceeded() {
653 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
654 let mut map = BTreeMap::new();
655 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
659 for _ in 0..required_entries {
660 map.insert(StorageMapKey::from_raw(rand_value()), rand_value::<Word>());
661 }
662 let storage_patch = StorageMapPatch::Update {
663 entries: StorageMapPatchEntries::from_raw(map),
664 };
665
666 let storage_patch =
668 AccountStoragePatch::from_iters([], [], [(StorageSlotName::mock(4), storage_patch)]);
669 let patch = AccountPatch::new(
670 account_id,
671 storage_patch,
672 AccountVaultPatch::default(),
673 None,
674 Some(Felt::from(2u32)),
675 )
676 .unwrap();
677 let details = AccountUpdateDetails::Public(patch);
678 let details_size = details.get_size_hint();
679
680 let err = TxAccountUpdate::new(
681 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
682 EMPTY_WORD,
683 EMPTY_WORD,
684 EMPTY_WORD,
685 details,
686 )
687 .unwrap_err();
688
689 assert!(
690 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
691 );
692 }
693
694 #[test]
697 fn account_update_id_mismatch_between_account_id_and_patch() -> anyhow::Result<()> {
698 let patch_account = Account::builder([9; 32])
699 .account_type(AccountType::Public)
700 .with_auth_component(NoopAuthComponent)
701 .with_component(AddComponent)
702 .build_existing()?;
703 let patch = AccountPatch::try_from(patch_account.clone())?;
704
705 let other_account_id =
706 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)?;
707 assert_ne!(patch_account.id(), other_account_id);
708
709 let err = TxAccountUpdate::new(
710 other_account_id,
711 patch_account.to_commitment(),
712 patch_account.to_commitment(),
713 Word::empty(),
714 AccountUpdateDetails::Public(patch),
715 )
716 .unwrap_err();
717
718 assert_matches!(
719 err,
720 ProvenTransactionError::AccountIdMismatch {
721 tx_account_id,
722 details_account_id,
723 } => {
724 assert_eq!(tx_account_id, other_account_id);
725 assert_eq!(details_account_id, patch_account.id());
726 }
727 );
728
729 Ok(())
730 }
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 = ExecutionProof::new_dummy();
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}