1use alloc::collections::btree_map::Entry;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::string::ToString;
4use alloc::vec::Vec;
5
6use crate::account::AccountId;
7use crate::batch::{BatchAccountUpdate, BatchId};
8use crate::block::BlockNumber;
9use crate::errors::ProvenBatchError;
10use crate::note::Nullifier;
11use crate::transaction::{
12 InputNoteCommitment,
13 InputNotes,
14 OrderedTransactionHeaders,
15 OutputNote,
16 TransactionHeader,
17};
18use crate::utils::serde::{
19 ByteReader,
20 ByteWriter,
21 Deserializable,
22 DeserializationError,
23 Serializable,
24};
25use crate::vm::ExecutionProof;
26use crate::{
27 MAX_ACCOUNTS_PER_BATCH,
28 MAX_INPUT_NOTES_PER_BATCH,
29 MAX_OUTPUT_NOTES_PER_BATCH,
30 MIN_PROOF_SECURITY_LEVEL,
31 Word,
32};
33
34#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ProvenBatch {
38 id: BatchId,
39 reference_block_commitment: Word,
40 reference_block_num: BlockNumber,
41 account_updates: BTreeMap<AccountId, BatchAccountUpdate>,
42 input_notes: InputNotes<InputNoteCommitment>,
43 output_notes: Vec<OutputNote>,
44 batch_expiration_block_num: BlockNumber,
45 transactions: OrderedTransactionHeaders,
46 proof: ExecutionProof,
47}
48
49impl ProvenBatch {
50 #[allow(clippy::too_many_arguments)]
71 pub fn new(
72 reference_block_commitment: Word,
73 reference_block_num: BlockNumber,
74 account_updates: impl IntoIterator<Item = BatchAccountUpdate>,
75 input_notes: InputNotes<InputNoteCommitment>,
76 output_notes: Vec<OutputNote>,
77 batch_expiration_block_num: BlockNumber,
78 transactions: OrderedTransactionHeaders,
79 proof: ExecutionProof,
80 ) -> Result<Self, ProvenBatchError> {
81 if proof.has_precompiles() {
82 return Err(ProvenBatchError::BatchProofContainsPrecompiles);
83 }
84
85 if transactions.as_slice().is_empty() {
86 return Err(ProvenBatchError::EmptyTransactionBatch);
87 }
88
89 let mut transaction_ids = BTreeSet::new();
90 for transaction in transactions.as_slice() {
91 if !transaction_ids.insert(transaction.id()) {
92 return Err(ProvenBatchError::DuplicateTransaction(transaction.id()));
93 }
94 }
95
96 let mut account_updates_by_id = BTreeMap::new();
97 for (index, update) in account_updates.into_iter().enumerate() {
98 let account_update_count = index + 1;
99 if account_update_count > MAX_ACCOUNTS_PER_BATCH {
100 return Err(ProvenBatchError::TooManyAccountUpdates(account_update_count));
101 }
102
103 let account_id = update.account_id();
104 if account_updates_by_id.insert(account_id, update).is_some() {
105 return Err(ProvenBatchError::DuplicateAccountUpdate(account_id));
106 }
107 }
108
109 let input_note_count = usize::from(input_notes.num_notes());
110 if input_note_count > MAX_INPUT_NOTES_PER_BATCH {
111 return Err(ProvenBatchError::TooManyInputNotes(input_note_count));
112 }
113
114 if output_notes.len() > MAX_OUTPUT_NOTES_PER_BATCH {
115 return Err(ProvenBatchError::TooManyOutputNotes(output_notes.len()));
116 }
117
118 validate_account_updates(&account_updates_by_id, transactions.as_slice())?;
119 validate_notes(&input_notes, &output_notes)?;
120
121 let id =
122 BatchId::from_ids(transactions.as_slice().iter().map(|tx| (tx.id(), tx.account_id())));
123 Self::new_unchecked(
124 id,
125 reference_block_commitment,
126 reference_block_num,
127 account_updates_by_id,
128 input_notes,
129 output_notes,
130 batch_expiration_block_num,
131 transactions,
132 proof,
133 )
134 }
135
136 #[allow(clippy::too_many_arguments)]
147 pub fn new_unchecked(
148 id: BatchId,
149 reference_block_commitment: Word,
150 reference_block_num: BlockNumber,
151 account_updates: BTreeMap<AccountId, BatchAccountUpdate>,
152 input_notes: InputNotes<InputNoteCommitment>,
153 output_notes: Vec<OutputNote>,
154 batch_expiration_block_num: BlockNumber,
155 transactions: OrderedTransactionHeaders,
156 proof: ExecutionProof,
157 ) -> Result<Self, ProvenBatchError> {
158 if batch_expiration_block_num <= reference_block_num {
160 return Err(ProvenBatchError::InvalidBatchExpirationBlockNum {
161 batch_expiration_block_num,
162 reference_block_num,
163 });
164 }
165
166 Ok(Self {
167 id,
168 reference_block_commitment,
169 reference_block_num,
170 account_updates,
171 input_notes,
172 output_notes,
173 batch_expiration_block_num,
174 transactions,
175 proof,
176 })
177 }
178
179 pub fn id(&self) -> BatchId {
184 self.id
185 }
186
187 pub fn reference_block_commitment(&self) -> Word {
189 self.reference_block_commitment
190 }
191
192 pub fn reference_block_num(&self) -> BlockNumber {
194 self.reference_block_num
195 }
196
197 pub fn batch_expiration_block_num(&self) -> BlockNumber {
199 self.batch_expiration_block_num
200 }
201
202 pub fn updated_accounts(&self) -> impl Iterator<Item = AccountId> + use<'_> {
204 self.account_updates.keys().copied()
205 }
206
207 pub fn proof_security_level(&self) -> u32 {
209 MIN_PROOF_SECURITY_LEVEL
210 }
211
212 pub fn account_updates(&self) -> &BTreeMap<AccountId, BatchAccountUpdate> {
222 &self.account_updates
223 }
224
225 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
227 &self.input_notes
228 }
229
230 pub fn created_nullifiers(&self) -> impl Iterator<Item = Nullifier> + use<'_> {
232 self.input_notes.iter().map(InputNoteCommitment::nullifier)
233 }
234
235 pub fn output_notes(&self) -> &[OutputNote] {
237 &self.output_notes
238 }
239
240 pub fn transactions(&self) -> &OrderedTransactionHeaders {
242 &self.transactions
243 }
244
245 pub fn proof(&self) -> &ExecutionProof {
247 &self.proof
248 }
249
250 pub fn into_transactions(self) -> OrderedTransactionHeaders {
255 self.transactions
256 }
257}
258
259fn validate_account_updates(
263 account_updates: &BTreeMap<AccountId, BatchAccountUpdate>,
264 transactions: &[TransactionHeader],
265) -> Result<(), ProvenBatchError> {
266 let mut expected_updates = BTreeMap::<AccountId, (Word, Word)>::new();
267
268 for transaction in transactions {
269 match expected_updates.entry(transaction.account_id()) {
270 Entry::Vacant(entry) => {
271 entry.insert((
272 transaction.initial_state_commitment(),
273 transaction.final_state_commitment(),
274 ));
275 },
276 Entry::Occupied(mut entry) => {
277 let (_, previous_final_state_commitment) = entry.get_mut();
278 if *previous_final_state_commitment != transaction.initial_state_commitment() {
279 return Err(ProvenBatchError::TransactionAccountStateMismatch {
280 account_id: transaction.account_id(),
281 transaction_id: transaction.id(),
282 expected_initial_state_commitment: *previous_final_state_commitment,
283 actual_initial_state_commitment: transaction.initial_state_commitment(),
284 });
285 }
286 *previous_final_state_commitment = transaction.final_state_commitment();
287 },
288 }
289 }
290
291 for (account_id, (expected_initial, expected_final)) in &expected_updates {
292 let update = account_updates
293 .get(account_id)
294 .ok_or(ProvenBatchError::MissingAccountUpdate(*account_id))?;
295
296 if update.initial_state_commitment() != *expected_initial {
297 return Err(ProvenBatchError::AccountUpdateInitialStateMismatch {
298 account_id: *account_id,
299 expected: *expected_initial,
300 actual: update.initial_state_commitment(),
301 });
302 }
303 if update.final_state_commitment() != *expected_final {
304 return Err(ProvenBatchError::AccountUpdateFinalStateMismatch {
305 account_id: *account_id,
306 expected: *expected_final,
307 actual: update.final_state_commitment(),
308 });
309 }
310 }
311
312 if let Some(account_id) = account_updates
313 .keys()
314 .find(|account_id| !expected_updates.contains_key(account_id))
315 {
316 return Err(ProvenBatchError::UnexpectedAccountUpdate(*account_id));
317 }
318
319 Ok(())
320}
321
322fn validate_notes(
323 input_notes: &InputNotes<InputNoteCommitment>,
324 output_notes: &[OutputNote],
325) -> Result<(), ProvenBatchError> {
326 let mut input_nullifiers = BTreeSet::new();
327 let mut input_note_ids = BTreeSet::new();
328 for input_note in input_notes {
329 if !input_nullifiers.insert(input_note.nullifier()) {
330 return Err(ProvenBatchError::DuplicateInputNote(input_note.nullifier()));
331 }
332 if let Some(header) = input_note.header() {
333 input_note_ids.insert(header.id());
334 }
335 }
336
337 let mut output_note_ids = BTreeSet::new();
338 for output_note in output_notes {
339 let note_id = output_note.id();
340 if !output_note_ids.insert(note_id) {
341 return Err(ProvenBatchError::DuplicateOutputNote(note_id));
342 }
343 if input_note_ids.contains(¬e_id) {
344 return Err(ProvenBatchError::NoteCreatedAndConsumed(note_id));
345 }
346 }
347
348 Ok(())
349}
350
351impl Serializable for ProvenBatch {
355 fn write_into<W: ByteWriter>(&self, target: &mut W) {
356 self.reference_block_commitment.write_into(target);
357 self.reference_block_num.write_into(target);
358 self.account_updates.write_into(target);
359 self.input_notes.write_into(target);
360 self.output_notes.write_into(target);
361 self.batch_expiration_block_num.write_into(target);
362 self.transactions.write_into(target);
363 self.proof.write_into(target);
364 }
365}
366
367impl Deserializable for ProvenBatch {
368 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
369 let reference_block_commitment = Word::read_from(source)?;
370 let reference_block_num = BlockNumber::read_from(source)?;
371 let account_updates = BTreeMap::<AccountId, BatchAccountUpdate>::read_from(source)?;
372 let input_notes = InputNotes::<InputNoteCommitment>::read_from(source)?;
373 let output_notes = Vec::<OutputNote>::read_from(source)?;
374 let batch_expiration_block_num = BlockNumber::read_from(source)?;
375 let transactions = OrderedTransactionHeaders::read_from(source)?;
376 let proof = ExecutionProof::read_from(source)?;
377
378 Self::new(
379 reference_block_commitment,
380 reference_block_num,
381 account_updates.into_values(),
382 input_notes,
383 output_notes,
384 batch_expiration_block_num,
385 transactions,
386 proof,
387 )
388 .map_err(|e| DeserializationError::UnknownError(e.to_string()))
389 }
390}
391
392#[cfg(test)]
396mod tests {
397 use alloc::collections::BTreeMap;
398 use alloc::string::ToString;
399 use alloc::vec::Vec;
400
401 use assert_matches::assert_matches;
402 use rstest::rstest;
403
404 use super::ProvenBatch;
405 use crate::account::{AccountId, AccountType, AccountUpdateDetails};
406 use crate::batch::{BatchAccountUpdate, BatchId};
407 use crate::block::BlockNumber;
408 use crate::errors::ProvenBatchError;
409 use crate::note::{Note, NoteHeader, NoteId};
410 use crate::testing::account_id::{
411 ACCOUNT_ID_PRIVATE_SENDER,
412 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
413 AccountIdBuilder,
414 };
415 use crate::testing::{
416 dummy_deferred_execution_proof,
417 dummy_execution_proof,
418 dummy_precompile_execution_proof,
419 };
420 use crate::transaction::{
421 InputNoteCommitment,
422 InputNotes,
423 OrderedTransactionHeaders,
424 OutputNote,
425 RawOutputNote,
426 TransactionHeader,
427 };
428 use crate::utils::serde::{Deserializable, Serializable};
429 use crate::{MAX_ACCOUNTS_PER_BATCH, Word};
430
431 fn account_id() -> AccountId {
432 AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap()
433 }
434
435 fn transaction_header(
436 initial_state_commitment: Word,
437 final_state_commitment: Word,
438 input_notes: InputNotes<InputNoteCommitment>,
439 output_notes: Vec<NoteHeader>,
440 ) -> TransactionHeader {
441 TransactionHeader::new(
442 account_id(),
443 initial_state_commitment,
444 final_state_commitment,
445 input_notes,
446 output_notes,
447 )
448 .unwrap()
449 }
450
451 fn transaction_headers() -> OrderedTransactionHeaders {
452 let transaction = transaction_header(
453 Word::from([1_u32, 2, 3, 4]),
454 Word::from([5_u32, 6, 7, 8]),
455 InputNotes::default(),
456 vec![],
457 );
458
459 OrderedTransactionHeaders::new_unchecked(vec![transaction])
460 }
461
462 fn private_account_update() -> BatchAccountUpdate {
463 BatchAccountUpdate::new(
464 account_id(),
465 Word::from([1_u32, 2, 3, 4]),
466 Word::from([5_u32, 6, 7, 8]),
467 AccountUpdateDetails::Private,
468 )
469 .unwrap()
470 }
471
472 fn private_account_update_for(account_id: AccountId) -> BatchAccountUpdate {
473 BatchAccountUpdate::new(
474 account_id,
475 Word::from([1_u32, 2, 3, 4]),
476 Word::from([5_u32, 6, 7, 8]),
477 AccountUpdateDetails::Private,
478 )
479 .unwrap()
480 }
481
482 fn conflicting_notes() -> (NoteId, InputNotes<InputNoteCommitment>, Vec<OutputNote>) {
483 let note = Note::mock_noop(Word::empty());
484 let note_id = note.id();
485 let input_note =
486 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
487 let output_note = RawOutputNote::Full(note).into_output_note().unwrap();
488
489 (note_id, InputNotes::new(vec![input_note]).unwrap(), vec![output_note])
490 }
491
492 fn transactions_with_conflicting_notes(
493 input_notes: &InputNotes<InputNoteCommitment>,
494 output_notes: &[OutputNote],
495 ) -> (OrderedTransactionHeaders, BatchAccountUpdate) {
496 let states = [
497 Word::from([1_u32, 2, 3, 4]),
498 Word::from([5_u32, 6, 7, 8]),
499 Word::from([9_u32, 10, 11, 12]),
500 ];
501 let output_note_headers = output_notes.iter().map(|note| *note.header()).collect();
502 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
503 transaction_header(states[0], states[1], input_notes.clone(), vec![]),
504 transaction_header(states[1], states[2], InputNotes::default(), output_note_headers),
505 ]);
506 let update = BatchAccountUpdate::new(
507 account_id(),
508 states[0],
509 states[2],
510 AccountUpdateDetails::Private,
511 )
512 .unwrap();
513
514 (transactions, update)
515 }
516
517 #[test]
518 fn accepts_note_consumed_before_created_in_transaction_headers() {
519 let (_note_id, input_notes, output_notes) = conflicting_notes();
520 let (transactions, update) =
521 transactions_with_conflicting_notes(&input_notes, &output_notes);
522
523 ProvenBatch::new(
524 Word::empty(),
525 BlockNumber::from(1),
526 vec![update],
527 InputNotes::default(),
528 Vec::new(),
529 BlockNumber::from(2),
530 transactions,
531 dummy_execution_proof(),
532 )
533 .unwrap();
534 }
535
536 #[test]
537 fn derives_account_update_keys_from_updates() {
538 let update = private_account_update();
539 let account_id = update.account_id();
540
541 let batch = ProvenBatch::new(
542 Word::empty(),
543 BlockNumber::from(1),
544 vec![update],
545 InputNotes::default(),
546 Vec::new(),
547 BlockNumber::from(2),
548 transaction_headers(),
549 dummy_execution_proof(),
550 )
551 .unwrap();
552
553 assert_eq!(batch.account_updates().keys().copied().collect::<Vec<_>>(), vec![account_id]);
554 }
555
556 #[test]
557 fn rejects_proofs_with_precompiles() {
558 for proof in [dummy_deferred_execution_proof(), dummy_precompile_execution_proof()] {
559 let error = ProvenBatch::new(
560 Word::empty(),
561 BlockNumber::from(1),
562 vec![private_account_update()],
563 InputNotes::default(),
564 Vec::new(),
565 BlockNumber::from(2),
566 transaction_headers(),
567 proof,
568 )
569 .unwrap_err();
570
571 assert_matches!(error, ProvenBatchError::BatchProofContainsPrecompiles);
572 }
573 }
574
575 #[test]
576 fn rejects_duplicate_account_updates() {
577 let update = private_account_update();
578 let account_id = update.account_id();
579
580 let error = ProvenBatch::new(
581 Word::empty(),
582 BlockNumber::from(1),
583 vec![update.clone(), update],
584 InputNotes::default(),
585 Vec::new(),
586 BlockNumber::from(2),
587 transaction_headers(),
588 dummy_execution_proof(),
589 )
590 .unwrap_err();
591
592 assert_matches!(error, ProvenBatchError::DuplicateAccountUpdate(id) if id == account_id);
593 }
594
595 #[test]
596 fn rejects_too_many_account_updates_without_consuming_the_tail() {
597 let mut next_index = 0_u64;
598 let account_updates = core::iter::from_fn(move || {
599 assert!(
600 next_index <= MAX_ACCOUNTS_PER_BATCH as u64,
601 "account update iterator was consumed past the batch limit"
602 );
603
604 let mut seed = [0_u8; 32];
605 seed[..8].copy_from_slice(&next_index.to_le_bytes());
606 next_index += 1;
607
608 let account_id =
609 AccountIdBuilder::new().account_type(AccountType::Private).build_with_seed(seed);
610 Some(private_account_update_for(account_id))
611 });
612
613 let error = ProvenBatch::new(
614 Word::empty(),
615 BlockNumber::from(1),
616 account_updates,
617 InputNotes::default(),
618 Vec::new(),
619 BlockNumber::from(2),
620 transaction_headers(),
621 dummy_execution_proof(),
622 )
623 .unwrap_err();
624
625 assert_matches!(
626 &error,
627 ProvenBatchError::TooManyAccountUpdates(count)
628 if *count == MAX_ACCOUNTS_PER_BATCH + 1
629 );
630 assert_eq!(
631 error.to_string(),
632 format!(
633 "transaction batch has at least {} account updates but at most {MAX_ACCOUNTS_PER_BATCH} are allowed",
634 MAX_ACCOUNTS_PER_BATCH + 1
635 )
636 );
637 }
638
639 #[test]
640 fn rejects_missing_account_update() {
641 let error = ProvenBatch::new(
642 Word::empty(),
643 BlockNumber::from(1),
644 Vec::new(),
645 InputNotes::default(),
646 Vec::new(),
647 BlockNumber::from(2),
648 transaction_headers(),
649 dummy_execution_proof(),
650 )
651 .unwrap_err();
652
653 assert_matches!(error, ProvenBatchError::MissingAccountUpdate(id) if id == account_id());
654 }
655
656 #[test]
657 fn rejects_unexpected_account_update() {
658 let unexpected_account_id =
659 AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
660
661 let error = ProvenBatch::new(
662 Word::empty(),
663 BlockNumber::from(1),
664 vec![private_account_update(), private_account_update_for(unexpected_account_id)],
665 InputNotes::default(),
666 Vec::new(),
667 BlockNumber::from(2),
668 transaction_headers(),
669 dummy_execution_proof(),
670 )
671 .unwrap_err();
672
673 assert_matches!(
674 error,
675 ProvenBatchError::UnexpectedAccountUpdate(id) if id == unexpected_account_id
676 );
677 }
678
679 #[rstest]
680 #[case::initial(true)]
681 #[case::final_state(false)]
682 fn rejects_account_update_commitment_mismatch(#[case] mismatch_initial: bool) {
683 let expected_initial = Word::from([1_u32, 2, 3, 4]);
684 let expected_final = Word::from([5_u32, 6, 7, 8]);
685 let actual_initial = if mismatch_initial {
686 Word::from([9_u32, 2, 3, 4])
687 } else {
688 expected_initial
689 };
690 let actual_final = if mismatch_initial {
691 expected_final
692 } else {
693 Word::from([9_u32, 6, 7, 8])
694 };
695 let update = BatchAccountUpdate::new(
696 account_id(),
697 actual_initial,
698 actual_final,
699 AccountUpdateDetails::Private,
700 )
701 .unwrap();
702
703 let error = ProvenBatch::new(
704 Word::empty(),
705 BlockNumber::from(1),
706 vec![update],
707 InputNotes::default(),
708 Vec::new(),
709 BlockNumber::from(2),
710 transaction_headers(),
711 dummy_execution_proof(),
712 )
713 .unwrap_err();
714
715 if mismatch_initial {
716 assert_matches!(
717 error,
718 ProvenBatchError::AccountUpdateInitialStateMismatch { account_id: id, .. }
719 if id == account_id()
720 );
721 } else {
722 assert_matches!(
723 error,
724 ProvenBatchError::AccountUpdateFinalStateMismatch { account_id: id, .. }
725 if id == account_id()
726 );
727 }
728 }
729
730 #[test]
731 fn rejects_non_chained_transaction_headers() {
732 let initial = Word::from([1_u32, 2, 3, 4]);
733 let intermediate = Word::from([5_u32, 6, 7, 8]);
734 let unexpected = Word::from([9_u32, 10, 11, 12]);
735 let final_state = Word::from([13_u32, 14, 15, 16]);
736 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
737 transaction_header(initial, intermediate, InputNotes::default(), vec![]),
738 transaction_header(unexpected, final_state, InputNotes::default(), vec![]),
739 ]);
740 let update = BatchAccountUpdate::new(
741 account_id(),
742 initial,
743 final_state,
744 AccountUpdateDetails::Private,
745 )
746 .unwrap();
747
748 let error = ProvenBatch::new(
749 Word::empty(),
750 BlockNumber::from(1),
751 vec![update],
752 InputNotes::default(),
753 Vec::new(),
754 BlockNumber::from(2),
755 transactions,
756 dummy_execution_proof(),
757 )
758 .unwrap_err();
759
760 assert_matches!(
761 error,
762 ProvenBatchError::TransactionAccountStateMismatch { account_id: id, .. }
763 if id == account_id()
764 );
765 }
766
767 #[test]
768 fn accepts_input_notes_missing_from_batch() {
769 let note = Note::mock_noop(Word::empty());
770 let input =
771 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
772 let transactions = OrderedTransactionHeaders::new_unchecked(vec![transaction_header(
773 Word::from([1_u32, 2, 3, 4]),
774 Word::from([5_u32, 6, 7, 8]),
775 InputNotes::new(vec![input]).unwrap(),
776 vec![],
777 )]);
778
779 ProvenBatch::new(
780 Word::empty(),
781 BlockNumber::from(1),
782 vec![private_account_update()],
783 InputNotes::default(),
784 Vec::new(),
785 BlockNumber::from(2),
786 transactions,
787 dummy_execution_proof(),
788 )
789 .unwrap();
790 }
791
792 #[test]
793 fn deserialization_accepts_note_consumed_before_created_in_transaction_headers() {
794 let (_note_id, input_notes, output_notes) = conflicting_notes();
795 let (transactions, update) =
796 transactions_with_conflicting_notes(&input_notes, &output_notes);
797 let id =
798 BatchId::from_ids(transactions.as_slice().iter().map(|tx| (tx.id(), tx.account_id())));
799 let invalid_batch = ProvenBatch::new_unchecked(
800 id,
801 Word::empty(),
802 BlockNumber::from(1),
803 BTreeMap::from([(account_id(), update)]),
804 InputNotes::default(),
805 Vec::new(),
806 BlockNumber::from(2),
807 transactions,
808 dummy_execution_proof(),
809 )
810 .unwrap();
811
812 ProvenBatch::read_from_bytes(&invalid_batch.to_bytes()).unwrap();
813 }
814
815 #[test]
816 fn deserialization_accepts_input_notes_missing_from_batch() {
817 let note = Note::mock_noop(Word::empty());
818 let input =
819 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
820 let transactions = OrderedTransactionHeaders::new_unchecked(vec![transaction_header(
821 Word::from([1_u32, 2, 3, 4]),
822 Word::from([5_u32, 6, 7, 8]),
823 InputNotes::new(vec![input]).unwrap(),
824 vec![],
825 )]);
826 let id =
827 BatchId::from_ids(transactions.as_slice().iter().map(|tx| (tx.id(), tx.account_id())));
828 let invalid_batch = ProvenBatch::new_unchecked(
829 id,
830 Word::empty(),
831 BlockNumber::from(1),
832 BTreeMap::from([(account_id(), private_account_update())]),
833 InputNotes::default(),
834 Vec::new(),
835 BlockNumber::from(2),
836 transactions,
837 dummy_execution_proof(),
838 )
839 .unwrap();
840
841 ProvenBatch::read_from_bytes(&invalid_batch.to_bytes()).unwrap();
842 }
843
844 #[test]
845 fn accepts_output_note_missing_from_transaction_headers() {
846 let (_, _, output_notes) = conflicting_notes();
847
848 ProvenBatch::new(
849 Word::empty(),
850 BlockNumber::from(1),
851 vec![private_account_update()],
852 InputNotes::default(),
853 output_notes,
854 BlockNumber::from(2),
855 transaction_headers(),
856 dummy_execution_proof(),
857 )
858 .unwrap();
859 }
860
861 #[test]
862 fn accepts_output_note_missing_from_batch() {
863 let note = Note::mock_noop(Word::empty());
864 let transactions = OrderedTransactionHeaders::new_unchecked(vec![transaction_header(
865 Word::from([1_u32, 2, 3, 4]),
866 Word::from([5_u32, 6, 7, 8]),
867 InputNotes::default(),
868 vec![*note.header()],
869 )]);
870
871 ProvenBatch::new(
872 Word::empty(),
873 BlockNumber::from(1),
874 vec![private_account_update()],
875 InputNotes::default(),
876 Vec::new(),
877 BlockNumber::from(2),
878 transactions,
879 dummy_execution_proof(),
880 )
881 .unwrap();
882 }
883
884 #[test]
885 fn rejects_duplicate_supplied_input_note() {
886 let note = Note::mock_noop(Word::empty());
887 let input =
888 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
889 let input_notes = InputNotes::new_unchecked(vec![input.clone(), input]);
890
891 let error = ProvenBatch::new(
892 Word::empty(),
893 BlockNumber::from(1),
894 vec![private_account_update()],
895 input_notes,
896 Vec::new(),
897 BlockNumber::from(2),
898 transaction_headers(),
899 dummy_execution_proof(),
900 )
901 .unwrap_err();
902
903 assert_matches!(
904 error,
905 ProvenBatchError::DuplicateInputNote(nullifier) if nullifier == note.nullifier()
906 );
907 }
908
909 #[test]
910 fn rejects_duplicate_supplied_output_note() {
911 let note = Note::mock_noop(Word::empty());
912 let output_note = RawOutputNote::Full(note.clone()).into_output_note().unwrap();
913
914 let error = ProvenBatch::new(
915 Word::empty(),
916 BlockNumber::from(1),
917 vec![private_account_update()],
918 InputNotes::default(),
919 vec![output_note.clone(), output_note],
920 BlockNumber::from(2),
921 transaction_headers(),
922 dummy_execution_proof(),
923 )
924 .unwrap_err();
925
926 assert_matches!(
927 error,
928 ProvenBatchError::DuplicateOutputNote(note_id) if note_id == note.id()
929 );
930 }
931
932 #[test]
933 fn rejects_supplied_input_output_overlap() {
934 let (note_id, input_notes, output_notes) = conflicting_notes();
935
936 let error = ProvenBatch::new(
937 Word::empty(),
938 BlockNumber::from(1),
939 vec![private_account_update()],
940 input_notes,
941 output_notes,
942 BlockNumber::from(2),
943 transaction_headers(),
944 dummy_execution_proof(),
945 )
946 .unwrap_err();
947
948 assert_matches!(error, ProvenBatchError::NoteCreatedAndConsumed(id) if id == note_id);
949 }
950}