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 new(
93 account_update: TxAccountUpdate,
94 input_notes: impl IntoIterator<Item = impl Into<InputNoteCommitment>>,
95 output_notes: impl IntoIterator<Item = impl Into<OutputNote>>,
96 ref_block_num: BlockNumber,
97 ref_block_commitment: Word,
98 fee: FungibleAsset,
99 expiration_block_num: BlockNumber,
100 proof: ExecutionProof,
101 ) -> Result<Self, ProvenTransactionError> {
102 let input_notes: Vec<InputNoteCommitment> =
103 input_notes.into_iter().map(Into::into).collect();
104 let output_notes: Vec<OutputNote> = output_notes.into_iter().map(Into::into).collect();
105
106 let input_notes =
107 InputNotes::new(input_notes).map_err(ProvenTransactionError::InputNotesError)?;
108 let output_notes =
109 OutputNotes::new(output_notes).map_err(ProvenTransactionError::OutputNotesError)?;
110
111 let id = TransactionId::new(
112 account_update.initial_state_commitment(),
113 account_update.final_state_commitment(),
114 input_notes.commitment(),
115 output_notes.commitment(),
116 fee,
117 );
118
119 let proven_transaction = Self {
120 id,
121 account_update,
122 input_notes,
123 output_notes,
124 ref_block_num,
125 ref_block_commitment,
126 fee,
127 expiration_block_num,
128 proof,
129 };
130
131 proven_transaction.validate()
132 }
133
134 pub fn id(&self) -> TransactionId {
139 self.id
140 }
141
142 pub fn account_id(&self) -> AccountId {
144 self.account_update.account_id()
145 }
146
147 pub fn account_update(&self) -> &TxAccountUpdate {
149 &self.account_update
150 }
151
152 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
154 &self.input_notes
155 }
156
157 pub fn output_notes(&self) -> &OutputNotes {
159 &self.output_notes
160 }
161
162 pub fn proof(&self) -> &ExecutionProof {
164 &self.proof
165 }
166
167 pub fn ref_block_num(&self) -> BlockNumber {
169 self.ref_block_num
170 }
171
172 pub fn ref_block_commitment(&self) -> Word {
174 self.ref_block_commitment
175 }
176
177 pub fn fee(&self) -> FungibleAsset {
179 self.fee
180 }
181
182 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
184 self.input_notes.iter().filter_map(|note| note.header())
185 }
186
187 pub fn expiration_block_num(&self) -> BlockNumber {
189 self.expiration_block_num
190 }
191
192 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
196 self.input_notes.iter().map(InputNoteCommitment::nullifier)
197 }
198
199 fn validate(mut self) -> Result<Self, ProvenTransactionError> {
212 if self.account_update.initial_state_commitment()
215 == self.account_update.final_state_commitment()
216 && self.input_notes.commitment().is_empty()
217 {
218 return Err(ProvenTransactionError::EmptyTransaction);
219 }
220
221 match &mut self.account_update.details {
222 AccountUpdateDetails::Private => (),
225 AccountUpdateDetails::Delta(post_fee_account_delta) => {
226 post_fee_account_delta.vault_mut().add_asset(self.fee.into()).map_err(|err| {
229 ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err))
230 })?;
231
232 let expected_commitment = self.account_update.account_delta_commitment;
233 let actual_commitment = post_fee_account_delta.to_commitment();
234 if expected_commitment != actual_commitment {
235 return Err(ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(
236 format!(
237 "expected account delta commitment {expected_commitment} but found {actual_commitment}"
238 ),
239 )));
240 }
241
242 post_fee_account_delta.vault_mut().remove_asset(self.fee.into()).map_err(
244 |err| ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err)),
245 )?;
246 },
247 }
248
249 Ok(self)
250 }
251}
252
253impl Serializable for ProvenTransaction {
254 fn write_into<W: ByteWriter>(&self, target: &mut W) {
255 self.account_update.write_into(target);
256 self.input_notes.write_into(target);
257 self.output_notes.write_into(target);
258 self.ref_block_num.write_into(target);
259 self.ref_block_commitment.write_into(target);
260 self.fee.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 fee = FungibleAsset::read_from(source)?;
276 let expiration_block_num = BlockNumber::read_from(source)?;
277 let proof = ExecutionProof::read_from(source)?;
278
279 let id = TransactionId::new(
280 account_update.initial_state_commitment(),
281 account_update.final_state_commitment(),
282 input_notes.commitment(),
283 output_notes.commitment(),
284 fee,
285 );
286
287 let proven_transaction = Self {
288 id,
289 account_update,
290 input_notes,
291 output_notes,
292 ref_block_num,
293 ref_block_commitment,
294 fee,
295 expiration_block_num,
296 proof,
297 };
298
299 proven_transaction
300 .validate()
301 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct TxAccountUpdate {
311 account_id: AccountId,
313
314 init_state_commitment: Word,
318
319 final_state_commitment: Word,
321
322 account_delta_commitment: Word,
332
333 details: AccountUpdateDetails,
337}
338
339impl TxAccountUpdate {
340 pub fn new(
355 account_id: AccountId,
356 init_state_commitment: Word,
357 final_state_commitment: Word,
358 account_delta_commitment: Word,
359 details: AccountUpdateDetails,
360 ) -> Result<Self, ProvenTransactionError> {
361 let account_update = Self {
362 account_id,
363 init_state_commitment,
364 final_state_commitment,
365 account_delta_commitment,
366 details,
367 };
368
369 let account_update_size = account_update.details.get_size_hint();
370 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
371 return Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
372 account_id,
373 update_size: account_update_size,
374 });
375 }
376
377 if account_id.is_private() {
378 if account_update.details.is_private() {
379 return Ok(account_update);
380 } else {
381 return Err(ProvenTransactionError::PrivateAccountWithDetails(account_id));
382 }
383 }
384
385 match account_update.details() {
386 AccountUpdateDetails::Private => {
387 return Err(ProvenTransactionError::PublicStateAccountMissingDetails(
388 account_update.account_id(),
389 ));
390 },
391 AccountUpdateDetails::Delta(delta) => {
392 let is_new_account = account_update.initial_state_commitment().is_empty();
393 if is_new_account {
394 let account = Account::try_from(delta).map_err(|err| {
397 ProvenTransactionError::NewPublicStateAccountRequiresFullStateDelta {
398 id: delta.id(),
399 source: err,
400 }
401 })?;
402
403 if account.id() != account_id {
404 return Err(ProvenTransactionError::AccountIdMismatch {
405 tx_account_id: account_id,
406 details_account_id: account.id(),
407 });
408 }
409
410 if account.to_commitment() != account_update.final_state_commitment {
411 return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
412 tx_final_commitment: account_update.final_state_commitment,
413 details_commitment: account.to_commitment(),
414 });
415 }
416 }
417 },
418 }
419
420 Ok(account_update)
421 }
422
423 pub fn account_id(&self) -> AccountId {
425 self.account_id
426 }
427
428 pub fn initial_state_commitment(&self) -> Word {
430 self.init_state_commitment
431 }
432
433 pub fn final_state_commitment(&self) -> Word {
435 self.final_state_commitment
436 }
437
438 pub fn account_delta_commitment(&self) -> Word {
440 self.account_delta_commitment
441 }
442
443 pub fn details(&self) -> &AccountUpdateDetails {
448 &self.details
449 }
450
451 pub fn is_private(&self) -> bool {
453 self.details.is_private()
454 }
455}
456
457impl Serializable for TxAccountUpdate {
458 fn write_into<W: ByteWriter>(&self, target: &mut W) {
459 self.account_id.write_into(target);
460 self.init_state_commitment.write_into(target);
461 self.final_state_commitment.write_into(target);
462 self.account_delta_commitment.write_into(target);
463 self.details.write_into(target);
464 }
465}
466
467impl Deserializable for TxAccountUpdate {
468 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
469 let account_id = AccountId::read_from(source)?;
470 let init_state_commitment = Word::read_from(source)?;
471 let final_state_commitment = Word::read_from(source)?;
472 let account_delta_commitment = Word::read_from(source)?;
473 let details = AccountUpdateDetails::read_from(source)?;
474
475 Self::new(
476 account_id,
477 init_state_commitment,
478 final_state_commitment,
479 account_delta_commitment,
480 details,
481 )
482 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
483 }
484}
485
486#[derive(Debug, Clone, PartialEq, Eq)]
495pub struct InputNoteCommitment {
496 nullifier: Nullifier,
497 header: Option<NoteHeader>,
498}
499
500impl InputNoteCommitment {
501 pub fn from_parts_unchecked(nullifier: Nullifier, header: Option<NoteHeader>) -> Self {
508 Self { nullifier, header }
509 }
510
511 pub fn nullifier(&self) -> Nullifier {
513 self.nullifier
514 }
515
516 pub fn header(&self) -> Option<&NoteHeader> {
521 self.header.as_ref()
522 }
523
524 pub fn is_authenticated(&self) -> bool {
530 self.header.is_none()
531 }
532}
533
534impl From<InputNote> for InputNoteCommitment {
535 fn from(note: InputNote) -> Self {
536 Self::from(¬e)
537 }
538}
539
540impl From<&InputNote> for InputNoteCommitment {
541 fn from(note: &InputNote) -> Self {
542 match note {
543 InputNote::Authenticated { note, .. } => Self {
544 nullifier: note.nullifier(),
545 header: None,
546 },
547 InputNote::Unauthenticated { note } => Self {
548 nullifier: note.nullifier(),
549 header: Some(note.header().clone()),
550 },
551 }
552 }
553}
554
555impl From<Nullifier> for InputNoteCommitment {
556 fn from(nullifier: Nullifier) -> Self {
557 Self { nullifier, header: None }
558 }
559}
560
561impl ToInputNoteCommitments for InputNoteCommitment {
562 fn nullifier(&self) -> Nullifier {
563 self.nullifier
564 }
565
566 fn note_commitment(&self) -> Option<Word> {
567 self.header.as_ref().map(NoteHeader::commitment)
568 }
569}
570
571impl Serializable for InputNoteCommitment {
575 fn write_into<W: ByteWriter>(&self, target: &mut W) {
576 self.nullifier.write_into(target);
577 self.header.write_into(target);
578 }
579}
580
581impl Deserializable for InputNoteCommitment {
582 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
583 let nullifier = Nullifier::read_from(source)?;
584 let header = <Option<NoteHeader>>::read_from(source)?;
585
586 Ok(Self { nullifier, header })
587 }
588}
589
590#[cfg(test)]
594mod tests {
595 use alloc::collections::BTreeMap;
596 use alloc::vec::Vec;
597
598 use anyhow::Context;
599 use miden_crypto::rand::test_utils::rand_value;
600 use miden_verifier::ExecutionProof;
601
602 use super::ProvenTransaction;
603 use crate::account::delta::AccountUpdateDetails;
604 use crate::account::{
605 Account,
606 AccountDelta,
607 AccountId,
608 AccountIdVersion,
609 AccountStorageDelta,
610 AccountStorageMode,
611 AccountType,
612 AccountVaultDelta,
613 StorageMapDelta,
614 StorageMapKey,
615 StorageSlotName,
616 };
617 use crate::asset::FungibleAsset;
618 use crate::block::BlockNumber;
619 use crate::errors::ProvenTransactionError;
620 use crate::testing::account_id::{
621 ACCOUNT_ID_PRIVATE_SENDER,
622 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
623 };
624 use crate::testing::add_component::AddComponent;
625 use crate::testing::noop_auth_component::NoopAuthComponent;
626 use crate::transaction::{InputNoteCommitment, OutputNote, TxAccountUpdate};
627 use crate::utils::serde::{Deserializable, Serializable};
628 use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, LexicographicWord, ONE, Word};
629
630 fn check_if_sync<T: Sync>() {}
631 fn check_if_send<T: Send>() {}
632
633 #[test]
636 fn test_proven_transaction_is_sync() {
637 check_if_sync::<ProvenTransaction>();
638 }
639
640 #[test]
643 fn test_proven_transaction_is_send() {
644 check_if_send::<ProvenTransaction>();
645 }
646
647 #[test]
648 fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
649 let account = Account::builder([9; 32])
651 .account_type(AccountType::RegularAccountUpdatableCode)
652 .storage_mode(AccountStorageMode::Public)
653 .with_auth_component(NoopAuthComponent)
654 .with_component(AddComponent)
655 .build_existing()?;
656 let delta = AccountDelta::try_from(account.clone())?;
657
658 let details = AccountUpdateDetails::Delta(delta);
659
660 TxAccountUpdate::new(
661 account.id(),
662 account.to_commitment(),
663 account.to_commitment(),
664 Word::empty(),
665 details,
666 )?;
667
668 Ok(())
669 }
670
671 #[test]
672 fn account_update_size_limit_exceeded() {
673 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
674 let mut map = BTreeMap::new();
675 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
679 for _ in 0..required_entries {
680 map.insert(
681 LexicographicWord::new(StorageMapKey::from_raw(rand_value())),
682 rand_value::<Word>(),
683 );
684 }
685 let storage_delta = StorageMapDelta::new(map);
686
687 let storage_delta =
689 AccountStorageDelta::from_iters([], [], [(StorageSlotName::mock(4), storage_delta)]);
690 let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE)
691 .unwrap();
692 let details = AccountUpdateDetails::Delta(delta);
693 let details_size = details.get_size_hint();
694
695 let err = TxAccountUpdate::new(
696 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
697 EMPTY_WORD,
698 EMPTY_WORD,
699 EMPTY_WORD,
700 details,
701 )
702 .unwrap_err();
703
704 assert!(
705 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
706 );
707 }
708
709 #[test]
710 fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
711 let account_id = AccountId::dummy(
712 [1; 15],
713 AccountIdVersion::Version0,
714 AccountType::FungibleFaucet,
715 AccountStorageMode::Private,
716 );
717 let initial_account_commitment =
718 [2; 32].try_into().expect("failed to create initial account commitment");
719 let final_account_commitment =
720 [3; 32].try_into().expect("failed to create final account commitment");
721 let account_delta_commitment =
722 [4; 32].try_into().expect("failed to create account delta commitment");
723 let ref_block_num = BlockNumber::from(1);
724 let ref_block_commitment = Word::empty();
725 let expiration_block_num = BlockNumber::from(2);
726 let proof = ExecutionProof::new_dummy();
727
728 let account_update = TxAccountUpdate::new(
729 account_id,
730 initial_account_commitment,
731 final_account_commitment,
732 account_delta_commitment,
733 AccountUpdateDetails::Private,
734 )
735 .context("failed to build account update")?;
736
737 let tx = ProvenTransaction::new(
738 account_update,
739 Vec::<InputNoteCommitment>::new(),
740 Vec::<OutputNote>::new(),
741 ref_block_num,
742 ref_block_commitment,
743 FungibleAsset::mock(42).unwrap_fungible(),
744 expiration_block_num,
745 proof,
746 )
747 .context("failed to build proven transaction")?;
748
749 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
750
751 assert_eq!(tx, deserialized);
752
753 Ok(())
754 }
755}