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