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::{MAX_ACCOUNTS_PER_BATCH, MAX_INPUT_NOTES_PER_BATCH, MAX_OUTPUT_NOTES_PER_BATCH};
23
24/// A proposed batch of transactions with all necessary data to validate it.
25///
26/// See [`ProposedBatch::new`] for what a proposed batch expects and guarantees.
27///
28/// This type is fairly large, so consider boxing it.
29#[derive(Debug, Clone)]
30pub struct ProposedBatch {
31    /// The transactions of this batch.
32    transactions: Vec<Arc<ProvenTransaction>>,
33    /// The header of the reference block that this batch is proposed for.
34    reference_block_header: BlockHeader,
35    /// The partial blockchain used to authenticate:
36    /// - all unauthenticated notes that can be authenticated,
37    /// - all block commitments referenced by the transactions in the batch.
38    partial_blockchain: PartialBlockchain,
39    /// The note inclusion proofs for unauthenticated notes that were consumed in the batch which
40    /// can be authenticated.
41    unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
42    /// The ID of the batch, which is a cryptographic commitment to the transactions in the batch.
43    id: BatchId,
44    /// A map from account ID's updated in this batch to the aggregated update from all
45    /// transaction's that touched the account.
46    account_updates: BTreeMap<AccountId, BatchAccountUpdate>,
47    /// The block number at which the batch will expire. This is the minimum of all transaction's
48    /// expiration block number.
49    batch_expiration_block_num: BlockNumber,
50    /// The input note commitment of the transaction batch. This consists of all authenticated
51    /// notes that transactions in the batch consume as well as unauthenticated notes whose
52    /// authentication is delayed to the block kernel. These are sorted by
53    /// [`InputNoteCommitment::nullifier`].
54    input_notes: InputNotes<InputNoteCommitment>,
55    /// The output notes of this batch. This consists of all notes created by transactions in the
56    /// batch that are not consumed within the same batch. These are sorted by
57    /// [`OutputNote::id`].
58    output_notes: Vec<OutputNote>,
59}
60
61impl ProposedBatch {
62    // CONSTRUCTORS
63    // --------------------------------------------------------------------------------------------
64
65    /// Creates a new [`ProposedBatch`] from the provided parts.
66    ///
67    /// # Inputs
68    ///
69    /// - The given transactions must be correctly ordered. That is, if two transactions A and B
70    ///   update the same account in this order, meaning A's initial account state commitment
71    ///   matches the account state before any transactions are executed and B's initial account
72    ///   state commitment matches the final account state commitment of A, then A must come before
73    ///   B.
74    /// - The partial blockchain's hashed peaks must match the reference block's `chain_commitment`
75    ///   and it must contain all block headers:
76    ///   - that are referenced by note inclusion proofs in `unauthenticated_note_proofs`.
77    ///   - that are referenced by a transaction in the batch.
78    /// - The `unauthenticated_note_proofs` should contain [`NoteInclusionProof`]s for any
79    ///   unauthenticated note consumed by the transaction's in the batch which can be
80    ///   authenticated. This means it is not required that every unauthenticated note has an entry
81    ///   in this map for two reasons.
82    ///     - Unauthenticated note authentication can be delayed to the block kernel.
83    ///     - Another transaction in the batch creates an output note matching an unauthenticated
84    ///       input note, in which case inclusion in the chain does not need to be proven.
85    /// - The reference block of a batch must satisfy the following requirement: Its block number
86    ///   must be greater or equal to the highest block number referenced by any transaction. This
87    ///   is not verified explicitly, but will implicitly cause an error during the validation that
88    ///   each reference block of a transaction is in the partial blockchain.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if:
93    ///
94    /// - The number of input notes exceeds [`MAX_INPUT_NOTES_PER_BATCH`].
95    ///   - Note that unauthenticated notes that are created in the same batch do not count. Any
96    ///     other input notes, unauthenticated or not, do count.
97    /// - The number of output notes exceeds [`MAX_OUTPUT_NOTES_PER_BATCH`].
98    ///   - Note that output notes that are consumed in the same batch as unauthenticated input
99    ///     notes do not count.
100    /// - Any note is consumed more than once.
101    /// - Any note is created more than once.
102    /// - An unauthenticated note is consumed before it is created (as determined by the order in
103    ///   which transactions are given).
104    /// - The number of account updates exceeds [`MAX_ACCOUNTS_PER_BATCH`].
105    ///   - Note that any number of transactions against the same account count as one update.
106    /// - The partial blockchains chain length does not match the block header's block number. This
107    ///   means the partial blockchain should not contain the block header itself as it is added to
108    ///   the MMR in the batch kernel.
109    /// - The partial blockchains hashed peaks do not match the block header's chain commitment.
110    /// - The reference block of any transaction is not in the partial blockchain.
111    /// - The note inclusion proof for an unauthenticated note fails to verify.
112    /// - The block referenced by a note inclusion proof for an unauthenticated note is missing from
113    ///   the partial blockchain.
114    /// - The transactions in the proposed batch which update the same account are not correctly
115    ///   ordered.
116    /// - The provided list of transactions is empty. An empty batch is pointless and would
117    ///   potentially result in the same [`BatchId`] for two empty batches which would mean batch
118    ///   IDs are no longer unique.
119    /// - There are duplicate transactions.
120    /// - If any transaction's expiration block number is less than or equal to the batch's
121    ///   reference block.
122    fn new_batch_inner(
123        transactions: Vec<Arc<ProvenTransaction>>,
124        reference_block_header: BlockHeader,
125        partial_blockchain: PartialBlockchain,
126        unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
127    ) -> Result<Self, ProposedBatchError> {
128        // Check for empty or duplicate transactions.
129        // --------------------------------------------------------------------------------------------
130
131        if transactions.is_empty() {
132            return Err(ProposedBatchError::EmptyTransactionBatch);
133        }
134
135        let mut transaction_set = BTreeSet::new();
136        for tx in transactions.iter() {
137            if !transaction_set.insert(tx.id()) {
138                return Err(ProposedBatchError::DuplicateTransaction { transaction_id: tx.id() });
139            }
140        }
141
142        // Verify block header and partial blockchain match.
143        // --------------------------------------------------------------------------------------------
144
145        if partial_blockchain.chain_length() != reference_block_header.block_num() {
146            return Err(ProposedBatchError::InconsistentChainLength {
147                expected: reference_block_header.block_num(),
148                actual: partial_blockchain.chain_length(),
149            });
150        }
151
152        let hashed_peaks = partial_blockchain.peaks().hash_peaks();
153        if hashed_peaks != reference_block_header.chain_commitment() {
154            return Err(ProposedBatchError::InconsistentChainRoot {
155                expected: reference_block_header.chain_commitment(),
156                actual: hashed_peaks,
157            });
158        }
159
160        // Verify all block references from the transactions are in the partial blockchain, except
161        // for the batch's reference block.
162        //
163        // Note that some block X is only added to the blockchain by block X + 1. This
164        // is because block X cannot compute its own block commitment and thus cannot add
165        // itself to the chain. So, more generally, a block is added to the blockchain by its child
166        // block.
167        //
168        // The reference block of a batch may be the latest block in the chain and, as mentioned,
169        // the block is not yet part of the blockchain, so its inclusion cannot be proven.
170        // Since the inclusion cannot be proven, the batch kernel instead commits to this reference
171        // block's commitment as a public input, which means the block kernel will prove
172        // this block's inclusion when including this batch and verifying its ZK proof.
173        //
174        // Finally, note that we don't verify anything cryptographically here. We have previously
175        // verified that the chain commitment of the batch's reference block matches the hashed
176        // peaks of the `PartialBlockchain`. This means the provided blockchain is consistent with
177        // the batch's reference block and that all blocks contained in the blockchain are
178        // consistent, too. So, as long as each transaction's reference block (number and
179        // commitment) is contained in the partial blockchain, we know the transaction's
180        // block header is consistent with the batch's reference block, too.
181        // --------------------------------------------------------------------------------------------
182
183        for tx in transactions.iter() {
184            // Differentiate between validation against the batch's reference block or a block from
185            // the chain (see above).
186            if reference_block_header.block_num() == tx.ref_block_num() {
187                if reference_block_header.commitment() != tx.ref_block_commitment() {
188                    return Err(ProposedBatchError::TransactionReferenceBlockCommitmentMismatch {
189                        transaction_id: tx.id(),
190                        block_num: tx.ref_block_num(),
191                        actual_block_commitment: tx.ref_block_commitment(),
192                        expected_block_commitment: reference_block_header.commitment(),
193                    });
194                }
195            } else {
196                let block_header =
197                    partial_blockchain.get_block(tx.ref_block_num()).ok_or_else(|| {
198                        ProposedBatchError::MissingTransactionReferenceBlock {
199                            transaction_id: tx.id(),
200                            block_num: tx.ref_block_num(),
201                        }
202                    })?;
203
204                if block_header.commitment() != tx.ref_block_commitment() {
205                    return Err(ProposedBatchError::TransactionReferenceBlockCommitmentMismatch {
206                        transaction_id: tx.id(),
207                        block_num: tx.ref_block_num(),
208                        actual_block_commitment: tx.ref_block_commitment(),
209                        expected_block_commitment: block_header.commitment(),
210                    });
211                }
212            }
213        }
214
215        // Aggregate individual tx-level account updates into a batch-level account update - one per
216        // account.
217        // --------------------------------------------------------------------------------------------
218
219        // Populate batch output notes and updated accounts.
220        let mut account_updates = BTreeMap::<AccountId, BatchAccountUpdate>::new();
221        for tx in transactions.iter() {
222            // Merge account updates so that state transitions A->B->C become A->C.
223            match account_updates.entry(tx.account_id()) {
224                Entry::Vacant(vacant) => {
225                    let batch_account_update = BatchAccountUpdate::from_transaction(tx);
226                    vacant.insert(batch_account_update);
227                },
228                Entry::Occupied(occupied) => {
229                    // This returns an error if the transactions are not correctly ordered, e.g. if
230                    // B comes before A.
231                    occupied.into_mut().merge_proven_tx(tx).map_err(|source| {
232                        ProposedBatchError::AccountUpdateError {
233                            account_id: tx.account_id(),
234                            source,
235                        }
236                    })?;
237                },
238            };
239        }
240
241        if account_updates.len() > MAX_ACCOUNTS_PER_BATCH {
242            return Err(ProposedBatchError::TooManyAccountUpdates(account_updates.len()));
243        }
244
245        // Check that all transaction's expiration block numbers are greater than the reference
246        // block.
247        // --------------------------------------------------------------------------------------------
248
249        let mut batch_expiration_block_num = BlockNumber::from(u32::MAX);
250        for tx in transactions.iter() {
251            if tx.expiration_block_num() <= reference_block_header.block_num() {
252                return Err(ProposedBatchError::ExpiredTransaction {
253                    transaction_id: tx.id(),
254                    transaction_expiration_num: tx.expiration_block_num(),
255                    reference_block_num: reference_block_header.block_num(),
256                });
257            }
258
259            // The expiration block of the batch is the minimum of all transaction's expiration
260            // block.
261            batch_expiration_block_num = batch_expiration_block_num.min(tx.expiration_block_num());
262        }
263
264        // Check for duplicates in input notes.
265        // --------------------------------------------------------------------------------------------
266
267        // Check for duplicate input notes both within a transaction and across transactions.
268        // This also includes authenticated notes, as the transaction kernel doesn't check for
269        // duplicates.
270        let mut input_note_map = BTreeMap::new();
271
272        for tx in transactions.iter() {
273            for note in tx.input_notes() {
274                let nullifier = note.nullifier();
275                if let Some(first_transaction_id) = input_note_map.insert(nullifier, tx.id()) {
276                    return Err(ProposedBatchError::DuplicateInputNote {
277                        note_nullifier: nullifier,
278                        first_transaction_id,
279                        second_transaction_id: tx.id(),
280                    });
281                }
282            }
283        }
284
285        // Create input and output note set of the batch.
286        // --------------------------------------------------------------------------------------------
287
288        // Check for duplicate output notes and remove all output notes from the batch output note
289        // set that are consumed by transactions.
290        let mut tracker = NoteTracker::new(
291            &partial_blockchain,
292            &reference_block_header,
293            &unauthenticated_note_proofs,
294        );
295        for tx in transactions.iter() {
296            tracker.push(tx.as_ref()).map_err(ProposedBatchError::from)?;
297        }
298        let TrackerOutput { input_notes, output_notes, .. } =
299            tracker.finalize().map_err(ProposedBatchError::from)?;
300
301        // Collect the remaining (non-erased) output notes into the final set of output notes.
302        let output_notes: Vec<OutputNote> =
303            output_notes.into_values().map(|(_, output_note)| output_note).collect();
304
305        if input_notes.len() > MAX_INPUT_NOTES_PER_BATCH {
306            return Err(ProposedBatchError::TooManyInputNotes(input_notes.len()));
307        }
308        // SAFETY: This is safe as we have checked for duplicates and the max number of input notes
309        // in a batch.
310        let input_notes = InputNotes::new_unchecked(input_notes);
311
312        if output_notes.len() > MAX_OUTPUT_NOTES_PER_BATCH {
313            return Err(ProposedBatchError::TooManyOutputNotes(output_notes.len()));
314        }
315
316        // Compute batch ID.
317        // --------------------------------------------------------------------------------------------
318
319        let id = BatchId::from_transactions(transactions.iter().map(AsRef::as_ref));
320
321        Ok(Self {
322            id,
323            transactions,
324            reference_block_header,
325            partial_blockchain,
326            unauthenticated_note_proofs,
327            account_updates,
328            batch_expiration_block_num,
329            input_notes,
330            output_notes,
331        })
332    }
333
334    /// Creates a new [`ProposedBatch`] from the provided parts, verifying every transaction's
335    /// execution proof against the transaction kernel.
336    ///
337    /// Transactions whose precompile claims are still outstanding are accepted: verification checks
338    /// that their deferred witness matches their VM proof, and the batch prover settles the claims
339    /// of all transactions in the batch with a single precompile proof.
340    ///
341    /// # Errors
342    ///
343    /// Returns an error for any of the batch-validation conditions documented on `new_batch_inner`,
344    /// or if a transaction's proof fails to verify or does not meet `proof_security_level`.
345    pub fn new(
346        transactions: Vec<Arc<ProvenTransaction>>,
347        reference_block_header: BlockHeader,
348        partial_blockchain: PartialBlockchain,
349        unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
350        proof_security_level: u32,
351    ) -> Result<Self, ProposedBatchError> {
352        let batch = Self::new_batch_inner(
353            transactions,
354            reference_block_header,
355            partial_blockchain,
356            unauthenticated_note_proofs,
357        )?;
358
359        let verifier = TransactionVerifier::new(proof_security_level);
360        for tx in batch.transactions() {
361            // The outcome may carry an outstanding precompile obligation, which the batch prover
362            // settles for all transactions at once.
363            let _verification_outcome = verifier.verify(tx).map_err(|source| {
364                ProposedBatchError::TransactionVerificationFailed {
365                    transaction_id: tx.id(),
366                    source,
367                }
368            })?;
369        }
370
371        Ok(batch)
372    }
373
374    /// Creates a new [`ProposedBatch`] **without verifying the transactions' execution proofs**.
375    ///
376    /// Runs the same batch validation as [`Self::new`] but skips proof verification. Exposed for
377    /// tests that build batches from mock transactions carrying dummy proofs.
378    #[cfg(any(test, feature = "testing"))]
379    pub fn new_unverified(
380        transactions: Vec<Arc<ProvenTransaction>>,
381        reference_block_header: BlockHeader,
382        partial_blockchain: PartialBlockchain,
383        unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
384    ) -> Result<Self, ProposedBatchError> {
385        Self::new_batch_inner(
386            transactions,
387            reference_block_header,
388            partial_blockchain,
389            unauthenticated_note_proofs,
390        )
391    }
392
393    // PUBLIC ACCESSORS
394    // --------------------------------------------------------------------------------------------
395
396    /// Returns a slice of the [`ProvenTransaction`]s in the batch.
397    pub fn transactions(&self) -> &[Arc<ProvenTransaction>] {
398        &self.transactions
399    }
400
401    /// Returns the ordered set of transactions in the batch.
402    pub fn transaction_headers(&self) -> OrderedTransactionHeaders {
403        // SAFETY: This constructs an ordered set in the order of the transactions in the batch.
404        OrderedTransactionHeaders::new_unchecked(
405            self.transactions
406                .iter()
407                .map(AsRef::as_ref)
408                .map(TransactionHeader::from)
409                .collect(),
410        )
411    }
412
413    /// Returns the map of account IDs mapped to their [`BatchAccountUpdate`]s.
414    ///
415    /// If an account was updated by multiple transactions, the [`BatchAccountUpdate`] is the result
416    /// of merging the individual updates.
417    ///
418    /// For example, suppose an account's state before this batch is `A` and the batch contains two
419    /// transactions that updated it. Applying the first transaction results in intermediate state
420    /// `B`, and applying the second one results in state `C`. Then the returned update represents
421    /// the state transition from `A` to `C`.
422    pub fn account_updates(&self) -> &BTreeMap<AccountId, BatchAccountUpdate> {
423        &self.account_updates
424    }
425
426    /// The ID of this batch. See [`BatchId`] for details on how it is computed.
427    pub fn id(&self) -> BatchId {
428        self.id
429    }
430
431    /// Returns the header of the reference block this batch is proposed for.
432    pub fn reference_block_header(&self) -> &BlockHeader {
433        &self.reference_block_header
434    }
435
436    /// Returns the block number at which the batch will expire.
437    pub fn batch_expiration_block_num(&self) -> BlockNumber {
438        self.batch_expiration_block_num
439    }
440
441    /// Returns the [`InputNotes`] of this batch.
442    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
443        &self.input_notes
444    }
445
446    /// Returns the output notes of the batch.
447    ///
448    /// This is the aggregation of all output notes by the transactions in the batch, except the
449    /// ones that were consumed within the batch itself.
450    pub fn output_notes(&self) -> &[OutputNote] {
451        &self.output_notes
452    }
453
454    /// Consumes the proposed batch and returns its underlying parts.
455    #[allow(clippy::type_complexity)]
456    pub fn into_parts(
457        self,
458    ) -> (
459        Vec<Arc<ProvenTransaction>>,
460        BlockHeader,
461        PartialBlockchain,
462        BTreeMap<NoteId, NoteInclusionProof>,
463        BatchId,
464        BTreeMap<AccountId, BatchAccountUpdate>,
465        InputNotes<InputNoteCommitment>,
466        Vec<OutputNote>,
467        BlockNumber,
468    ) {
469        (
470            self.transactions,
471            self.reference_block_header,
472            self.partial_blockchain,
473            self.unauthenticated_note_proofs,
474            self.id,
475            self.account_updates,
476            self.input_notes,
477            self.output_notes,
478            self.batch_expiration_block_num,
479        )
480    }
481}