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