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,
41
42 ref_block_commitment: Digest,
44
45 expiration_block_num: BlockNumber,
47
48 proof: ExecutionProof,
50}
51
52impl ProvenTransaction {
53 pub fn id(&self) -> TransactionId {
55 self.id
56 }
57
58 pub fn account_id(&self) -> AccountId {
60 self.account_update.account_id()
61 }
62
63 pub fn account_update(&self) -> &TxAccountUpdate {
65 &self.account_update
66 }
67
68 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
70 &self.input_notes
71 }
72
73 pub fn output_notes(&self) -> &OutputNotes {
75 &self.output_notes
76 }
77
78 pub fn proof(&self) -> &ExecutionProof {
80 &self.proof
81 }
82
83 pub fn ref_block_num(&self) -> BlockNumber {
85 self.ref_block_num
86 }
87
88 pub fn ref_block_commitment(&self) -> Digest {
90 self.ref_block_commitment
91 }
92
93 pub fn get_unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
95 self.input_notes.iter().filter_map(|note| note.header())
96 }
97
98 pub fn expiration_block_num(&self) -> BlockNumber {
100 self.expiration_block_num
101 }
102
103 pub fn get_nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
107 self.input_notes.iter().map(InputNoteCommitment::nullifier)
108 }
109
110 fn validate(self) -> Result<Self, ProvenTransactionError> {
114 if self.account_id().is_public() {
115 self.account_update.validate()?;
116
117 let is_new_account =
118 self.account_update.initial_state_commitment() == Digest::default();
119 match self.account_update.details() {
120 AccountUpdateDetails::Private => {
121 return Err(ProvenTransactionError::PublicAccountMissingDetails(
122 self.account_id(),
123 ));
124 },
125 AccountUpdateDetails::New(account) => {
126 if !is_new_account {
127 return Err(
128 ProvenTransactionError::ExistingPublicAccountRequiresDeltaDetails(
129 self.account_id(),
130 ),
131 );
132 }
133 if account.id() != self.account_id() {
134 return Err(ProvenTransactionError::AccountIdMismatch {
135 tx_account_id: self.account_id(),
136 details_account_id: account.id(),
137 });
138 }
139 if account.commitment() != self.account_update.final_state_commitment() {
140 return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
141 tx_final_commitment: self.account_update.final_state_commitment(),
142 details_commitment: account.commitment(),
143 });
144 }
145 },
146 AccountUpdateDetails::Delta(_) => {
147 if is_new_account {
148 return Err(ProvenTransactionError::NewPublicAccountRequiresFullDetails(
149 self.account_id(),
150 ));
151 }
152 },
153 }
154 } else if !self.account_update.is_private() {
155 return Err(ProvenTransactionError::PrivateAccountWithDetails(self.account_id()));
156 }
157
158 Ok(self)
159 }
160}
161
162impl Serializable for ProvenTransaction {
163 fn write_into<W: ByteWriter>(&self, target: &mut W) {
164 self.account_update.write_into(target);
165 self.input_notes.write_into(target);
166 self.output_notes.write_into(target);
167 self.ref_block_num.write_into(target);
168 self.ref_block_commitment.write_into(target);
169 self.expiration_block_num.write_into(target);
170 self.proof.write_into(target);
171 }
172}
173
174impl Deserializable for ProvenTransaction {
175 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
176 let account_update = TxAccountUpdate::read_from(source)?;
177
178 let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
179 let output_notes = OutputNotes::read_from(source)?;
180
181 let ref_block_num = BlockNumber::read_from(source)?;
182 let ref_block_commitment = Digest::read_from(source)?;
183 let expiration_block_num = BlockNumber::read_from(source)?;
184 let proof = ExecutionProof::read_from(source)?;
185
186 let id = TransactionId::new(
187 account_update.initial_state_commitment(),
188 account_update.final_state_commitment(),
189 input_notes.commitment(),
190 output_notes.commitment(),
191 );
192
193 let proven_transaction = Self {
194 id,
195 account_update,
196 input_notes,
197 output_notes,
198 ref_block_num,
199 ref_block_commitment,
200 expiration_block_num,
201 proof,
202 };
203
204 proven_transaction
205 .validate()
206 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
207 }
208}
209
210#[derive(Clone, Debug)]
215pub struct ProvenTransactionBuilder {
216 account_id: AccountId,
218
219 initial_account_commitment: Digest,
221
222 final_account_commitment: Digest,
224
225 account_update_details: AccountUpdateDetails,
227
228 input_notes: Vec<InputNoteCommitment>,
230
231 output_notes: Vec<OutputNote>,
233
234 ref_block_num: BlockNumber,
236
237 ref_block_commitment: Digest,
239
240 expiration_block_num: BlockNumber,
242
243 proof: ExecutionProof,
245}
246
247impl ProvenTransactionBuilder {
248 pub fn new(
253 account_id: AccountId,
254 initial_account_commitment: Digest,
255 final_account_commitment: Digest,
256 ref_block_num: BlockNumber,
257 ref_block_commitment: Digest,
258 expiration_block_num: BlockNumber,
259 proof: ExecutionProof,
260 ) -> Self {
261 Self {
262 account_id,
263 initial_account_commitment,
264 final_account_commitment,
265 account_update_details: AccountUpdateDetails::Private,
266 input_notes: Vec::new(),
267 output_notes: Vec::new(),
268 ref_block_num,
269 ref_block_commitment,
270 expiration_block_num,
271 proof,
272 }
273 }
274
275 pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
280 self.account_update_details = details;
281 self
282 }
283
284 pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
286 where
287 I: IntoIterator<Item = T>,
288 T: Into<InputNoteCommitment>,
289 {
290 self.input_notes.extend(notes.into_iter().map(|note| note.into()));
291 self
292 }
293
294 pub fn add_output_notes<T>(mut self, notes: T) -> Self
296 where
297 T: IntoIterator<Item = OutputNote>,
298 {
299 self.output_notes.extend(notes);
300 self
301 }
302
303 pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
310 let input_notes =
311 InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
312 let output_notes = OutputNotes::new(self.output_notes)
313 .map_err(ProvenTransactionError::OutputNotesError)?;
314 let id = TransactionId::new(
315 self.initial_account_commitment,
316 self.final_account_commitment,
317 input_notes.commitment(),
318 output_notes.commitment(),
319 );
320 let account_update = TxAccountUpdate::new(
321 self.account_id,
322 self.initial_account_commitment,
323 self.final_account_commitment,
324 self.account_update_details,
325 );
326
327 let proven_transaction = ProvenTransaction {
328 id,
329 account_update,
330 input_notes,
331 output_notes,
332 ref_block_num: self.ref_block_num,
333 ref_block_commitment: self.ref_block_commitment,
334 expiration_block_num: self.expiration_block_num,
335 proof: self.proof,
336 };
337
338 proven_transaction.validate()
339 }
340}
341
342#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct TxAccountUpdate {
348 account_id: AccountId,
350
351 init_state_commitment: Digest,
355
356 final_state_commitment: Digest,
358
359 details: AccountUpdateDetails,
363}
364
365impl TxAccountUpdate {
366 pub const fn new(
368 account_id: AccountId,
369 init_state_commitment: Digest,
370 final_state_commitment: Digest,
371 details: AccountUpdateDetails,
372 ) -> Self {
373 Self {
374 account_id,
375 init_state_commitment,
376 final_state_commitment,
377 details,
378 }
379 }
380
381 pub fn account_id(&self) -> AccountId {
383 self.account_id
384 }
385
386 pub fn initial_state_commitment(&self) -> Digest {
388 self.init_state_commitment
389 }
390
391 pub fn final_state_commitment(&self) -> Digest {
393 self.final_state_commitment
394 }
395
396 pub fn details(&self) -> &AccountUpdateDetails {
401 &self.details
402 }
403
404 pub fn is_private(&self) -> bool {
406 self.details.is_private()
407 }
408
409 pub fn validate(&self) -> Result<(), ProvenTransactionError> {
413 let account_update_size = self.details().get_size_hint();
414 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
415 Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
416 account_id: self.account_id(),
417 update_size: account_update_size,
418 })
419 } else {
420 Ok(())
421 }
422 }
423}
424
425impl Serializable for TxAccountUpdate {
426 fn write_into<W: ByteWriter>(&self, target: &mut W) {
427 self.account_id.write_into(target);
428 self.init_state_commitment.write_into(target);
429 self.final_state_commitment.write_into(target);
430 self.details.write_into(target);
431 }
432}
433
434impl Deserializable for TxAccountUpdate {
435 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
436 Ok(Self {
437 account_id: AccountId::read_from(source)?,
438 init_state_commitment: Digest::read_from(source)?,
439 final_state_commitment: Digest::read_from(source)?,
440 details: AccountUpdateDetails::read_from(source)?,
441 })
442 }
443}
444
445#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct InputNoteCommitment {
455 nullifier: Nullifier,
456 header: Option<NoteHeader>,
457}
458
459impl InputNoteCommitment {
460 pub fn nullifier(&self) -> Nullifier {
462 self.nullifier
463 }
464
465 pub fn header(&self) -> Option<&NoteHeader> {
470 self.header.as_ref()
471 }
472
473 pub fn is_authenticated(&self) -> bool {
479 self.header.is_none()
480 }
481}
482
483impl From<InputNote> for InputNoteCommitment {
484 fn from(note: InputNote) -> Self {
485 Self::from(¬e)
486 }
487}
488
489impl From<&InputNote> for InputNoteCommitment {
490 fn from(note: &InputNote) -> Self {
491 match note {
492 InputNote::Authenticated { note, .. } => Self {
493 nullifier: note.nullifier(),
494 header: None,
495 },
496 InputNote::Unauthenticated { note } => Self {
497 nullifier: note.nullifier(),
498 header: Some(*note.header()),
499 },
500 }
501 }
502}
503
504impl From<Nullifier> for InputNoteCommitment {
505 fn from(nullifier: Nullifier) -> Self {
506 Self { nullifier, header: None }
507 }
508}
509
510impl ToInputNoteCommitments for InputNoteCommitment {
511 fn nullifier(&self) -> Nullifier {
512 self.nullifier
513 }
514
515 fn note_commitment(&self) -> Option<Digest> {
516 self.header.map(|header| header.commitment())
517 }
518}
519
520impl Serializable for InputNoteCommitment {
524 fn write_into<W: ByteWriter>(&self, target: &mut W) {
525 self.nullifier.write_into(target);
526 self.header.write_into(target);
527 }
528}
529
530impl Deserializable for InputNoteCommitment {
531 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
532 let nullifier = Nullifier::read_from(source)?;
533 let header = <Option<NoteHeader>>::read_from(source)?;
534
535 Ok(Self { nullifier, header })
536 }
537}
538
539#[cfg(test)]
543mod tests {
544 use alloc::collections::BTreeMap;
545
546 use miden_verifier::ExecutionProof;
547 use vm_core::utils::Deserializable;
548 use winter_air::proof::Proof;
549 use winter_rand_utils::rand_array;
550
551 use super::ProvenTransaction;
552 use crate::{
553 ACCOUNT_UPDATE_MAX_SIZE, Digest, EMPTY_WORD, ONE, ProvenTransactionError, ZERO,
554 account::{
555 AccountDelta, AccountId, AccountIdVersion, AccountStorageDelta, AccountStorageMode,
556 AccountType, AccountVaultDelta, StorageMapDelta, delta::AccountUpdateDetails,
557 },
558 block::BlockNumber,
559 testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
560 transaction::{ProvenTransactionBuilder, TxAccountUpdate},
561 utils::Serializable,
562 };
563
564 fn check_if_sync<T: Sync>() {}
565 fn check_if_send<T: Send>() {}
566
567 #[test]
570 fn test_proven_transaction_is_sync() {
571 check_if_sync::<ProvenTransaction>();
572 }
573
574 #[test]
577 fn test_proven_transaction_is_send() {
578 check_if_send::<ProvenTransaction>();
579 }
580
581 #[test]
582 fn account_update_size_limit_not_exceeded() {
583 let storage_delta = AccountStorageDelta::from_iters(
585 [1, 2, 3, 4],
586 [(2, [ONE, ONE, ONE, ONE]), (3, [ONE, ONE, ZERO, ONE])],
587 [],
588 );
589 let delta =
590 AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
591 let details = AccountUpdateDetails::Delta(delta);
592 TxAccountUpdate::new(
593 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
594 Digest::new(EMPTY_WORD),
595 Digest::new(EMPTY_WORD),
596 details,
597 )
598 .validate()
599 .unwrap();
600 }
601
602 #[test]
603 fn account_update_size_limit_exceeded() {
604 let mut map = BTreeMap::new();
605 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
609 for _ in 0..required_entries {
610 map.insert(Digest::new(rand_array()), rand_array());
611 }
612 let storage_delta = StorageMapDelta::new(map);
613
614 let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
616 let delta =
617 AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
618 let details = AccountUpdateDetails::Delta(delta);
619 let details_size = details.get_size_hint();
620
621 let err = TxAccountUpdate::new(
622 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
623 Digest::new(EMPTY_WORD),
624 Digest::new(EMPTY_WORD),
625 details,
626 )
627 .validate()
628 .unwrap_err();
629
630 assert!(
631 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
632 );
633 }
634
635 #[test]
636 fn test_proven_tx_serde_roundtrip() {
637 let account_id = AccountId::dummy(
638 [1; 15],
639 AccountIdVersion::Version0,
640 AccountType::FungibleFaucet,
641 AccountStorageMode::Private,
642 );
643 let initial_account_commitment =
644 [2; 32].try_into().expect("failed to create initial account commitment");
645 let final_account_commitment =
646 [3; 32].try_into().expect("failed to create final account commitment");
647 let ref_block_num = BlockNumber::from(1);
648 let ref_block_commitment = Digest::default();
649 let expiration_block_num = BlockNumber::from(2);
650 let proof = ExecutionProof::new(Proof::new_dummy(), Default::default());
651
652 let tx = ProvenTransactionBuilder::new(
653 account_id,
654 initial_account_commitment,
655 final_account_commitment,
656 ref_block_num,
657 ref_block_commitment,
658 expiration_block_num,
659 proof,
660 )
661 .build()
662 .expect("failed to build proven transaction");
663
664 let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
665
666 assert_eq!(tx, deserialized);
667 }
668}