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