1use alloc::{string::ToString, vec::Vec};
2
3use miden_verifier::ExecutionProof;
4
5use super::{InputNote, ToInputNoteCommitments};
6use crate::{
7 account::delta::AccountUpdateDetails,
8 block::BlockNumber,
9 note::NoteHeader,
10 transaction::{
11 AccountId, Digest, InputNotes, Nullifier, OutputNote, OutputNotes, TransactionId,
12 },
13 utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
14 ProvenTransactionError, ACCOUNT_UPDATE_MAX_SIZE,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ProvenTransaction {
24 id: TransactionId,
26
27 account_update: TxAccountUpdate,
29
30 input_notes: InputNotes<InputNoteCommitment>,
32
33 output_notes: OutputNotes,
36
37 block_ref: Digest,
39
40 expiration_block_num: BlockNumber,
42
43 proof: ExecutionProof,
45}
46
47impl ProvenTransaction {
48 pub fn id(&self) -> TransactionId {
50 self.id
51 }
52
53 pub fn account_id(&self) -> AccountId {
55 self.account_update.account_id()
56 }
57
58 pub fn account_update(&self) -> &TxAccountUpdate {
60 &self.account_update
61 }
62
63 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
65 &self.input_notes
66 }
67
68 pub fn output_notes(&self) -> &OutputNotes {
70 &self.output_notes
71 }
72
73 pub fn proof(&self) -> &ExecutionProof {
75 &self.proof
76 }
77
78 pub fn block_ref(&self) -> Digest {
80 self.block_ref
81 }
82
83 pub fn get_unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
85 self.input_notes.iter().filter_map(|note| note.header())
86 }
87
88 pub fn expiration_block_num(&self) -> BlockNumber {
90 self.expiration_block_num
91 }
92
93 pub fn get_nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
97 self.input_notes.iter().map(InputNoteCommitment::nullifier)
98 }
99
100 fn validate(self) -> Result<Self, ProvenTransactionError> {
104 if self.account_id().is_public() {
105 self.account_update.validate()?;
106
107 let is_new_account = self.account_update.init_state_hash() == Digest::default();
108 match self.account_update.details() {
109 AccountUpdateDetails::Private => {
110 return Err(ProvenTransactionError::OnChainAccountMissingDetails(
111 self.account_id(),
112 ))
113 },
114 AccountUpdateDetails::New(ref account) => {
115 if !is_new_account {
116 return Err(
117 ProvenTransactionError::ExistingOnChainAccountRequiresDeltaDetails(
118 self.account_id(),
119 ),
120 );
121 }
122 if account.id() != self.account_id() {
123 return Err(ProvenTransactionError::AccountIdMismatch {
124 tx_account_id: self.account_id(),
125 details_account_id: account.id(),
126 });
127 }
128 if account.hash() != self.account_update.final_state_hash() {
129 return Err(ProvenTransactionError::AccountFinalHashMismatch {
130 tx_final_hash: self.account_update.final_state_hash(),
131 details_hash: account.hash(),
132 });
133 }
134 },
135 AccountUpdateDetails::Delta(_) => {
136 if is_new_account {
137 return Err(ProvenTransactionError::NewOnChainAccountRequiresFullDetails(
138 self.account_id(),
139 ));
140 }
141 },
142 }
143 } else if !self.account_update.is_private() {
144 return Err(ProvenTransactionError::OffChainAccountWithDetails(self.account_id()));
145 }
146
147 Ok(self)
148 }
149}
150
151impl Serializable for ProvenTransaction {
152 fn write_into<W: ByteWriter>(&self, target: &mut W) {
153 self.account_update.write_into(target);
154 self.input_notes.write_into(target);
155 self.output_notes.write_into(target);
156 self.block_ref.write_into(target);
157 self.expiration_block_num.write_into(target);
158 self.proof.write_into(target);
159 }
160}
161
162impl Deserializable for ProvenTransaction {
163 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
164 let account_update = TxAccountUpdate::read_from(source)?;
165
166 let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
167 let output_notes = OutputNotes::read_from(source)?;
168
169 let block_ref = Digest::read_from(source)?;
170 let expiration_block_num = BlockNumber::read_from(source)?;
171 let proof = ExecutionProof::read_from(source)?;
172
173 let id = TransactionId::new(
174 account_update.init_state_hash(),
175 account_update.final_state_hash(),
176 input_notes.commitment(),
177 output_notes.commitment(),
178 );
179
180 let proven_transaction = Self {
181 id,
182 account_update,
183 input_notes,
184 output_notes,
185 block_ref,
186 expiration_block_num,
187 proof,
188 };
189
190 proven_transaction
191 .validate()
192 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
193 }
194}
195
196#[derive(Clone, Debug)]
201pub struct ProvenTransactionBuilder {
202 account_id: AccountId,
204
205 initial_account_hash: Digest,
207
208 final_account_hash: Digest,
210
211 account_update_details: AccountUpdateDetails,
213
214 input_notes: Vec<InputNoteCommitment>,
216
217 output_notes: Vec<OutputNote>,
219
220 block_ref: Digest,
222
223 expiration_block_num: BlockNumber,
225
226 proof: ExecutionProof,
228}
229
230impl ProvenTransactionBuilder {
231 pub fn new(
236 account_id: AccountId,
237 initial_account_hash: Digest,
238 final_account_hash: Digest,
239 block_ref: Digest,
240 expiration_block_num: BlockNumber,
241 proof: ExecutionProof,
242 ) -> Self {
243 Self {
244 account_id,
245 initial_account_hash,
246 final_account_hash,
247 account_update_details: AccountUpdateDetails::Private,
248 input_notes: Vec::new(),
249 output_notes: Vec::new(),
250 block_ref,
251 expiration_block_num,
252 proof,
253 }
254 }
255
256 pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
261 self.account_update_details = details;
262 self
263 }
264
265 pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
267 where
268 I: IntoIterator<Item = T>,
269 T: Into<InputNoteCommitment>,
270 {
271 self.input_notes.extend(notes.into_iter().map(|note| note.into()));
272 self
273 }
274
275 pub fn add_output_notes<T>(mut self, notes: T) -> Self
277 where
278 T: IntoIterator<Item = OutputNote>,
279 {
280 self.output_notes.extend(notes);
281 self
282 }
283
284 pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
291 let input_notes =
292 InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
293 let output_notes = OutputNotes::new(self.output_notes)
294 .map_err(ProvenTransactionError::OutputNotesError)?;
295 let id = TransactionId::new(
296 self.initial_account_hash,
297 self.final_account_hash,
298 input_notes.commitment(),
299 output_notes.commitment(),
300 );
301 let account_update = TxAccountUpdate::new(
302 self.account_id,
303 self.initial_account_hash,
304 self.final_account_hash,
305 self.account_update_details,
306 );
307
308 let proven_transaction = ProvenTransaction {
309 id,
310 account_update,
311 input_notes,
312 output_notes,
313 block_ref: self.block_ref,
314 expiration_block_num: self.expiration_block_num,
315 proof: self.proof,
316 };
317
318 proven_transaction.validate()
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct TxAccountUpdate {
328 account_id: AccountId,
330
331 init_state_hash: Digest,
335
336 final_state_hash: Digest,
338
339 details: AccountUpdateDetails,
343}
344
345impl TxAccountUpdate {
346 pub const fn new(
348 account_id: AccountId,
349 init_state_hash: Digest,
350 final_state_hash: Digest,
351 details: AccountUpdateDetails,
352 ) -> Self {
353 Self {
354 account_id,
355 init_state_hash,
356 final_state_hash,
357 details,
358 }
359 }
360
361 pub fn account_id(&self) -> AccountId {
363 self.account_id
364 }
365
366 pub fn init_state_hash(&self) -> Digest {
368 self.init_state_hash
369 }
370
371 pub fn final_state_hash(&self) -> Digest {
373 self.final_state_hash
374 }
375
376 pub fn details(&self) -> &AccountUpdateDetails {
381 &self.details
382 }
383
384 pub fn is_private(&self) -> bool {
386 self.details.is_private()
387 }
388
389 pub fn validate(&self) -> Result<(), ProvenTransactionError> {
393 let account_update_size = self.details().get_size_hint();
394 if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
395 Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
396 account_id: self.account_id(),
397 update_size: account_update_size,
398 })
399 } else {
400 Ok(())
401 }
402 }
403}
404
405impl Serializable for TxAccountUpdate {
406 fn write_into<W: ByteWriter>(&self, target: &mut W) {
407 self.account_id.write_into(target);
408 self.init_state_hash.write_into(target);
409 self.final_state_hash.write_into(target);
410 self.details.write_into(target);
411 }
412}
413
414impl Deserializable for TxAccountUpdate {
415 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
416 Ok(Self {
417 account_id: AccountId::read_from(source)?,
418 init_state_hash: Digest::read_from(source)?,
419 final_state_hash: Digest::read_from(source)?,
420 details: AccountUpdateDetails::read_from(source)?,
421 })
422 }
423}
424
425#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct InputNoteCommitment {
435 nullifier: Nullifier,
436 header: Option<NoteHeader>,
437}
438
439impl InputNoteCommitment {
440 pub fn nullifier(&self) -> Nullifier {
442 self.nullifier
443 }
444
445 pub fn header(&self) -> Option<&NoteHeader> {
450 self.header.as_ref()
451 }
452
453 pub fn is_authenticated(&self) -> bool {
459 self.header.is_none()
460 }
461}
462
463impl From<InputNote> for InputNoteCommitment {
464 fn from(note: InputNote) -> Self {
465 Self::from(¬e)
466 }
467}
468
469impl From<&InputNote> for InputNoteCommitment {
470 fn from(note: &InputNote) -> Self {
471 match note {
472 InputNote::Authenticated { note, .. } => Self {
473 nullifier: note.nullifier(),
474 header: None,
475 },
476 InputNote::Unauthenticated { note } => Self {
477 nullifier: note.nullifier(),
478 header: Some(*note.header()),
479 },
480 }
481 }
482}
483
484impl From<Nullifier> for InputNoteCommitment {
485 fn from(nullifier: Nullifier) -> Self {
486 Self { nullifier, header: None }
487 }
488}
489
490impl ToInputNoteCommitments for InputNoteCommitment {
491 fn nullifier(&self) -> Nullifier {
492 self.nullifier
493 }
494
495 fn note_hash(&self) -> Option<Digest> {
496 self.header.map(|header| header.hash())
497 }
498}
499
500impl Serializable for InputNoteCommitment {
504 fn write_into<W: ByteWriter>(&self, target: &mut W) {
505 self.nullifier.write_into(target);
506 self.header.write_into(target);
507 }
508}
509
510impl Deserializable for InputNoteCommitment {
511 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
512 let nullifier = Nullifier::read_from(source)?;
513 let header = <Option<NoteHeader>>::read_from(source)?;
514
515 Ok(Self { nullifier, header })
516 }
517}
518
519#[cfg(test)]
523mod tests {
524 use alloc::collections::BTreeMap;
525
526 use winter_rand_utils::rand_array;
527
528 use super::ProvenTransaction;
529 use crate::{
530 account::{
531 delta::AccountUpdateDetails, AccountDelta, AccountId, AccountStorageDelta,
532 AccountVaultDelta, StorageMapDelta,
533 },
534 testing::account_id::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN,
535 transaction::TxAccountUpdate,
536 utils::Serializable,
537 Digest, ProvenTransactionError, ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, ONE, ZERO,
538 };
539
540 fn check_if_sync<T: Sync>() {}
541 fn check_if_send<T: Send>() {}
542
543 #[test]
546 fn test_proven_transaction_is_sync() {
547 check_if_sync::<ProvenTransaction>();
548 }
549
550 #[test]
553 fn test_proven_transaction_is_send() {
554 check_if_send::<ProvenTransaction>();
555 }
556
557 #[test]
558 fn account_update_size_limit_not_exceeded() {
559 let storage_delta = AccountStorageDelta::from_iters(
561 [1, 2, 3, 4],
562 [(2, [ONE, ONE, ONE, ONE]), (3, [ONE, ONE, ZERO, ONE])],
563 [],
564 );
565 let delta =
566 AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
567 let details = AccountUpdateDetails::Delta(delta);
568 TxAccountUpdate::new(
569 AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(),
570 Digest::new(EMPTY_WORD),
571 Digest::new(EMPTY_WORD),
572 details,
573 )
574 .validate()
575 .unwrap();
576 }
577
578 #[test]
579 fn account_update_size_limit_exceeded() {
580 let mut map = BTreeMap::new();
581 let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
585 for _ in 0..required_entries {
586 map.insert(Digest::new(rand_array()), rand_array());
587 }
588 let storage_delta = StorageMapDelta::new(map);
589
590 let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
592 let delta =
593 AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
594 let details = AccountUpdateDetails::Delta(delta);
595 let details_size = details.get_size_hint();
596
597 let err = TxAccountUpdate::new(
598 AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(),
599 Digest::new(EMPTY_WORD),
600 Digest::new(EMPTY_WORD),
601 details,
602 )
603 .validate()
604 .unwrap_err();
605
606 assert!(
607 matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
608 );
609 }
610}