1use alloc::{string::ToString, vec::Vec};
2
3use super::{InputNote, ToInputNoteCommitments};
4use crate::{
5 ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, ProvenTransactionError,
6 account::delta::AccountUpdateDetails,
7 block::BlockNumber,
8 note::NoteHeader,
9 transaction::{
10 AccountId, Digest, InputNotes, Nullifier, OutputNote, OutputNotes, TransactionId,
11 },
12 utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
13 vm::ExecutionProof,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ProvenTransaction {
30 id: TransactionId,
32
33 account_update: TxAccountUpdate,
35
36 input_notes: InputNotes<InputNoteCommitment>,
38
39 output_notes: OutputNotes,
42
43 ref_block_num: BlockNumber,
45
46 ref_block_commitment: Digest,
48
49 expiration_block_num: BlockNumber,
51
52 proof: ExecutionProof,
54}
55
56impl ProvenTransaction {
57 pub fn id(&self) -> TransactionId {
59 self.id
60 }
61
62 pub fn account_id(&self) -> AccountId {
64 self.account_update.account_id()
65 }
66
67 pub fn account_update(&self) -> &TxAccountUpdate {
69 &self.account_update
70 }
71
72 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
74 &self.input_notes
75 }
76
77 pub fn output_notes(&self) -> &OutputNotes {
79 &self.output_notes
80 }
81
82 pub fn proof(&self) -> &ExecutionProof {
84 &self.proof
85 }
86
87 pub fn ref_block_num(&self) -> BlockNumber {
89 self.ref_block_num
90 }
91
92 pub fn ref_block_commitment(&self) -> Digest {
94 self.ref_block_commitment
95 }
96
97 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
99 self.input_notes.iter().filter_map(|note| note.header())
100 }
101
102 pub fn expiration_block_num(&self) -> BlockNumber {
104 self.expiration_block_num
105 }
106
107 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
111 self.input_notes.iter().map(InputNoteCommitment::nullifier)
112 }
113
114 fn validate(self) -> Result<Self, ProvenTransactionError> {
138 if self.account_id().is_onchain() {
140 self.account_update.validate()?;
141
142 if self.account_update.initial_state_commitment()
145 == self.account_update.final_state_commitment()
146 && *self.input_notes.commitment() == EMPTY_WORD
147 {
148 return Err(ProvenTransactionError::EmptyTransaction);
149 }
150
151 let is_new_account =
152 self.account_update.initial_state_commitment() == Digest::default();
153 match self.account_update.details() {
154 AccountUpdateDetails::Private => {
155 return Err(ProvenTransactionError::OnChainAccountMissingDetails(
156 self.account_id(),
157 ));
158 },
159 AccountUpdateDetails::New(account) => {
160 if !is_new_account {
161 return Err(
162 ProvenTransactionError::ExistingOnChainAccountRequiresDeltaDetails(
163 self.account_id(),
164 ),
165 );
166 }
167 if account.id() != self.account_id() {
168 return Err(ProvenTransactionError::AccountIdMismatch {
169 tx_account_id: self.account_id(),
170 details_account_id: account.id(),
171 });
172 }
173 if account.commitment() != self.account_update.final_state_commitment() {
174 return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
175 tx_final_commitment: self.account_update.final_state_commitment(),
176 details_commitment: account.commitment(),
177 });
178 }
179 },
180 AccountUpdateDetails::Delta(_) => {
181 if is_new_account {
182 return Err(ProvenTransactionError::NewOnChainAccountRequiresFullDetails(
183 self.account_id(),
184 ));
185 }
186 },
187 }
188 } else if !self.account_update.is_private() {
189 return Err(ProvenTransactionError::PrivateAccountWithDetails(self.account_id()));
190 }
191
192 Ok(self)
193 }
194}
195
196impl Serializable for ProvenTransaction {
197 fn write_into<W: ByteWriter>(&self, target: &mut W) {
198 self.account_update.write_into(target);
199 self.input_notes.write_into(target);
200 self.output_notes.write_into(target);
201 self.ref_block_num.write_into(target);
202 self.ref_block_commitment.write_into(target);
203 self.expiration_block_num.write_into(target);
204 self.proof.write_into(target);
205 }
206}
207
208impl Deserializable for ProvenTransaction {
209 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
210 let account_update = TxAccountUpdate::read_from(source)?;
211
212 let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
213 let output_notes = OutputNotes::read_from(source)?;
214
215 let ref_block_num = BlockNumber::read_from(source)?;
216 let ref_block_commitment = Digest::read_from(source)?;
217 let expiration_block_num = BlockNumber::read_from(source)?;
218 let proof = ExecutionProof::read_from(source)?;
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 let proven_transaction = 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 proven_transaction
239 .validate()
240 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
241 }
242}
243
244#[derive(Clone, Debug)]
249pub struct ProvenTransactionBuilder {
250 account_id: AccountId,
252
253 initial_account_commitment: Digest,
255
256 final_account_commitment: Digest,
258
259 account_delta_commitment: Digest,
261
262 account_update_details: AccountUpdateDetails,
264
265 input_notes: Vec<InputNoteCommitment>,
267
268 output_notes: Vec<OutputNote>,
270
271 ref_block_num: BlockNumber,
273
274 ref_block_commitment: Digest,
276
277 expiration_block_num: BlockNumber,
279
280 proof: ExecutionProof,
282}
283
284impl ProvenTransactionBuilder {
285 pub fn new(
290 account_id: AccountId,
291 initial_account_commitment: Digest,
292 final_account_commitment: Digest,
293 account_delta_commitment: Digest,
294 ref_block_num: BlockNumber,
295 ref_block_commitment: Digest,
296 expiration_block_num: BlockNumber,
297 proof: ExecutionProof,
298 ) -> Self {
299 Self {
300 account_id,
301 initial_account_commitment,
302 final_account_commitment,
303 account_delta_commitment,
304 account_update_details: AccountUpdateDetails::Private,
305 input_notes: Vec::new(),
306 output_notes: Vec::new(),
307 ref_block_num,
308 ref_block_commitment,
309 expiration_block_num,
310 proof,
311 }
312 }
313
314 pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
319 self.account_update_details = details;
320 self
321 }
322
323 pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
325 where
326 I: IntoIterator<Item = T>,
327 T: Into<InputNoteCommitment>,
328 {
329 self.input_notes.extend(notes.into_iter().map(|note| note.into()));
330 self
331 }
332
333 pub fn add_output_notes<T>(mut self, notes: T) -> Self
335 where
336 T: IntoIterator<Item = OutputNote>,
337 {
338 self.output_notes.extend(notes);
339 self
340 }
341
342 pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
369 let input_notes =
370 InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
371 let output_notes = OutputNotes::new(self.output_notes)
372 .map_err(ProvenTransactionError::OutputNotesError)?;
373 let id = TransactionId::new(
374 self.initial_account_commitment,
375 self.final_account_commitment,
376 input_notes.commitment(),
377 output_notes.commitment(),
378 );
379 let account_update = TxAccountUpdate::new(
380 self.account_id,
381 self.initial_account_commitment,
382 self.final_account_commitment,
383 self.account_delta_commitment,
384 self.account_update_details,
385 );
386
387 let proven_transaction = ProvenTransaction {
388 id,
389 account_update,
390 input_notes,
391 output_notes,
392 ref_block_num: self.ref_block_num,
393 ref_block_commitment: self.ref_block_commitment,
394 expiration_block_num: self.expiration_block_num,
395 proof: self.proof,
396 };
397
398 proven_transaction.validate()
399 }
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
407pub struct TxAccountUpdate {
408 account_id: AccountId,
410
411 init_state_commitment: Digest,
415
416 final_state_commitment: Digest,
418
419 account_delta_commitment: Digest,
421
422 details: AccountUpdateDetails,
426}
427
428impl TxAccountUpdate {
429 pub const fn new(
431 account_id: AccountId,
432 init_state_commitment: Digest,
433 final_state_commitment: Digest,
434 account_delta_commitment: Digest,
435 details: AccountUpdateDetails,
436 ) -> Self {
437 Self {
438 account_id,
439 init_state_commitment,
440 final_state_commitment,
441 account_delta_commitment,
442 details,
443 }
444 }
445
446 pub fn account_id(&self) -> AccountId {
448 self.account_id
449 }
450
451 pub fn initial_state_commitment(&self) -> Digest {
453 self.init_state_commitment
454 }
455
456 pub fn final_state_commitment(&self) -> Digest {
458 self.final_state_commitment
459 }
460
461 pub fn account_delta_commitment(&self) -> Digest {
463 self.account_delta_commitment
464 }
465
466 pub fn details(&self) -> &AccountUpdateDetails {
471 &self.details
472 }
473
474 pub fn is_private(&self) -> bool {
476 self.details.is_private()
477 }
478
479 pub fn validate(&self) -> Result<(), ProvenTransactionError> {
483 let account_update_size = self.details().get_size_hint();
484 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
485 Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
486 account_id: self.account_id(),
487 update_size: account_update_size,
488 })
489 } else {
490 Ok(())
491 }
492 }
493}
494
495impl Serializable for TxAccountUpdate {
496 fn write_into<W: ByteWriter>(&self, target: &mut W) {
497 self.account_id.write_into(target);
498 self.init_state_commitment.write_into(target);
499 self.final_state_commitment.write_into(target);
500 self.account_delta_commitment.write_into(target);
501 self.details.write_into(target);
502 }
503}
504
505impl Deserializable for TxAccountUpdate {
506 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
507 Ok(Self {
508 account_id: AccountId::read_from(source)?,
509 init_state_commitment: Digest::read_from(source)?,
510 final_state_commitment: Digest::read_from(source)?,
511 account_delta_commitment: Digest::read_from(source)?,
512 details: AccountUpdateDetails::read_from(source)?,
513 })
514 }
515}
516
517#[derive(Debug, Clone, PartialEq, Eq)]
526pub struct InputNoteCommitment {
527 nullifier: Nullifier,
528 header: Option<NoteHeader>,
529}
530
531impl InputNoteCommitment {
532 pub fn nullifier(&self) -> Nullifier {
534 self.nullifier
535 }
536
537 pub fn header(&self) -> Option<&NoteHeader> {
542 self.header.as_ref()
543 }
544
545 pub fn is_authenticated(&self) -> bool {
551 self.header.is_none()
552 }
553}
554
555impl From<InputNote> for InputNoteCommitment {
556 fn from(note: InputNote) -> Self {
557 Self::from(¬e)
558 }
559}
560
561impl From<&InputNote> for InputNoteCommitment {
562 fn from(note: &InputNote) -> Self {
563 match note {
564 InputNote::Authenticated { note, .. } => Self {
565 nullifier: note.nullifier(),
566 header: None,
567 },
568 InputNote::Unauthenticated { note } => Self {
569 nullifier: note.nullifier(),
570 header: Some(*note.header()),
571 },
572 }
573 }
574}
575
576impl From<Nullifier> for InputNoteCommitment {
577 fn from(nullifier: Nullifier) -> Self {
578 Self { nullifier, header: None }
579 }
580}
581
582impl ToInputNoteCommitments for InputNoteCommitment {
583 fn nullifier(&self) -> Nullifier {
584 self.nullifier
585 }
586
587 fn note_commitment(&self) -> Option<Digest> {
588 self.header.map(|header| header.commitment())
589 }
590}
591
592impl Serializable for InputNoteCommitment {
596 fn write_into<W: ByteWriter>(&self, target: &mut W) {
597 self.nullifier.write_into(target);
598 self.header.write_into(target);
599 }
600}
601
602impl Deserializable for InputNoteCommitment {
603 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
604 let nullifier = Nullifier::read_from(source)?;
605 let header = <Option<NoteHeader>>::read_from(source)?;
606
607 Ok(Self { nullifier, header })
608 }
609}
610
611#[cfg(test)]
615mod tests {
616 use alloc::collections::BTreeMap;
617
618 use anyhow::Context;
619 use miden_verifier::ExecutionProof;
620 use vm_core::utils::Deserializable;
621 use winter_air::proof::Proof;
622 use winter_rand_utils::rand_array;
623
624 use super::ProvenTransaction;
625 use crate::{
626 ACCOUNT_UPDATE_MAX_SIZE, Digest, EMPTY_WORD, ONE, ProvenTransactionError, ZERO,
627 account::{
628 AccountDelta, AccountId, AccountIdVersion, AccountStorageDelta, AccountStorageMode,
629 AccountType, AccountVaultDelta, StorageMapDelta,
630 delta::{AccountUpdateDetails, LexicographicWord},
631 },
632 block::BlockNumber,
633 testing::account_id::{
634 ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
635 },
636 transaction::{ProvenTransactionBuilder, TxAccountUpdate},
637 utils::Serializable,
638 };
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() {
659 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
661 let storage_delta = AccountStorageDelta::from_iters(
662 [1, 2, 3, 4],
663 [(2, [ONE, ONE, ONE, ONE]), (3, [ONE, ONE, ZERO, ONE])],
664 [],
665 );
666 let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE)
667 .unwrap();
668 let details = AccountUpdateDetails::Delta(delta);
669 TxAccountUpdate::new(
670 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
671 Digest::new(EMPTY_WORD),
672 Digest::new(EMPTY_WORD),
673 Digest::new(EMPTY_WORD),
674 details,
675 )
676 .validate()
677 .unwrap();
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(LexicographicWord::new(Digest::new(rand_array())), rand_array());
690 }
691 let storage_delta = StorageMapDelta::new(map);
692
693 let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
695 let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE)
696 .unwrap();
697 let details = AccountUpdateDetails::Delta(delta);
698 let details_size = details.get_size_hint();
699
700 let err = TxAccountUpdate::new(
701 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
702 Digest::new(EMPTY_WORD),
703 Digest::new(EMPTY_WORD),
704 Digest::new(EMPTY_WORD),
705 details,
706 )
707 .validate()
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 = AccountId::dummy(
718 [1; 15],
719 AccountIdVersion::Version0,
720 AccountType::FungibleFaucet,
721 AccountStorageMode::Private,
722 );
723 let initial_account_commitment =
724 [2; 32].try_into().expect("failed to create initial account commitment");
725 let final_account_commitment =
726 [3; 32].try_into().expect("failed to create final account commitment");
727 let account_delta_commitment =
728 [4; 32].try_into().expect("failed to create account delta commitment");
729 let ref_block_num = BlockNumber::from(1);
730 let ref_block_commitment = Digest::default();
731 let expiration_block_num = BlockNumber::from(2);
732 let proof = ExecutionProof::new(Proof::new_dummy(), Default::default());
733
734 let tx = ProvenTransactionBuilder::new(
735 account_id,
736 initial_account_commitment,
737 final_account_commitment,
738 account_delta_commitment,
739 ref_block_num,
740 ref_block_commitment,
741 expiration_block_num,
742 proof,
743 )
744 .build()
745 .context("failed to build proven transaction")?;
746
747 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
748
749 assert_eq!(tx, deserialized);
750
751 Ok(())
752 }
753}