1use alloc::{string::ToString, vec::Vec};
2
3use super::{InputNote, ToInputNoteCommitments};
4use crate::{
5 ACCOUNT_UPDATE_MAX_SIZE, 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)]
22pub struct ProvenTransaction {
23 id: TransactionId,
25
26 account_update: TxAccountUpdate,
28
29 input_notes: InputNotes<InputNoteCommitment>,
31
32 output_notes: OutputNotes,
35
36 ref_block_num: BlockNumber,
38
39 ref_block_commitment: Digest,
41
42 expiration_block_num: BlockNumber,
44
45 proof: ExecutionProof,
47}
48
49impl ProvenTransaction {
50 pub fn id(&self) -> TransactionId {
52 self.id
53 }
54
55 pub fn account_id(&self) -> AccountId {
57 self.account_update.account_id()
58 }
59
60 pub fn account_update(&self) -> &TxAccountUpdate {
62 &self.account_update
63 }
64
65 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
67 &self.input_notes
68 }
69
70 pub fn output_notes(&self) -> &OutputNotes {
72 &self.output_notes
73 }
74
75 pub fn proof(&self) -> &ExecutionProof {
77 &self.proof
78 }
79
80 pub fn ref_block_num(&self) -> BlockNumber {
82 self.ref_block_num
83 }
84
85 pub fn ref_block_commitment(&self) -> Digest {
87 self.ref_block_commitment
88 }
89
90 pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
92 self.input_notes.iter().filter_map(|note| note.header())
93 }
94
95 pub fn expiration_block_num(&self) -> BlockNumber {
97 self.expiration_block_num
98 }
99
100 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
104 self.input_notes.iter().map(InputNoteCommitment::nullifier)
105 }
106
107 fn validate(self) -> Result<Self, ProvenTransactionError> {
129 if self.account_id().is_onchain() {
131 self.account_update.validate()?;
132
133 let is_new_account =
134 self.account_update.initial_state_commitment() == Digest::default();
135 match self.account_update.details() {
136 AccountUpdateDetails::Private => {
137 return Err(ProvenTransactionError::OnChainAccountMissingDetails(
138 self.account_id(),
139 ));
140 },
141 AccountUpdateDetails::New(account) => {
142 if !is_new_account {
143 return Err(
144 ProvenTransactionError::ExistingOnChainAccountRequiresDeltaDetails(
145 self.account_id(),
146 ),
147 );
148 }
149 if account.id() != self.account_id() {
150 return Err(ProvenTransactionError::AccountIdMismatch {
151 tx_account_id: self.account_id(),
152 details_account_id: account.id(),
153 });
154 }
155 if account.commitment() != self.account_update.final_state_commitment() {
156 return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
157 tx_final_commitment: self.account_update.final_state_commitment(),
158 details_commitment: account.commitment(),
159 });
160 }
161 },
162 AccountUpdateDetails::Delta(_) => {
163 if is_new_account {
164 return Err(ProvenTransactionError::NewOnChainAccountRequiresFullDetails(
165 self.account_id(),
166 ));
167 }
168 },
169 }
170 } else if !self.account_update.is_private() {
171 return Err(ProvenTransactionError::PrivateAccountWithDetails(self.account_id()));
172 }
173
174 Ok(self)
175 }
176}
177
178impl Serializable for ProvenTransaction {
179 fn write_into<W: ByteWriter>(&self, target: &mut W) {
180 self.account_update.write_into(target);
181 self.input_notes.write_into(target);
182 self.output_notes.write_into(target);
183 self.ref_block_num.write_into(target);
184 self.ref_block_commitment.write_into(target);
185 self.expiration_block_num.write_into(target);
186 self.proof.write_into(target);
187 }
188}
189
190impl Deserializable for ProvenTransaction {
191 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
192 let account_update = TxAccountUpdate::read_from(source)?;
193
194 let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
195 let output_notes = OutputNotes::read_from(source)?;
196
197 let ref_block_num = BlockNumber::read_from(source)?;
198 let ref_block_commitment = Digest::read_from(source)?;
199 let expiration_block_num = BlockNumber::read_from(source)?;
200 let proof = ExecutionProof::read_from(source)?;
201
202 let id = TransactionId::new(
203 account_update.initial_state_commitment(),
204 account_update.final_state_commitment(),
205 input_notes.commitment(),
206 output_notes.commitment(),
207 );
208
209 let proven_transaction = Self {
210 id,
211 account_update,
212 input_notes,
213 output_notes,
214 ref_block_num,
215 ref_block_commitment,
216 expiration_block_num,
217 proof,
218 };
219
220 proven_transaction
221 .validate()
222 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
223 }
224}
225
226#[derive(Clone, Debug)]
231pub struct ProvenTransactionBuilder {
232 account_id: AccountId,
234
235 initial_account_commitment: Digest,
237
238 final_account_commitment: Digest,
240
241 account_update_details: AccountUpdateDetails,
243
244 input_notes: Vec<InputNoteCommitment>,
246
247 output_notes: Vec<OutputNote>,
249
250 ref_block_num: BlockNumber,
252
253 ref_block_commitment: Digest,
255
256 expiration_block_num: BlockNumber,
258
259 proof: ExecutionProof,
261}
262
263impl ProvenTransactionBuilder {
264 pub fn new(
269 account_id: AccountId,
270 initial_account_commitment: Digest,
271 final_account_commitment: Digest,
272 ref_block_num: BlockNumber,
273 ref_block_commitment: Digest,
274 expiration_block_num: BlockNumber,
275 proof: ExecutionProof,
276 ) -> Self {
277 Self {
278 account_id,
279 initial_account_commitment,
280 final_account_commitment,
281 account_update_details: AccountUpdateDetails::Private,
282 input_notes: Vec::new(),
283 output_notes: Vec::new(),
284 ref_block_num,
285 ref_block_commitment,
286 expiration_block_num,
287 proof,
288 }
289 }
290
291 pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
296 self.account_update_details = details;
297 self
298 }
299
300 pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
302 where
303 I: IntoIterator<Item = T>,
304 T: Into<InputNoteCommitment>,
305 {
306 self.input_notes.extend(notes.into_iter().map(|note| note.into()));
307 self
308 }
309
310 pub fn add_output_notes<T>(mut self, notes: T) -> Self
312 where
313 T: IntoIterator<Item = OutputNote>,
314 {
315 self.output_notes.extend(notes);
316 self
317 }
318
319 pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
344 let input_notes =
345 InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
346 let output_notes = OutputNotes::new(self.output_notes)
347 .map_err(ProvenTransactionError::OutputNotesError)?;
348 let id = TransactionId::new(
349 self.initial_account_commitment,
350 self.final_account_commitment,
351 input_notes.commitment(),
352 output_notes.commitment(),
353 );
354 let account_update = TxAccountUpdate::new(
355 self.account_id,
356 self.initial_account_commitment,
357 self.final_account_commitment,
358 self.account_update_details,
359 );
360
361 let proven_transaction = ProvenTransaction {
362 id,
363 account_update,
364 input_notes,
365 output_notes,
366 ref_block_num: self.ref_block_num,
367 ref_block_commitment: self.ref_block_commitment,
368 expiration_block_num: self.expiration_block_num,
369 proof: self.proof,
370 };
371
372 proven_transaction.validate()
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct TxAccountUpdate {
382 account_id: AccountId,
384
385 init_state_commitment: Digest,
389
390 final_state_commitment: Digest,
392
393 details: AccountUpdateDetails,
397}
398
399impl TxAccountUpdate {
400 pub const fn new(
402 account_id: AccountId,
403 init_state_commitment: Digest,
404 final_state_commitment: Digest,
405 details: AccountUpdateDetails,
406 ) -> Self {
407 Self {
408 account_id,
409 init_state_commitment,
410 final_state_commitment,
411 details,
412 }
413 }
414
415 pub fn account_id(&self) -> AccountId {
417 self.account_id
418 }
419
420 pub fn initial_state_commitment(&self) -> Digest {
422 self.init_state_commitment
423 }
424
425 pub fn final_state_commitment(&self) -> Digest {
427 self.final_state_commitment
428 }
429
430 pub fn details(&self) -> &AccountUpdateDetails {
435 &self.details
436 }
437
438 pub fn is_private(&self) -> bool {
440 self.details.is_private()
441 }
442
443 pub fn validate(&self) -> Result<(), ProvenTransactionError> {
447 let account_update_size = self.details().get_size_hint();
448 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
449 Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
450 account_id: self.account_id(),
451 update_size: account_update_size,
452 })
453 } else {
454 Ok(())
455 }
456 }
457}
458
459impl Serializable for TxAccountUpdate {
460 fn write_into<W: ByteWriter>(&self, target: &mut W) {
461 self.account_id.write_into(target);
462 self.init_state_commitment.write_into(target);
463 self.final_state_commitment.write_into(target);
464 self.details.write_into(target);
465 }
466}
467
468impl Deserializable for TxAccountUpdate {
469 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
470 Ok(Self {
471 account_id: AccountId::read_from(source)?,
472 init_state_commitment: Digest::read_from(source)?,
473 final_state_commitment: Digest::read_from(source)?,
474 details: AccountUpdateDetails::read_from(source)?,
475 })
476 }
477}
478
479#[derive(Debug, Clone, PartialEq, Eq)]
488pub struct InputNoteCommitment {
489 nullifier: Nullifier,
490 header: Option<NoteHeader>,
491}
492
493impl InputNoteCommitment {
494 pub fn nullifier(&self) -> Nullifier {
496 self.nullifier
497 }
498
499 pub fn header(&self) -> Option<&NoteHeader> {
504 self.header.as_ref()
505 }
506
507 pub fn is_authenticated(&self) -> bool {
513 self.header.is_none()
514 }
515}
516
517impl From<InputNote> for InputNoteCommitment {
518 fn from(note: InputNote) -> Self {
519 Self::from(¬e)
520 }
521}
522
523impl From<&InputNote> for InputNoteCommitment {
524 fn from(note: &InputNote) -> Self {
525 match note {
526 InputNote::Authenticated { note, .. } => Self {
527 nullifier: note.nullifier(),
528 header: None,
529 },
530 InputNote::Unauthenticated { note } => Self {
531 nullifier: note.nullifier(),
532 header: Some(*note.header()),
533 },
534 }
535 }
536}
537
538impl From<Nullifier> for InputNoteCommitment {
539 fn from(nullifier: Nullifier) -> Self {
540 Self { nullifier, header: None }
541 }
542}
543
544impl ToInputNoteCommitments for InputNoteCommitment {
545 fn nullifier(&self) -> Nullifier {
546 self.nullifier
547 }
548
549 fn note_commitment(&self) -> Option<Digest> {
550 self.header.map(|header| header.commitment())
551 }
552}
553
554impl Serializable for InputNoteCommitment {
558 fn write_into<W: ByteWriter>(&self, target: &mut W) {
559 self.nullifier.write_into(target);
560 self.header.write_into(target);
561 }
562}
563
564impl Deserializable for InputNoteCommitment {
565 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
566 let nullifier = Nullifier::read_from(source)?;
567 let header = <Option<NoteHeader>>::read_from(source)?;
568
569 Ok(Self { nullifier, header })
570 }
571}
572
573#[cfg(test)]
577mod tests {
578 use alloc::collections::BTreeMap;
579
580 use anyhow::Context;
581 use miden_verifier::ExecutionProof;
582 use vm_core::utils::Deserializable;
583 use winter_air::proof::Proof;
584 use winter_rand_utils::rand_array;
585
586 use super::ProvenTransaction;
587 use crate::{
588 ACCOUNT_UPDATE_MAX_SIZE, Digest, EMPTY_WORD, ONE, ProvenTransactionError, ZERO,
589 account::{
590 AccountDelta, AccountId, AccountIdVersion, AccountStorageDelta, AccountStorageMode,
591 AccountType, AccountVaultDelta, StorageMapDelta, delta::AccountUpdateDetails,
592 },
593 block::BlockNumber,
594 testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
595 transaction::{ProvenTransactionBuilder, TxAccountUpdate},
596 utils::Serializable,
597 };
598
599 fn check_if_sync<T: Sync>() {}
600 fn check_if_send<T: Send>() {}
601
602 #[test]
605 fn test_proven_transaction_is_sync() {
606 check_if_sync::<ProvenTransaction>();
607 }
608
609 #[test]
612 fn test_proven_transaction_is_send() {
613 check_if_send::<ProvenTransaction>();
614 }
615
616 #[test]
617 fn account_update_size_limit_not_exceeded() {
618 let storage_delta = AccountStorageDelta::from_iters(
620 [1, 2, 3, 4],
621 [(2, [ONE, ONE, ONE, ONE]), (3, [ONE, ONE, ZERO, ONE])],
622 [],
623 );
624 let delta =
625 AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
626 let details = AccountUpdateDetails::Delta(delta);
627 TxAccountUpdate::new(
628 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
629 Digest::new(EMPTY_WORD),
630 Digest::new(EMPTY_WORD),
631 details,
632 )
633 .validate()
634 .unwrap();
635 }
636
637 #[test]
638 fn account_update_size_limit_exceeded() {
639 let mut map = BTreeMap::new();
640 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
644 for _ in 0..required_entries {
645 map.insert(Digest::new(rand_array()), rand_array());
646 }
647 let storage_delta = StorageMapDelta::new(map);
648
649 let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
651 let delta =
652 AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
653 let details = AccountUpdateDetails::Delta(delta);
654 let details_size = details.get_size_hint();
655
656 let err = TxAccountUpdate::new(
657 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
658 Digest::new(EMPTY_WORD),
659 Digest::new(EMPTY_WORD),
660 details,
661 )
662 .validate()
663 .unwrap_err();
664
665 assert!(
666 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
667 );
668 }
669
670 #[test]
671 fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
672 let account_id = AccountId::dummy(
673 [1; 15],
674 AccountIdVersion::Version0,
675 AccountType::FungibleFaucet,
676 AccountStorageMode::Private,
677 );
678 let initial_account_commitment =
679 [2; 32].try_into().expect("failed to create initial account commitment");
680 let final_account_commitment =
681 [3; 32].try_into().expect("failed to create final account commitment");
682 let ref_block_num = BlockNumber::from(1);
683 let ref_block_commitment = Digest::default();
684 let expiration_block_num = BlockNumber::from(2);
685 let proof = ExecutionProof::new(Proof::new_dummy(), Default::default());
686
687 let tx = ProvenTransactionBuilder::new(
688 account_id,
689 initial_account_commitment,
690 final_account_commitment,
691 ref_block_num,
692 ref_block_commitment,
693 expiration_block_num,
694 proof,
695 )
696 .build()
697 .context("failed to build proven transaction")?;
698
699 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
700
701 assert_eq!(tx, deserialized);
702
703 Ok(())
704 }
705}