Skip to main content

miden_protocol/batch/
proposed_batch.rs

1use alloc::collections::btree_map::Entry;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use crate::account::AccountId;
7use crate::batch::note_tracker::{NoteTracker, TrackerOutput};
8use crate::batch::{BatchAccountUpdate, BatchId};
9use crate::block::{BlockHeader, BlockNumber};
10use crate::errors::ProposedBatchError;
11use crate::note::{NoteId, NoteInclusionProof};
12use crate::transaction::{
13    InputNoteCommitment,
14    InputNotes,
15    OrderedTransactionHeaders,
16    OutputNote,
17    PartialBlockchain,
18    ProvenTransaction,
19    TransactionHeader,
20    TransactionVerifier,
21};
22use crate::utils::serde::{
23    ByteReader,
24    ByteWriter,
25    Deserializable,
26    DeserializationError,
27    Serializable,
28};
29use crate::{MAX_ACCOUNTS_PER_BATCH, MAX_INPUT_NOTES_PER_BATCH, MAX_OUTPUT_NOTES_PER_BATCH};
30
31/// A proposed batch of transactions with all necessary data to validate it.
32///
33/// See [`ProposedBatch::new`] for what a proposed batch expects and guarantees.
34///
35/// This type is fairly large, so consider boxing it.
36#[derive(Debug, Clone)]
37pub struct ProposedBatch {
38    /// The transactions of this batch.
39    transactions: Vec<Arc<ProvenTransaction>>,
40    /// The header of the reference block that this batch is proposed for.
41    reference_block_header: BlockHeader,
42    /// The partial blockchain used to authenticate:
43    /// - all unauthenticated notes that can be authenticated,
44    /// - all block commitments referenced by the transactions in the batch.
45    partial_blockchain: PartialBlockchain,
46    /// The note inclusion proofs for unauthenticated notes that were consumed in the batch which
47    /// can be authenticated.
48    unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
49    /// The ID of the batch, which is a cryptographic commitment to the transactions in the batch.
50    id: BatchId,
51    /// A map from account ID's updated in this batch to the aggregated update from all
52    /// transaction's that touched the account.
53    account_updates: BTreeMap<AccountId, BatchAccountUpdate>,
54    /// The block number at which the batch will expire. This is the minimum of all transaction's
55    /// expiration block number.
56    batch_expiration_block_num: BlockNumber,
57    /// The input note commitment of the transaction batch. This consists of all authenticated
58    /// notes that transactions in the batch consume as well as unauthenticated notes whose
59    /// authentication is delayed to the block kernel. These are sorted by
60    /// [`InputNoteCommitment::nullifier`].
61    input_notes: InputNotes<InputNoteCommitment>,
62    /// The output notes of this batch. This consists of all notes created by transactions in the
63    /// batch that are not consumed within the same batch. These are sorted by
64    /// [`OutputNote::id`].
65    output_notes: Vec<OutputNote>,
66}
67
68impl ProposedBatch {
69    // CONSTRUCTORS
70    // --------------------------------------------------------------------------------------------
71
72    /// Creates a new [`ProposedBatch`] from the provided parts.
73    ///
74    /// # Inputs
75    ///
76    /// - The given transactions must be correctly ordered. That is, if two transactions A and B
77    ///   update the same account in this order, meaning A's initial account state commitment
78    ///   matches the account state before any transactions are executed and B's initial account
79    ///   state commitment matches the final account state commitment of A, then A must come before
80    ///   B.
81    /// - The partial blockchain's hashed peaks must match the reference block's `chain_commitment`
82    ///   and it must contain all block headers:
83    ///   - that are referenced by note inclusion proofs in `unauthenticated_note_proofs`.
84    ///   - that are referenced by a transaction in the batch.
85    /// - The `unauthenticated_note_proofs` should contain [`NoteInclusionProof`]s for any
86    ///   unauthenticated note consumed by the transaction's in the batch which can be
87    ///   authenticated. This means it is not required that every unauthenticated note has an entry
88    ///   in this map for two reasons.
89    ///     - Unauthenticated note authentication can be delayed to the block kernel.
90    ///     - Another transaction in the batch creates an output note matching an unauthenticated
91    ///       input note, in which case inclusion in the chain does not need to be proven.
92    /// - The reference block of a batch must satisfy the following requirement: Its block number
93    ///   must be greater or equal to the highest block number referenced by any transaction. This
94    ///   is not verified explicitly, but will implicitly cause an error during the validation that
95    ///   each reference block of a transaction is in the partial blockchain.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if:
100    ///
101    /// - The number of input notes exceeds [`MAX_INPUT_NOTES_PER_BATCH`].
102    ///   - Note that unauthenticated notes that are created in the same batch do not count. Any
103    ///     other input notes, unauthenticated or not, do count.
104    /// - The number of output notes exceeds [`MAX_OUTPUT_NOTES_PER_BATCH`].
105    ///   - Note that output notes that are consumed in the same batch as unauthenticated input
106    ///     notes do not count.
107    /// - Any note is consumed more than once.
108    /// - Any note is created more than once.
109    /// - An unauthenticated note is consumed before it is created (as determined by the order in
110    ///   which transactions are given).
111    /// - The number of account updates exceeds [`MAX_ACCOUNTS_PER_BATCH`].
112    ///   - Note that any number of transactions against the same account count as one update.
113    /// - The partial blockchains chain length does not match the block header's block number. This
114    ///   means the partial blockchain should not contain the block header itself as it is added to
115    ///   the MMR in the batch kernel.
116    /// - The partial blockchains hashed peaks do not match the block header's chain commitment.
117    /// - The reference block of any transaction is not in the partial blockchain.
118    /// - The note inclusion proof for an unauthenticated note fails to verify.
119    /// - The block referenced by a note inclusion proof for an unauthenticated note is missing from
120    ///   the partial blockchain.
121    /// - The transactions in the proposed batch which update the same account are not correctly
122    ///   ordered.
123    /// - The provided list of transactions is empty. An empty batch is pointless and would
124    ///   potentially result in the same [`BatchId`] for two empty batches which would mean batch
125    ///   IDs are no longer unique.
126    /// - There are duplicate transactions.
127    /// - If any transaction's expiration block number is less than or equal to the batch's
128    ///   reference block.
129    fn new_batch_inner(
130        transactions: Vec<Arc<ProvenTransaction>>,
131        reference_block_header: BlockHeader,
132        partial_blockchain: PartialBlockchain,
133        unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
134    ) -> Result<Self, ProposedBatchError> {
135        // Check for empty or duplicate transactions.
136        // --------------------------------------------------------------------------------------------
137
138        if transactions.is_empty() {
139            return Err(ProposedBatchError::EmptyTransactionBatch);
140        }
141
142        let mut transaction_set = BTreeSet::new();
143        for tx in transactions.iter() {
144            if !transaction_set.insert(tx.id()) {
145                return Err(ProposedBatchError::DuplicateTransaction { transaction_id: tx.id() });
146            }
147        }
148
149        // Verify block header and partial blockchain match.
150        // --------------------------------------------------------------------------------------------
151
152        if partial_blockchain.chain_length() != reference_block_header.block_num() {
153            return Err(ProposedBatchError::InconsistentChainLength {
154                expected: reference_block_header.block_num(),
155                actual: partial_blockchain.chain_length(),
156            });
157        }
158
159        let hashed_peaks = partial_blockchain.peaks().hash_peaks();
160        if hashed_peaks != reference_block_header.chain_commitment() {
161            return Err(ProposedBatchError::InconsistentChainRoot {
162                expected: reference_block_header.chain_commitment(),
163                actual: hashed_peaks,
164            });
165        }
166
167        // Verify all block references from the transactions are in the partial blockchain, except
168        // for the batch's reference block.
169        //
170        // Note that some block X is only added to the blockchain by block X + 1. This
171        // is because block X cannot compute its own block commitment and thus cannot add
172        // itself to the chain. So, more generally, a block is added to the blockchain by its child
173        // block.
174        //
175        // The reference block of a batch may be the latest block in the chain and, as mentioned,
176        // the block is not yet part of the blockchain, so its inclusion cannot be proven.
177        // Since the inclusion cannot be proven, the batch kernel instead commits to this reference
178        // block's commitment as a public input, which means the block kernel will prove
179        // this block's inclusion when including this batch and verifying its ZK proof.
180        //
181        // Finally, note that we don't verify anything cryptographically here. We have previously
182        // verified that the chain commitment of the batch's reference block matches the hashed
183        // peaks of the `PartialBlockchain`. This means the provided blockchain is consistent with
184        // the batch's reference block and that all blocks contained in the blockchain are
185        // consistent, too. So, as long as each transaction's reference block (number and
186        // commitment) is contained in the partial blockchain, we know the transaction's
187        // block header is consistent with the batch's reference block, too.
188        // --------------------------------------------------------------------------------------------
189
190        for tx in transactions.iter() {
191            // Differentiate between validation against the batch's reference block or a block from
192            // the chain (see above).
193            if reference_block_header.block_num() == tx.ref_block_num() {
194                if reference_block_header.commitment() != tx.ref_block_commitment() {
195                    return Err(ProposedBatchError::TransactionReferenceBlockCommitmentMismatch {
196                        transaction_id: tx.id(),
197                        block_num: tx.ref_block_num(),
198                        actual_block_commitment: tx.ref_block_commitment(),
199                        expected_block_commitment: reference_block_header.commitment(),
200                    });
201                }
202            } else {
203                let block_header =
204                    partial_blockchain.get_block(tx.ref_block_num()).ok_or_else(|| {
205                        ProposedBatchError::MissingTransactionReferenceBlock {
206                            transaction_id: tx.id(),
207                            block_num: tx.ref_block_num(),
208                        }
209                    })?;
210
211                if block_header.commitment() != tx.ref_block_commitment() {
212                    return Err(ProposedBatchError::TransactionReferenceBlockCommitmentMismatch {
213                        transaction_id: tx.id(),
214                        block_num: tx.ref_block_num(),
215                        actual_block_commitment: tx.ref_block_commitment(),
216                        expected_block_commitment: block_header.commitment(),
217                    });
218                }
219            }
220        }
221
222        // Aggregate individual tx-level account updates into a batch-level account update - one per
223        // account.
224        // --------------------------------------------------------------------------------------------
225
226        // Populate batch output notes and updated accounts.
227        let mut account_updates = BTreeMap::<AccountId, BatchAccountUpdate>::new();
228        for tx in transactions.iter() {
229            // Merge account updates so that state transitions A->B->C become A->C.
230            match account_updates.entry(tx.account_id()) {
231                Entry::Vacant(vacant) => {
232                    let batch_account_update = BatchAccountUpdate::from_transaction(tx);
233                    vacant.insert(batch_account_update);
234                },
235                Entry::Occupied(occupied) => {
236                    // This returns an error if the transactions are not correctly ordered, e.g. if
237                    // B comes before A.
238                    occupied.into_mut().merge_proven_tx(tx).map_err(|source| {
239                        ProposedBatchError::AccountUpdateError {
240                            account_id: tx.account_id(),
241                            source,
242                        }
243                    })?;
244                },
245            };
246        }
247
248        if account_updates.len() > MAX_ACCOUNTS_PER_BATCH {
249            return Err(ProposedBatchError::TooManyAccountUpdates(account_updates.len()));
250        }
251
252        // Check that all transaction's expiration block numbers are greater than the reference
253        // block.
254        // --------------------------------------------------------------------------------------------
255
256        let mut batch_expiration_block_num = BlockNumber::from(u32::MAX);
257        for tx in transactions.iter() {
258            if tx.expiration_block_num() <= reference_block_header.block_num() {
259                return Err(ProposedBatchError::ExpiredTransaction {
260                    transaction_id: tx.id(),
261                    transaction_expiration_num: tx.expiration_block_num(),
262                    reference_block_num: reference_block_header.block_num(),
263                });
264            }
265
266            // The expiration block of the batch is the minimum of all transaction's expiration
267            // block.
268            batch_expiration_block_num = batch_expiration_block_num.min(tx.expiration_block_num());
269        }
270
271        // Check for duplicates in input notes.
272        // --------------------------------------------------------------------------------------------
273
274        // Check for duplicate input notes both within a transaction and across transactions.
275        // This also includes authenticated notes, as the transaction kernel doesn't check for
276        // duplicates.
277        let mut input_note_map = BTreeMap::new();
278
279        for tx in transactions.iter() {
280            for note in tx.input_notes() {
281                let nullifier = note.nullifier();
282                if let Some(first_transaction_id) = input_note_map.insert(nullifier, tx.id()) {
283                    return Err(ProposedBatchError::DuplicateInputNote {
284                        note_nullifier: nullifier,
285                        first_transaction_id,
286                        second_transaction_id: tx.id(),
287                    });
288                }
289            }
290        }
291
292        // Create input and output note set of the batch.
293        // --------------------------------------------------------------------------------------------
294
295        // Check for duplicate output notes and remove all output notes from the batch output note
296        // set that are consumed by transactions.
297        let mut tracker = NoteTracker::new(
298            &partial_blockchain,
299            &reference_block_header,
300            &unauthenticated_note_proofs,
301        );
302        for tx in transactions.iter() {
303            tracker.push(tx.as_ref()).map_err(ProposedBatchError::from)?;
304        }
305        let TrackerOutput { input_notes, output_notes, .. } =
306            tracker.finalize().map_err(ProposedBatchError::from)?;
307
308        // Collect the remaining (non-erased) output notes into the final set of output notes.
309        let output_notes: Vec<OutputNote> =
310            output_notes.into_values().map(|(_, output_note)| output_note).collect();
311
312        if input_notes.len() > MAX_INPUT_NOTES_PER_BATCH {
313            return Err(ProposedBatchError::TooManyInputNotes(input_notes.len()));
314        }
315        // SAFETY: This is safe as we have checked for duplicates and the max number of input notes
316        // in a batch.
317        let input_notes = InputNotes::new_unchecked(input_notes);
318
319        if output_notes.len() > MAX_OUTPUT_NOTES_PER_BATCH {
320            return Err(ProposedBatchError::TooManyOutputNotes(output_notes.len()));
321        }
322
323        // Compute batch ID.
324        // --------------------------------------------------------------------------------------------
325
326        let id = BatchId::from_transactions(transactions.iter().map(AsRef::as_ref));
327
328        Ok(Self {
329            id,
330            transactions,
331            reference_block_header,
332            partial_blockchain,
333            unauthenticated_note_proofs,
334            account_updates,
335            batch_expiration_block_num,
336            input_notes,
337            output_notes,
338        })
339    }
340
341    /// Creates a new [`ProposedBatch`] from the provided parts, verifying every transaction's
342    /// execution proof against the transaction kernel.
343    ///
344    /// # Errors
345    ///
346    /// Returns an error for any of the batch-validation conditions documented on `new_batch_inner`,
347    /// or if a transaction's proof fails to verify or does not meet `proof_security_level`.
348    pub fn new(
349        transactions: Vec<Arc<ProvenTransaction>>,
350        reference_block_header: BlockHeader,
351        partial_blockchain: PartialBlockchain,
352        unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
353        proof_security_level: u32,
354    ) -> Result<Self, ProposedBatchError> {
355        let batch = Self::new_batch_inner(
356            transactions,
357            reference_block_header,
358            partial_blockchain,
359            unauthenticated_note_proofs,
360        )?;
361
362        let verifier = TransactionVerifier::new(proof_security_level);
363        for tx in batch.transactions() {
364            verifier.verify(tx).map_err(|source| {
365                ProposedBatchError::TransactionVerificationFailed {
366                    transaction_id: tx.id(),
367                    source,
368                }
369            })?;
370        }
371
372        Ok(batch)
373    }
374
375    /// Creates a new [`ProposedBatch`] **without verifying the transactions' execution proofs**.
376    ///
377    /// Runs the same batch validation as [`Self::new`] but skips proof verification. Exposed for
378    /// tests that build batches from mock transactions carrying dummy proofs.
379    #[cfg(any(test, feature = "testing"))]
380    pub fn new_unverified(
381        transactions: Vec<Arc<ProvenTransaction>>,
382        reference_block_header: BlockHeader,
383        partial_blockchain: PartialBlockchain,
384        unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
385    ) -> Result<Self, ProposedBatchError> {
386        Self::new_batch_inner(
387            transactions,
388            reference_block_header,
389            partial_blockchain,
390            unauthenticated_note_proofs,
391        )
392    }
393
394    // PUBLIC ACCESSORS
395    // --------------------------------------------------------------------------------------------
396
397    /// Returns a slice of the [`ProvenTransaction`]s in the batch.
398    pub fn transactions(&self) -> &[Arc<ProvenTransaction>] {
399        &self.transactions
400    }
401
402    /// Returns the ordered set of transactions in the batch.
403    pub fn transaction_headers(&self) -> OrderedTransactionHeaders {
404        // SAFETY: This constructs an ordered set in the order of the transactions in the batch.
405        OrderedTransactionHeaders::new_unchecked(
406            self.transactions
407                .iter()
408                .map(AsRef::as_ref)
409                .map(TransactionHeader::from)
410                .collect(),
411        )
412    }
413
414    /// Returns the map of account IDs mapped to their [`BatchAccountUpdate`]s.
415    ///
416    /// If an account was updated by multiple transactions, the [`BatchAccountUpdate`] is the result
417    /// of merging the individual updates.
418    ///
419    /// For example, suppose an account's state before this batch is `A` and the batch contains two
420    /// transactions that updated it. Applying the first transaction results in intermediate state
421    /// `B`, and applying the second one results in state `C`. Then the returned update represents
422    /// the state transition from `A` to `C`.
423    pub fn account_updates(&self) -> &BTreeMap<AccountId, BatchAccountUpdate> {
424        &self.account_updates
425    }
426
427    /// The ID of this batch. See [`BatchId`] for details on how it is computed.
428    pub fn id(&self) -> BatchId {
429        self.id
430    }
431
432    /// Returns the header of the reference block this batch is proposed for.
433    pub fn reference_block_header(&self) -> &BlockHeader {
434        &self.reference_block_header
435    }
436
437    /// Returns the block number at which the batch will expire.
438    pub fn batch_expiration_block_num(&self) -> BlockNumber {
439        self.batch_expiration_block_num
440    }
441
442    /// Returns the [`InputNotes`] of this batch.
443    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
444        &self.input_notes
445    }
446
447    /// Returns the output notes of the batch.
448    ///
449    /// This is the aggregation of all output notes by the transactions in the batch, except the
450    /// ones that were consumed within the batch itself.
451    pub fn output_notes(&self) -> &[OutputNote] {
452        &self.output_notes
453    }
454
455    /// Consumes the proposed batch and returns its underlying parts.
456    #[allow(clippy::type_complexity)]
457    pub fn into_parts(
458        self,
459    ) -> (
460        Vec<Arc<ProvenTransaction>>,
461        BlockHeader,
462        PartialBlockchain,
463        BTreeMap<NoteId, NoteInclusionProof>,
464        BatchId,
465        BTreeMap<AccountId, BatchAccountUpdate>,
466        InputNotes<InputNoteCommitment>,
467        Vec<OutputNote>,
468        BlockNumber,
469    ) {
470        (
471            self.transactions,
472            self.reference_block_header,
473            self.partial_blockchain,
474            self.unauthenticated_note_proofs,
475            self.id,
476            self.account_updates,
477            self.input_notes,
478            self.output_notes,
479            self.batch_expiration_block_num,
480        )
481    }
482}
483
484// SERIALIZATION
485// ================================================================================================
486
487impl Serializable for ProposedBatch {
488    fn write_into<W: ByteWriter>(&self, target: &mut W) {
489        self.transactions
490            .iter()
491            .map(|tx| tx.as_ref().clone())
492            .collect::<Vec<ProvenTransaction>>()
493            .write_into(target);
494
495        self.reference_block_header.write_into(target);
496        self.partial_blockchain.write_into(target);
497        self.unauthenticated_note_proofs.write_into(target);
498    }
499}
500
501impl Deserializable for ProposedBatch {
502    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
503        let transactions = Vec::<ProvenTransaction>::read_from(source)?
504            .into_iter()
505            .map(Arc::new)
506            .collect::<Vec<Arc<ProvenTransaction>>>();
507
508        let block_header = BlockHeader::read_from(source)?;
509        let partial_blockchain = PartialBlockchain::read_from(source)?;
510        let unauthenticated_note_proofs =
511            BTreeMap::<NoteId, NoteInclusionProof>::read_from(source)?;
512
513        // Reconstruct structurally without verifying the transactions' proofs.
514        ProposedBatch::new_batch_inner(
515            transactions,
516            block_header,
517            partial_blockchain,
518            unauthenticated_note_proofs,
519        )
520        .map_err(|source| {
521            DeserializationError::UnknownError(format!("failed to create proposed batch: {source}"))
522        })
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use anyhow::Context;
529    use miden_crypto::merkle::mmr::{Mmr, PartialMmr};
530    use miden_crypto::rand::test_utils::rand_value;
531    use miden_verifier::ExecutionProof;
532
533    use super::*;
534    use crate::Word;
535    use crate::account::{AccountType, AccountUpdateDetails};
536    use crate::transaction::{InputNoteCommitment, OutputNote, ProvenTransaction, TxAccountUpdate};
537
538    #[test]
539    fn proposed_batch_serialization() -> anyhow::Result<()> {
540        // create partial blockchain with 3 blocks - i.e., 2 peaks
541        let mut mmr = Mmr::default();
542        for i in 0..3 {
543            let block_header = BlockHeader::mock(i, None, None, &[], Word::empty());
544            mmr.add(block_header.commitment())
545                .expect("mmr leaf count exceeds forest leaf bound");
546        }
547        let partial_mmr: PartialMmr = mmr.peaks().into();
548        let partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
549
550        let chain_commitment = partial_blockchain.peaks().hash_peaks();
551        let note_root = rand_value::<Word>();
552        let tx_kernel_commitment = rand_value::<Word>();
553        let reference_block_header = BlockHeader::mock(
554            3,
555            Some(chain_commitment),
556            Some(note_root),
557            &[],
558            tx_kernel_commitment,
559        );
560
561        let account_id =
562            AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
563        let initial_account_commitment =
564            [2; 32].try_into().expect("failed to create initial account commitment");
565        let final_account_commitment =
566            [3; 32].try_into().expect("failed to create final account commitment");
567        let account_patch_commitment =
568            [4; 32].try_into().expect("failed to create account patch commitment");
569        let block_num = reference_block_header.block_num();
570        let block_ref = reference_block_header.commitment();
571        let expiration_block_num = reference_block_header.block_num() + 1;
572        let proof = ExecutionProof::new_dummy();
573
574        let account_update = TxAccountUpdate::new(
575            account_id,
576            initial_account_commitment,
577            final_account_commitment,
578            account_patch_commitment,
579            AccountUpdateDetails::Private,
580        )
581        .context("failed to build account update")?;
582
583        let tx = ProvenTransaction::new(
584            account_update,
585            Vec::<InputNoteCommitment>::new(),
586            Vec::<OutputNote>::new(),
587            block_num,
588            block_ref,
589            expiration_block_num,
590            proof,
591        )
592        .context("failed to build proven transaction")?;
593
594        let batch = ProposedBatch::new_unverified(
595            vec![Arc::new(tx)],
596            reference_block_header,
597            partial_blockchain,
598            BTreeMap::new(),
599        )
600        .context("failed to propose batch")?;
601
602        let encoded_batch = batch.to_bytes();
603
604        let batch2 = ProposedBatch::read_from_bytes(&encoded_batch)
605            .context("failed to deserialize proposed batch")?;
606
607        assert_eq!(batch.transactions(), batch2.transactions());
608        assert_eq!(batch.reference_block_header, batch2.reference_block_header);
609        assert_eq!(batch.partial_blockchain, batch2.partial_blockchain);
610        assert_eq!(batch.unauthenticated_note_proofs, batch2.unauthenticated_note_proofs);
611        assert_eq!(batch.id, batch2.id);
612        assert_eq!(batch.account_updates, batch2.account_updates);
613        assert_eq!(batch.batch_expiration_block_num, batch2.batch_expiration_block_num);
614        assert_eq!(batch.input_notes, batch2.input_notes);
615        assert_eq!(batch.output_notes, batch2.output_notes);
616
617        Ok(())
618    }
619}