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, 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;
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(
94 account_update: TxAccountUpdate,
95 input_notes: impl IntoIterator<Item = impl Into<InputNoteCommitment>>,
96 output_notes: impl IntoIterator<Item = impl Into<OutputNote>>,
97 ref_block_num: BlockNumber,
98 ref_block_commitment: Word,
99 fee: FungibleAsset,
100 expiration_block_num: BlockNumber,
101 proof: ExecutionProof,
102 ) -> Result<Self, ProvenTransactionError> {
103 let input_notes: Vec<InputNoteCommitment> =
104 input_notes.into_iter().map(Into::into).collect();
105 let output_notes: Vec<OutputNote> = output_notes.into_iter().map(Into::into).collect();
106
107 let input_notes =
108 InputNotes::new(input_notes).map_err(ProvenTransactionError::InputNotesError)?;
109 let output_notes =
110 OutputNotes::new(output_notes).map_err(ProvenTransactionError::OutputNotesError)?;
111
112 for input_note in input_notes.iter().filter_map(InputNoteCommitment::header) {
117 if output_notes.iter().any(|output_note| output_note.id() == input_note.id()) {
118 return Err(ProvenTransactionError::NoteCreatedAndConsumed(input_note.id()));
119 }
120 }
121
122 let id = TransactionId::new(
123 account_update.initial_state_commitment(),
124 account_update.final_state_commitment(),
125 input_notes.commitment(),
126 output_notes.commitment(),
127 fee,
128 );
129
130 let proven_transaction = Self {
131 id,
132 account_update,
133 input_notes,
134 output_notes,
135 ref_block_num,
136 ref_block_commitment,
137 fee,
138 expiration_block_num,
139 proof,
140 };
141
142 proven_transaction.validate()
143 }
144
145 pub fn id(&self) -> TransactionId {
150 self.id
151 }
152
153 pub fn account_id(&self) -> AccountId {
155 self.account_update.account_id()
156 }
157
158 pub fn account_update(&self) -> &TxAccountUpdate {
160 &self.account_update
161 }
162
163 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
165 &self.input_notes
166 }
167
168 pub fn output_notes(&self) -> &OutputNotes {
170 &self.output_notes
171 }
172
173 pub fn proof(&self) -> &ExecutionProof {
175 &self.proof
176 }
177
178 pub fn ref_block_num(&self) -> BlockNumber {
180 self.ref_block_num
181 }
182
183 pub fn ref_block_commitment(&self) -> Word {
185 self.ref_block_commitment
186 }
187
188 pub fn fee(&self) -> FungibleAsset {
190 self.fee
191 }
192
193 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
195 self.input_notes.iter().filter_map(|note| note.header())
196 }
197
198 pub fn expiration_block_num(&self) -> BlockNumber {
200 self.expiration_block_num
201 }
202
203 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
207 self.input_notes.iter().map(InputNoteCommitment::nullifier)
208 }
209
210 fn validate(mut self) -> Result<Self, ProvenTransactionError> {
223 if self.account_update.initial_state_commitment()
226 == self.account_update.final_state_commitment()
227 && self.input_notes.commitment().is_empty()
228 {
229 return Err(ProvenTransactionError::EmptyTransaction);
230 }
231
232 match &mut self.account_update.details {
233 AccountUpdateDetails::Private => (),
236 AccountUpdateDetails::Delta(post_fee_account_delta) => {
237 post_fee_account_delta.vault_mut().add_asset(self.fee.into()).map_err(|err| {
240 ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err))
241 })?;
242
243 let expected_commitment = self.account_update.account_delta_commitment;
244 let actual_commitment = post_fee_account_delta.to_commitment();
245 if expected_commitment != actual_commitment {
246 return Err(ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(
247 format!(
248 "expected account delta commitment {expected_commitment} but found {actual_commitment}"
249 ),
250 )));
251 }
252
253 post_fee_account_delta.vault_mut().remove_asset(self.fee.into()).map_err(
255 |err| ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err)),
256 )?;
257 },
258 }
259
260 Ok(self)
261 }
262}
263
264impl Serializable for ProvenTransaction {
265 fn write_into<W: ByteWriter>(&self, target: &mut W) {
266 self.account_update.write_into(target);
267 self.input_notes.write_into(target);
268 self.output_notes.write_into(target);
269 self.ref_block_num.write_into(target);
270 self.ref_block_commitment.write_into(target);
271 self.fee.write_into(target);
272 self.expiration_block_num.write_into(target);
273 self.proof.write_into(target);
274 }
275}
276
277impl Deserializable for ProvenTransaction {
278 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
279 let account_update = TxAccountUpdate::read_from(source)?;
280
281 let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
282 let output_notes = OutputNotes::read_from(source)?;
283
284 let ref_block_num = BlockNumber::read_from(source)?;
285 let ref_block_commitment = Word::read_from(source)?;
286 let fee = FungibleAsset::read_from(source)?;
287 let expiration_block_num = BlockNumber::read_from(source)?;
288 let proof = ExecutionProof::read_from(source)?;
289
290 let id = TransactionId::new(
291 account_update.initial_state_commitment(),
292 account_update.final_state_commitment(),
293 input_notes.commitment(),
294 output_notes.commitment(),
295 fee,
296 );
297
298 let proven_transaction = Self {
299 id,
300 account_update,
301 input_notes,
302 output_notes,
303 ref_block_num,
304 ref_block_commitment,
305 fee,
306 expiration_block_num,
307 proof,
308 };
309
310 proven_transaction
311 .validate()
312 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct TxAccountUpdate {
322 account_id: AccountId,
324
325 init_state_commitment: Word,
329
330 final_state_commitment: Word,
332
333 account_delta_commitment: Word,
343
344 details: AccountUpdateDetails,
348}
349
350impl TxAccountUpdate {
351 pub fn new(
366 account_id: AccountId,
367 init_state_commitment: Word,
368 final_state_commitment: Word,
369 account_delta_commitment: Word,
370 details: AccountUpdateDetails,
371 ) -> Result<Self, ProvenTransactionError> {
372 let account_update = Self {
373 account_id,
374 init_state_commitment,
375 final_state_commitment,
376 account_delta_commitment,
377 details,
378 };
379
380 let account_update_size = account_update.details.get_size_hint();
381 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
382 return Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
383 account_id,
384 update_size: account_update_size,
385 });
386 }
387
388 if account_id.is_private() {
389 if account_update.details.is_private() {
390 return Ok(account_update);
391 } else {
392 return Err(ProvenTransactionError::PrivateAccountWithDetails(account_id));
393 }
394 }
395
396 match account_update.details() {
397 AccountUpdateDetails::Private => {
398 return Err(ProvenTransactionError::PublicStateAccountMissingDetails(
399 account_update.account_id(),
400 ));
401 },
402 AccountUpdateDetails::Delta(delta) => {
403 let is_new_account = account_update.initial_state_commitment().is_empty();
404 if is_new_account {
405 let account = Account::try_from(delta).map_err(|err| {
408 ProvenTransactionError::NewPublicStateAccountRequiresFullStateDelta {
409 id: delta.id(),
410 source: err,
411 }
412 })?;
413
414 if account.id() != account_id {
415 return Err(ProvenTransactionError::AccountIdMismatch {
416 tx_account_id: account_id,
417 details_account_id: account.id(),
418 });
419 }
420
421 if account.to_commitment() != account_update.final_state_commitment {
422 return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
423 tx_final_commitment: account_update.final_state_commitment,
424 details_commitment: account.to_commitment(),
425 });
426 }
427 }
428 },
429 }
430
431 Ok(account_update)
432 }
433
434 pub fn account_id(&self) -> AccountId {
436 self.account_id
437 }
438
439 pub fn initial_state_commitment(&self) -> Word {
441 self.init_state_commitment
442 }
443
444 pub fn final_state_commitment(&self) -> Word {
446 self.final_state_commitment
447 }
448
449 pub fn account_delta_commitment(&self) -> Word {
451 self.account_delta_commitment
452 }
453
454 pub fn details(&self) -> &AccountUpdateDetails {
459 &self.details
460 }
461
462 pub fn is_private(&self) -> bool {
464 self.details.is_private()
465 }
466}
467
468impl Serializable for TxAccountUpdate {
469 fn write_into<W: ByteWriter>(&self, target: &mut W) {
470 self.account_id.write_into(target);
471 self.init_state_commitment.write_into(target);
472 self.final_state_commitment.write_into(target);
473 self.account_delta_commitment.write_into(target);
474 self.details.write_into(target);
475 }
476}
477
478impl Deserializable for TxAccountUpdate {
479 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
480 let account_id = AccountId::read_from(source)?;
481 let init_state_commitment = Word::read_from(source)?;
482 let final_state_commitment = Word::read_from(source)?;
483 let account_delta_commitment = Word::read_from(source)?;
484 let details = AccountUpdateDetails::read_from(source)?;
485
486 Self::new(
487 account_id,
488 init_state_commitment,
489 final_state_commitment,
490 account_delta_commitment,
491 details,
492 )
493 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
494 }
495}
496
497#[derive(Debug, Clone, PartialEq, Eq)]
506pub struct InputNoteCommitment {
507 nullifier: Nullifier,
508 header: Option<NoteHeader>,
509}
510
511impl InputNoteCommitment {
512 pub fn from_parts_unchecked(nullifier: Nullifier, header: Option<NoteHeader>) -> Self {
519 Self { nullifier, header }
520 }
521
522 pub fn nullifier(&self) -> Nullifier {
524 self.nullifier
525 }
526
527 pub fn header(&self) -> Option<&NoteHeader> {
532 self.header.as_ref()
533 }
534
535 pub fn is_authenticated(&self) -> bool {
541 self.header.is_none()
542 }
543}
544
545impl From<InputNote> for InputNoteCommitment {
546 fn from(note: InputNote) -> Self {
547 Self::from(¬e)
548 }
549}
550
551impl From<&InputNote> for InputNoteCommitment {
552 fn from(note: &InputNote) -> Self {
553 match note {
554 InputNote::Authenticated { note, .. } => Self {
555 nullifier: note.nullifier(),
556 header: None,
557 },
558 InputNote::Unauthenticated { note } => Self {
559 nullifier: note.nullifier(),
560 header: Some(*note.header()),
561 },
562 }
563 }
564}
565
566impl From<Nullifier> for InputNoteCommitment {
567 fn from(nullifier: Nullifier) -> Self {
568 Self { nullifier, header: None }
569 }
570}
571
572impl ToInputNoteCommitments for InputNoteCommitment {
573 fn nullifier(&self) -> Nullifier {
574 self.nullifier
575 }
576
577 fn note_id(&self) -> Option<NoteId> {
578 self.header.as_ref().map(NoteHeader::id)
579 }
580}
581
582impl Serializable for InputNoteCommitment {
586 fn write_into<W: ByteWriter>(&self, target: &mut W) {
587 self.nullifier.write_into(target);
588 self.header.write_into(target);
589 }
590}
591
592impl Deserializable for InputNoteCommitment {
593 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
594 let nullifier = Nullifier::read_from(source)?;
595 let header = <Option<NoteHeader>>::read_from(source)?;
596
597 Ok(Self::from_parts_unchecked(nullifier, header))
598 }
599}
600
601#[cfg(test)]
605mod tests {
606 use alloc::collections::BTreeMap;
607 use alloc::vec::Vec;
608
609 use anyhow::Context;
610 use miden_crypto::rand::test_utils::rand_value;
611 use miden_verifier::ExecutionProof;
612
613 use super::ProvenTransaction;
614 use crate::account::delta::AccountUpdateDetails;
615 use crate::account::{
616 Account,
617 AccountDelta,
618 AccountId,
619 AccountIdVersion,
620 AccountStorageDelta,
621 AccountType,
622 AccountVaultDelta,
623 StorageMapDelta,
624 StorageMapKey,
625 StorageSlotName,
626 };
627 use crate::asset::FungibleAsset;
628 use crate::block::BlockNumber;
629 use crate::errors::ProvenTransactionError;
630 use crate::testing::account_id::{
631 ACCOUNT_ID_PRIVATE_SENDER,
632 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
633 };
634 use crate::testing::add_component::AddComponent;
635 use crate::testing::noop_auth_component::NoopAuthComponent;
636 use crate::transaction::{InputNoteCommitment, OutputNote, TxAccountUpdate};
637 use crate::utils::serde::{Deserializable, Serializable};
638 use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, ONE, Word};
639
640 fn check_if_sync<T: Sync>() {}
641 fn check_if_send<T: Send>() {}
642
643 #[test]
646 fn test_proven_transaction_is_sync() {
647 check_if_sync::<ProvenTransaction>();
648 }
649
650 #[test]
653 fn test_proven_transaction_is_send() {
654 check_if_send::<ProvenTransaction>();
655 }
656
657 #[test]
658 fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
659 let account = Account::builder([9; 32])
661 .account_type(AccountType::Public)
662 .with_auth_component(NoopAuthComponent)
663 .with_component(AddComponent)
664 .build_existing()?;
665 let delta = AccountDelta::try_from(account.clone())?;
666
667 let details = AccountUpdateDetails::Delta(delta);
668
669 TxAccountUpdate::new(
670 account.id(),
671 account.to_commitment(),
672 account.to_commitment(),
673 Word::empty(),
674 details,
675 )?;
676
677 Ok(())
678 }
679
680 #[test]
681 fn account_update_size_limit_exceeded() {
682 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
683 let mut map = BTreeMap::new();
684 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
688 for _ in 0..required_entries {
689 map.insert(StorageMapKey::from_raw(rand_value()), rand_value::<Word>());
690 }
691 let storage_delta = StorageMapDelta::new(map);
692
693 let storage_delta =
695 AccountStorageDelta::from_iters([], [], [(StorageSlotName::mock(4), storage_delta)]);
696 let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE)
697 .unwrap();
698 let details = AccountUpdateDetails::Delta(delta);
699 let details_size = details.get_size_hint();
700
701 let err = TxAccountUpdate::new(
702 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
703 EMPTY_WORD,
704 EMPTY_WORD,
705 EMPTY_WORD,
706 details,
707 )
708 .unwrap_err();
709
710 assert!(
711 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
712 );
713 }
714
715 #[test]
716 fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
717 let account_id =
718 AccountId::dummy([1; 15], AccountIdVersion::Version1, AccountType::Private);
719 let initial_account_commitment =
720 [2; 32].try_into().expect("failed to create initial account commitment");
721 let final_account_commitment =
722 [3; 32].try_into().expect("failed to create final account commitment");
723 let account_delta_commitment =
724 [4; 32].try_into().expect("failed to create account delta commitment");
725 let ref_block_num = BlockNumber::from(1);
726 let ref_block_commitment = Word::empty();
727 let expiration_block_num = BlockNumber::from(2);
728 let proof = ExecutionProof::new_dummy();
729
730 let account_update = TxAccountUpdate::new(
731 account_id,
732 initial_account_commitment,
733 final_account_commitment,
734 account_delta_commitment,
735 AccountUpdateDetails::Private,
736 )
737 .context("failed to build account update")?;
738
739 let tx = ProvenTransaction::new(
740 account_update,
741 Vec::<InputNoteCommitment>::new(),
742 Vec::<OutputNote>::new(),
743 ref_block_num,
744 ref_block_commitment,
745 FungibleAsset::mock(42).unwrap_fungible(),
746 expiration_block_num,
747 proof,
748 )
749 .context("failed to build proven transaction")?;
750
751 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
752
753 assert_eq!(tx, deserialized);
754
755 Ok(())
756 }
757}