Skip to main content

miden_protocol/batch/
proven_batch.rs

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/// A transaction batch with an execution proof.
35/// Currently, this only carries a skeleton proof which does not attest to anything meaningful.
36///
37/// The batch's transactions may carry outstanding precompile claims. The batch prover settles them
38/// with a single precompile proof, but that proof is not part of the proven batch (yet), so a party
39/// that holds only a [`ProvenBatch`] cannot check those claims. This goes away once the batch
40/// kernel verifies the precompile proof in-circuit, in which case the batch proof will recursively
41/// attest to the veracity of the precompile claims.
42#[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    // CONSTRUCTORS
57    // --------------------------------------------------------------------------------------------
58
59    /// Creates a new [`ProvenBatch`] from the provided parts and validates its local structural
60    /// constraints.
61    ///
62    /// This verifies that the account updates form the state transitions described by the
63    /// [`TransactionHeader`]s. It also checks the supplied input and output notes for duplicates
64    /// and known overlap, but does not verify that they are the correctly aggregated note sets
65    /// derived from the transaction headers.
66    ///
67    /// This does not verify the execution proof, per-transaction reference blocks or expiration
68    /// block numbers, or note inclusion proofs. Those checks require data which is not present in
69    /// a [`TransactionHeader`] and must be performed before constructing the proven batch or by a
70    /// batch verifier.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if the proof contains precompiles, any local structural limit or invariant
75    /// is violated, or the aggregate account updates do not match the transaction headers.
76    #[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    /// Creates a new [`ProvenBatch`] from the provided parts without checking any constraints
143    /// except the expiration constraint listed below.
144    ///
145    /// Callers must ensure that the batch satisfies the structural constraints checked by
146    /// [`ProvenBatch::new`].
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the batch expiration block number is not greater than the reference
151    /// block number.
152    #[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        // Check that the batch expiration block number is greater than the reference block number.
165        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    // PUBLIC ACCESSORS
186    // --------------------------------------------------------------------------------------------
187
188    /// The ID of this batch. See [`BatchId`] for details on how it is computed.
189    pub fn id(&self) -> BatchId {
190        self.id
191    }
192
193    /// Returns the commitment to the reference block of the batch.
194    pub fn reference_block_commitment(&self) -> Word {
195        self.reference_block_commitment
196    }
197
198    /// Returns the number of the reference block of the batch.
199    pub fn reference_block_num(&self) -> BlockNumber {
200        self.reference_block_num
201    }
202
203    /// Returns the block number at which the batch will expire.
204    pub fn batch_expiration_block_num(&self) -> BlockNumber {
205        self.batch_expiration_block_num
206    }
207
208    /// Returns an iterator over the IDs of all accounts updated in this batch.
209    pub fn updated_accounts(&self) -> impl Iterator<Item = AccountId> + use<'_> {
210        self.account_updates.keys().copied()
211    }
212
213    /// Returns the proof security level of the batch.
214    pub fn proof_security_level(&self) -> u32 {
215        MIN_PROOF_SECURITY_LEVEL
216    }
217
218    /// Returns the map of account IDs mapped to their [`BatchAccountUpdate`]s.
219    ///
220    /// If an account was updated by multiple transactions, the [`BatchAccountUpdate`] is the result
221    /// of merging the individual updates.
222    ///
223    /// For example, suppose an account's state before this batch is `A` and the batch contains two
224    /// transactions that updated it. Applying the first transaction results in intermediate state
225    /// `B`, and applying the second one results in state `C`. Then the returned update represents
226    /// the state transition from `A` to `C`.
227    pub fn account_updates(&self) -> &BTreeMap<AccountId, BatchAccountUpdate> {
228        &self.account_updates
229    }
230
231    /// Returns the input notes supplied for this batch.
232    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
233        &self.input_notes
234    }
235
236    /// Returns an iterator over the nullifiers derived from the supplied input notes.
237    pub fn created_nullifiers(&self) -> impl Iterator<Item = Nullifier> + use<'_> {
238        self.input_notes.iter().map(InputNoteCommitment::nullifier)
239    }
240
241    /// Returns the output notes supplied for this batch.
242    pub fn output_notes(&self) -> &[OutputNote] {
243        &self.output_notes
244    }
245
246    /// Returns the [`OrderedTransactionHeaders`] included in this batch.
247    pub fn transactions(&self) -> &OrderedTransactionHeaders {
248        &self.transactions
249    }
250
251    /// Returns the execution proof attached to this batch.
252    pub fn proof(&self) -> &ExecutionProof {
253        &self.proof
254    }
255
256    // MUTATORS
257    // --------------------------------------------------------------------------------------------
258
259    /// Consumes self and returns the contained [`OrderedTransactionHeaders`] of this batch.
260    pub fn into_transactions(self) -> OrderedTransactionHeaders {
261        self.transactions
262    }
263}
264
265// VALIDATION HELPERS
266// ================================================================================================
267
268fn 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(&note_id) {
350            return Err(ProvenBatchError::NoteCreatedAndConsumed(note_id));
351        }
352    }
353
354    Ok(())
355}
356
357// SERIALIZATION
358// ================================================================================================
359
360impl 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// TESTS
399// ================================================================================================
400
401#[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}