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}