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#[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    // CONSTRUCTORS
51    // --------------------------------------------------------------------------------------------
52
53    /// Creates a new [`ProvenBatch`] from the provided parts and validates its local structural
54    /// constraints.
55    ///
56    /// This verifies that the account updates form the state transitions described by the
57    /// [`TransactionHeader`]s. It also checks the supplied input and output notes for duplicates
58    /// and known overlap, but does not verify that they are the correctly aggregated note sets
59    /// derived from the transaction headers.
60    ///
61    /// This does not verify the execution proof, per-transaction reference blocks or expiration
62    /// block numbers, or note inclusion proofs. Those checks require data which is not present in
63    /// a [`TransactionHeader`] and must be performed before constructing the proven batch or by a
64    /// batch verifier.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the proof contains precompiles, any local structural limit or invariant
69    /// is violated, or the aggregate account updates do not match the transaction headers.
70    #[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    /// Creates a new [`ProvenBatch`] from the provided parts without checking any constraints
137    /// except the expiration constraint listed below.
138    ///
139    /// Callers must ensure that the batch satisfies the structural constraints checked by
140    /// [`ProvenBatch::new`].
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the batch expiration block number is not greater than the reference
145    /// block number.
146    #[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        // Check that the batch expiration block number is greater than the reference block number.
159        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    // PUBLIC ACCESSORS
180    // --------------------------------------------------------------------------------------------
181
182    /// The ID of this batch. See [`BatchId`] for details on how it is computed.
183    pub fn id(&self) -> BatchId {
184        self.id
185    }
186
187    /// Returns the commitment to the reference block of the batch.
188    pub fn reference_block_commitment(&self) -> Word {
189        self.reference_block_commitment
190    }
191
192    /// Returns the number of the reference block of the batch.
193    pub fn reference_block_num(&self) -> BlockNumber {
194        self.reference_block_num
195    }
196
197    /// Returns the block number at which the batch will expire.
198    pub fn batch_expiration_block_num(&self) -> BlockNumber {
199        self.batch_expiration_block_num
200    }
201
202    /// Returns an iterator over the IDs of all accounts updated in this batch.
203    pub fn updated_accounts(&self) -> impl Iterator<Item = AccountId> + use<'_> {
204        self.account_updates.keys().copied()
205    }
206
207    /// Returns the proof security level of the batch.
208    pub fn proof_security_level(&self) -> u32 {
209        MIN_PROOF_SECURITY_LEVEL
210    }
211
212    /// Returns the map of account IDs mapped to their [`BatchAccountUpdate`]s.
213    ///
214    /// If an account was updated by multiple transactions, the [`BatchAccountUpdate`] is the result
215    /// of merging the individual updates.
216    ///
217    /// For example, suppose an account's state before this batch is `A` and the batch contains two
218    /// transactions that updated it. Applying the first transaction results in intermediate state
219    /// `B`, and applying the second one results in state `C`. Then the returned update represents
220    /// the state transition from `A` to `C`.
221    pub fn account_updates(&self) -> &BTreeMap<AccountId, BatchAccountUpdate> {
222        &self.account_updates
223    }
224
225    /// Returns the input notes supplied for this batch.
226    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
227        &self.input_notes
228    }
229
230    /// Returns an iterator over the nullifiers derived from the supplied input notes.
231    pub fn created_nullifiers(&self) -> impl Iterator<Item = Nullifier> + use<'_> {
232        self.input_notes.iter().map(InputNoteCommitment::nullifier)
233    }
234
235    /// Returns the output notes supplied for this batch.
236    pub fn output_notes(&self) -> &[OutputNote] {
237        &self.output_notes
238    }
239
240    /// Returns the [`OrderedTransactionHeaders`] included in this batch.
241    pub fn transactions(&self) -> &OrderedTransactionHeaders {
242        &self.transactions
243    }
244
245    /// Returns the execution proof attached to this batch.
246    pub fn proof(&self) -> &ExecutionProof {
247        &self.proof
248    }
249
250    // MUTATORS
251    // --------------------------------------------------------------------------------------------
252
253    /// Consumes self and returns the contained [`OrderedTransactionHeaders`] of this batch.
254    pub fn into_transactions(self) -> OrderedTransactionHeaders {
255        self.transactions
256    }
257}
258
259// VALIDATION HELPERS
260// ================================================================================================
261
262fn 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(&note_id) {
344            return Err(ProvenBatchError::NoteCreatedAndConsumed(note_id));
345        }
346    }
347
348    Ok(())
349}
350
351// SERIALIZATION
352// ================================================================================================
353
354impl 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// TESTS
393// ================================================================================================
394
395#[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}