miden_protocol/block/proposed_block.rs
1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::vec::Vec;
4
5use crate::account::{AccountId, AccountUpdateDetails};
6use crate::batch::note_tracker::{NoteTracker, TrackerOutput};
7use crate::batch::{BatchAccountUpdate, BatchId, OrderedBatches, ProvenBatch};
8use crate::block::account_tree::{AccountWitness, PartialAccountTree};
9use crate::block::block_inputs::BlockInputs;
10use crate::block::nullifier_tree::{NullifierWitness, PartialNullifierTree};
11use crate::block::{
12 AccountUpdateWitness,
13 BlockBody,
14 BlockHeader,
15 BlockNoteIndex,
16 BlockNoteTree,
17 BlockNumber,
18 OutputNoteBatch,
19 ValidatorKeys,
20};
21use crate::errors::ProposedBlockError;
22use crate::note::{NoteId, Nullifier};
23use crate::transaction::{
24 InputNoteCommitment,
25 OutputNote,
26 PartialBlockchain,
27 TransactionHeader,
28 TransactionKernel,
29};
30use crate::utils::serde::{
31 ByteReader,
32 ByteWriter,
33 Deserializable,
34 DeserializationError,
35 Serializable,
36};
37use crate::{EMPTY_WORD, MAX_BATCHES_PER_BLOCK, Word};
38
39// PROPOSED BLOCK
40// =================================================================================================
41
42/// A proposed block with many, but not all constraints of a
43/// [`ProvenBlock`](crate::block::ProvenBlock) enforced.
44///
45/// See [`ProposedBlock::new_at`] for details on the checks.
46#[derive(Debug, Clone)]
47pub struct ProposedBlock {
48 /// The transaction batches in this block.
49 batches: OrderedBatches,
50 /// The unix timestamp of the block in seconds.
51 timestamp: u32,
52 /// All account's [`AccountUpdateWitness`] that were updated in this block. See its docs for
53 /// details.
54 account_updated_witnesses: Vec<(AccountId, AccountUpdateWitness)>,
55 /// Note batches created by the transactions in this block.
56 ///
57 /// These are the output notes after note erasure has been done, so they represent the actual
58 /// output notes of the block.
59 ///
60 /// The length of this vector is guaranteed to be equal to the length of `batches` and the
61 /// inner batch of output notes may be empty if a batch did not create any notes.
62 output_note_batches: Vec<OutputNoteBatch>,
63 /// The nullifiers created by this block.
64 ///
65 /// These are the nullifiers of all input notes after note erasure has been done, so these are
66 /// the nullifiers of all _authenticated_ notes consumed in the block.
67 created_nullifiers: BTreeMap<Nullifier, NullifierWitness>,
68 /// The [`PartialBlockchain`] at the state of the previous block header. It is used to:
69 /// - authenticate unauthenticated notes whose note inclusion proof references a block.
70 /// - authenticate all reference blocks of the batches in this block.
71 partial_blockchain: PartialBlockchain,
72 /// The previous block's header which this block builds on top of.
73 ///
74 /// As part of proving the block, this header will be added to the next partial blockchain.
75 prev_block_header: BlockHeader,
76 /// The validator public key set authorized to sign the *next* block, which is committed to in
77 /// this block's header.
78 ///
79 /// Defaults to the previous block's `validator_keys` (i.e. no rotation). Set a different set
80 /// via [`ProposedBlock::with_next_validator_keys`] to rotate the validator keys.
81 next_validator_keys: ValidatorKeys,
82}
83
84impl ProposedBlock {
85 // CONSTRUCTORS
86 // --------------------------------------------------------------------------------------------
87
88 /// Creates a new proposed block from the provided [`BlockInputs`], transaction batches and
89 /// timestamp.
90 ///
91 /// This checks most of the constraints of a block and computes most of the data structure
92 /// updates except for the more expensive tree updates (nullifier, account and chain
93 /// commitment).
94 ///
95 /// # Errors
96 ///
97 /// Returns an error if any of the following conditions are met.
98 ///
99 /// ## Batches
100 ///
101 /// - The number of batches exceeds [`MAX_BATCHES_PER_BLOCK`].
102 /// - There are duplicate batches, i.e. they have the same [`BatchId`].
103 /// - The expiration block number of any batch is less than the block number of the currently
104 /// proposed block.
105 ///
106 /// ## Chain
107 ///
108 /// - The length of the [`PartialBlockchain`] in the block inputs is not equal to the previous
109 /// block header in the block inputs.
110 /// - The [`PartialBlockchain`]'s chain commitment is not equal to the
111 /// [`BlockHeader::chain_commitment`] of the previous block header.
112 ///
113 /// ## Notes
114 ///
115 /// Note that, in the following, the set of authenticated notes includes unauthenticated notes
116 /// that have been authenticated.
117 ///
118 /// - The union of all input notes across all batches contain duplicates.
119 /// - The union of all output notes across all batches contain duplicates.
120 /// - An unauthenticated note is consumed before it is created (as determined by the order in
121 /// which batches are given).
122 /// - There is a note inclusion proof for an unauthenticated note whose referenced block is not
123 /// in the [`PartialBlockchain`].
124 /// - The note inclusion proof for an unauthenticated is invalid.
125 /// - There are any unauthenticated notes for which no note inclusion proof is provided.
126 /// - A [`NullifierWitness`] is missing for an authenticated note.
127 /// - If the [`NullifierWitness`] for an authenticated note proves that the note was already
128 /// consumed.
129 ///
130 /// ## Accounts
131 ///
132 /// - An [`AccountWitness`] is missing for an account updated by a batch.
133 /// - Any two batches update the same account from the same state. For example, if batch 1
134 /// updates some account from state A to B and batch 2 updates it from A to F, then those
135 /// batches conflict as they both start from the same initial state but produce a fork in the
136 /// account's state.
137 /// - Account updates from different batches cannot be brought in a contiguous order. For
138 /// example, if a batch 1 updates an account from state A to C, and a batch 2 updates it from
139 /// D to F, then the state transition from C to D is missing. Note that this does not mean,
140 /// that batches must be provided in an order where account updates chain together in the
141 /// order of the batches, which would generally be an impossible requirement to fulfill.
142 /// - Account updates cannot be merged, i.e. if [`AccountUpdateDetails::merge`] fails on the
143 /// updates from two batches.
144 ///
145 /// ## Time
146 ///
147 /// - The given `timestamp` does not increase monotonically compared to the previous block
148 /// header' timestamp.
149 pub fn new_at(
150 block_inputs: BlockInputs,
151 batches: Vec<ProvenBatch>,
152 timestamp: u32,
153 ) -> Result<Self, ProposedBlockError> {
154 // Check for duplicate and max number of batches.
155 // --------------------------------------------------------------------------------------------
156
157 if batches.len() > MAX_BATCHES_PER_BLOCK {
158 return Err(ProposedBlockError::TooManyBatches);
159 }
160
161 check_duplicate_batches(&batches)?;
162
163 // Check timestamp increases monotonically.
164 // --------------------------------------------------------------------------------------------
165
166 check_timestamp_increases_monotonically(timestamp, block_inputs.prev_block_header())?;
167
168 // Check for batch expiration.
169 // --------------------------------------------------------------------------------------------
170
171 check_batch_expiration(&batches, block_inputs.prev_block_header())?;
172
173 // Check for consistency between the partial blockchain and the referenced previous block.
174 // --------------------------------------------------------------------------------------------
175
176 check_reference_block_partial_blockchain_consistency(
177 block_inputs.partial_blockchain(),
178 block_inputs.prev_block_header(),
179 )?;
180
181 // Check every block referenced by a batch is in the partial blockchain.
182 // --------------------------------------------------------------------------------------------
183
184 check_batch_reference_blocks(
185 block_inputs.partial_blockchain(),
186 block_inputs.prev_block_header(),
187 &batches,
188 )?;
189
190 // Check for duplicates in the input and output notes and compute the input and output notes
191 // of the block by erasing notes that are created and consumed within this block as well as
192 // authenticating unauthenticated notes.
193 // --------------------------------------------------------------------------------------------
194
195 let mut tracker = NoteTracker::new(
196 block_inputs.partial_blockchain(),
197 block_inputs.prev_block_header(),
198 block_inputs.unauthenticated_note_proofs(),
199 );
200 for batch in batches.iter() {
201 tracker.push(batch)?;
202 }
203 let TrackerOutput {
204 input_notes: block_input_notes,
205 erased_notes: block_erased_notes,
206 output_notes: block_output_notes,
207 } = tracker.finalize()?;
208
209 // All unauthenticated notes must be erased or authenticated by now.
210 if let Some(nullifier) = block_input_notes
211 .iter()
212 .find_map(|note| (!note.is_authenticated()).then_some(note.nullifier()))
213 {
214 return Err(ProposedBlockError::UnauthenticatedNoteConsumed { nullifier });
215 }
216
217 // Check for nullifiers proofs and unspent nullifiers.
218 // --------------------------------------------------------------------------------------------
219
220 let (prev_block_header, partial_blockchain, account_witnesses, mut nullifier_witnesses, _) =
221 block_inputs.into_parts();
222
223 // Remove nullifiers of erased notes, so we only add the nullifiers of actual input notes to
224 // the proposed block.
225 remove_erased_nullifiers(&mut nullifier_witnesses, block_erased_notes.into_iter());
226
227 // Check against computed block_input_notes which also contain unauthenticated notes that
228 // have been authenticated.
229 check_nullifiers(
230 &nullifier_witnesses,
231 block_input_notes.iter().map(InputNoteCommitment::nullifier),
232 )?;
233
234 // Aggregate account updates across batches.
235 // --------------------------------------------------------------------------------------------
236
237 let aggregator = AccountUpdateAggregator::from_batches(&batches)?;
238 let account_updated_witnesses = aggregator.into_update_witnesses(account_witnesses)?;
239
240 // Compute the block's output note batches from the individual batch output notes.
241 // --------------------------------------------------------------------------------------------
242
243 let output_note_batches = compute_block_output_notes(&batches, block_output_notes);
244
245 // Build proposed blocks from parts.
246 // --------------------------------------------------------------------------------------------
247
248 let next_validator_keys = prev_block_header.validator_keys().clone();
249
250 Ok(Self {
251 batches: OrderedBatches::new(batches),
252 timestamp,
253 account_updated_witnesses,
254 output_note_batches,
255 created_nullifiers: nullifier_witnesses,
256 partial_blockchain,
257 prev_block_header,
258 next_validator_keys,
259 })
260 }
261
262 /// Creates a new proposed block from the provided [`BlockInputs`] and transaction batches.
263 ///
264 /// Equivalent to [`ProposedBlock::new_at`] except that the timestamp of the proposed block is
265 /// set to the current system time or the previous block header's timestamp + 1, whichever
266 /// is greater. This guarantees that the timestamp increases monotonically.
267 ///
268 /// See the [`ProposedBlock::new_at`] for details on errors and other constraints.
269 #[cfg(feature = "std")]
270 pub fn new(
271 block_inputs: BlockInputs,
272 batches: Vec<ProvenBatch>,
273 ) -> Result<Self, ProposedBlockError> {
274 let timestamp_now: u32 = std::time::SystemTime::now()
275 .duration_since(std::time::UNIX_EPOCH)
276 .expect("now should be after 1970")
277 .as_secs()
278 .try_into()
279 .expect("timestamp should fit in a u32 before the year 2106");
280
281 let timestamp = timestamp_now.max(block_inputs.prev_block_header().timestamp() + 1);
282
283 Self::new_at(block_inputs, batches, timestamp)
284 }
285
286 // BUILDERS
287 // --------------------------------------------------------------------------------------------
288
289 /// Sets the validator key set that this block commits to as the signer of the *next* block,
290 /// rotating away from the previous block's validator keys.
291 ///
292 /// The block this proposed block produces is still signed by the current validators (the keys
293 /// committed to by the previous block); the provided set only takes effect for the following
294 /// block.
295 #[must_use]
296 pub fn with_next_validator_keys(mut self, next_validator_keys: ValidatorKeys) -> Self {
297 self.next_validator_keys = next_validator_keys;
298 self
299 }
300
301 // ACCESSORS
302 // --------------------------------------------------------------------------------------------
303
304 /// Returns the block number of this proposed block.
305 pub fn block_num(&self) -> BlockNumber {
306 // The chain length is the length at the state of the previous block header, so we have to
307 // add one.
308 self.partial_blockchain().chain_length() + 1
309 }
310
311 /// Returns a reference to the previous block header that this block builds on top of.
312 pub fn prev_block_header(&self) -> &BlockHeader {
313 &self.prev_block_header
314 }
315
316 /// Returns the [`PartialBlockchain`] that this block contains.
317 pub fn partial_blockchain(&self) -> &PartialBlockchain {
318 &self.partial_blockchain
319 }
320
321 /// Returns a reference to the slice of transaction batches in this block.
322 pub fn batches(&self) -> &OrderedBatches {
323 &self.batches
324 }
325
326 /// Returns an iterator over all transactions in the block.
327 pub fn transactions(&self) -> impl Iterator<Item = &TransactionHeader> {
328 self.batches
329 .as_slice()
330 .iter()
331 .flat_map(|batch| batch.transactions().as_slice().iter())
332 }
333
334 /// Returns the map of nullifiers to their proofs from the proposed block.
335 pub fn created_nullifiers(&self) -> &BTreeMap<Nullifier, NullifierWitness> {
336 &self.created_nullifiers
337 }
338
339 /// Returns a reference to the slice of accounts updated in this block.
340 pub fn updated_accounts(&self) -> &[(AccountId, AccountUpdateWitness)] {
341 &self.account_updated_witnesses
342 }
343
344 /// Returns a slice of the [`OutputNoteBatch`] of each batch in this block.
345 pub fn output_note_batches(&self) -> &[OutputNoteBatch] {
346 &self.output_note_batches
347 }
348
349 /// Returns the timestamp of this block.
350 pub fn timestamp(&self) -> u32 {
351 self.timestamp
352 }
353
354 /// Returns the validator key set committed to by this block as the signer of the next block.
355 pub fn next_validator_keys(&self) -> &ValidatorKeys {
356 &self.next_validator_keys
357 }
358
359 // COMMITMENT COMPUTATIONS
360 // --------------------------------------------------------------------------------------------
361
362 /// Computes the new account tree root after the given updates.
363 pub fn compute_account_root(&self) -> Result<Word, ProposedBlockError> {
364 // If no accounts were updated, the account tree root is unchanged.
365 if self.account_updated_witnesses.is_empty() {
366 return Ok(self.prev_block_header.account_root());
367 }
368
369 // First reconstruct the current account tree from the provided merkle paths.
370 // If a witness points to a leaf where multiple account IDs share the same prefix, this will
371 // return an error.
372 let mut partial_account_tree = PartialAccountTree::with_witnesses(
373 self.account_updated_witnesses
374 .iter()
375 .map(|(_, update_witness)| update_witness.to_witness()),
376 )
377 .map_err(|source| ProposedBlockError::AccountWitnessTracking { source })?;
378
379 // Check the account tree root in the previous block header matches the reconstructed tree's
380 // root.
381 if self.prev_block_header.account_root() != partial_account_tree.root() {
382 return Err(ProposedBlockError::StaleAccountTreeRoot {
383 prev_block_account_root: self.prev_block_header.account_root(),
384 stale_account_root: partial_account_tree.root(),
385 });
386 }
387
388 // Second, update the account tree by inserting the new final account state commitments to
389 // compute the new root of the account tree.
390 // If an account ID's prefix already exists in the tree, this will return an error.
391 // Note that we have inserted all witnesses that we want to update into the partial account
392 // tree, so we should not run into the untracked key error.
393 partial_account_tree
394 .upsert_state_commitments(self.account_updated_witnesses.iter().map(
395 |(account_id, update_witness)| {
396 (*account_id, update_witness.final_state_commitment())
397 },
398 ))
399 .map_err(|source| ProposedBlockError::AccountIdPrefixDuplicate { source })?;
400
401 Ok(partial_account_tree.root())
402 }
403
404 /// Computes the new nullifier root by inserting the nullifier witnesses into a partial
405 /// nullifier tree and marking each nullifier as spent in the given block number.
406 pub fn compute_nullifier_root(&self) -> Result<Word, ProposedBlockError> {
407 // If no nullifiers were created, the nullifier tree root is unchanged.
408 if self.created_nullifiers.is_empty() {
409 return Ok(self.prev_block_header.nullifier_root());
410 }
411
412 // First, reconstruct the current nullifier tree with the merkle paths of the nullifiers we
413 // want to update.
414 // Due to the guarantees of ProposedBlock we can safely assume that each nullifier is mapped
415 // to its corresponding nullifier witness, so we don't have to check again whether
416 // they match.
417 let mut partial_nullifier_tree =
418 PartialNullifierTree::with_witnesses(self.created_nullifiers().values().cloned())
419 .map_err(ProposedBlockError::NullifierWitnessRootMismatch)?;
420
421 // Check the nullifier tree root in the previous block header matches the reconstructed
422 // tree's root.
423 if self.prev_block_header.nullifier_root() != partial_nullifier_tree.root() {
424 return Err(ProposedBlockError::StaleNullifierTreeRoot {
425 prev_block_nullifier_root: self.prev_block_header.nullifier_root(),
426 stale_nullifier_root: partial_nullifier_tree.root(),
427 });
428 }
429
430 // Second, mark each nullifier as spent in the tree. Note that checking whether each
431 // nullifier is unspent is checked as part of constructing the proposed block.
432
433 // SAFETY: As mentioned above, we can safely assume that each nullifier's witness was
434 // added and every nullifier should be tracked by the partial tree and
435 // therefore updatable.
436 partial_nullifier_tree
437 .mark_spent_all(self.created_nullifiers.keys().copied(), self.block_num())
438 .expect("nullifiers' merkle path should have been added to the partial tree and the nullifiers should be unspent");
439
440 Ok(partial_nullifier_tree.root())
441 }
442
443 /// Compute the block note tree from the output note batches.
444 pub fn compute_block_note_tree(&self) -> BlockNoteTree {
445 let output_notes_iter =
446 self.output_note_batches.iter().enumerate().flat_map(|(batch_idx, notes)| {
447 notes.iter().map(move |(note_idx_in_batch, note)| {
448 (
449 // SAFETY: The proposed block contains at most the max allowed number of
450 // batches and each batch is guaranteed to contain at most
451 // the max allowed number of output notes.
452 BlockNoteIndex::new(batch_idx, *note_idx_in_batch).expect(
453 "max batches in block and max notes in batches should be enforced",
454 ),
455 note.into(),
456 )
457 })
458 });
459
460 // SAFETY: We only construct proposed blocks that:
461 // - do not contain duplicates
462 // - contain at most the max allowed number of batches and each batch is guaranteed to
463 // contain at most the max allowed number of output notes.
464 BlockNoteTree::with_entries(output_notes_iter)
465 .expect("the output notes of the block should not contain duplicates and contain at most the allowed maximum")
466 }
467
468 /// Adds the commitment of the previous block header to the partial blockchain to compute the
469 /// new chain commitment.
470 pub fn compute_chain_commitment(&self) -> Word {
471 let mut partial_blockchain = self.partial_blockchain.clone();
472 // SAFETY: This does not panic as long as the block header we're adding is the next one in
473 // the chain which is validated as part of constructing a `ProposedBlock`.
474 partial_blockchain.add_block(&self.prev_block_header, true);
475 partial_blockchain.peaks().hash_peaks()
476 }
477
478 // STATE MUTATORS
479 // --------------------------------------------------------------------------------------------
480
481 /// Builds a [`BlockHeader`] and [`BlockBody`] by computing the following from the state
482 /// updates encapsulated by the provided [`ProposedBlock`]:
483 /// - the account root;
484 /// - the nullifier root;
485 /// - the note root;
486 /// - the transaction commitment; and
487 /// - the chain commitment.
488 ///
489 /// # Errors
490 ///
491 /// Returns an error if any of the following conditions are met.
492 ///
493 /// ## Account Tree
494 ///
495 /// - An account witness cannot be used to reconstruct the partial account tree (e.g. it points
496 /// to a leaf where multiple account IDs share the same prefix).
497 /// - The account root in the previous block header does not match the root of the reconstructed
498 /// partial account tree (stale account tree root).
499 /// - An account ID's prefix already exists in the tree when inserting the new state commitments
500 /// (duplicate account ID prefix).
501 ///
502 /// ## Nullifier Tree
503 ///
504 /// - The nullifier witnesses cannot be used to reconstruct the partial nullifier tree (root
505 /// mismatch between witnesses).
506 /// - The nullifier root in the previous block header does not match the root of the
507 /// reconstructed partial nullifier tree (stale nullifier tree root).
508 pub fn into_header_and_body(self) -> Result<(BlockHeader, BlockBody), ProposedBlockError> {
509 // Get fields from the proposed block before it is consumed.
510 let block_num = self.block_num();
511 let timestamp = self.timestamp();
512 let prev_block_header = self.prev_block_header().clone();
513 let next_validator_keys = self.next_validator_keys.clone();
514
515 // Insert the state commitments of updated accounts into the account tree to compute its new
516 // root.
517 let new_account_root = self.compute_account_root()?;
518
519 // Insert the created nullifiers into the nullifier tree to compute its new root.
520 let new_nullifier_root = self.compute_nullifier_root()?;
521
522 // Compute the root of the block note tree.
523 let note_tree = self.compute_block_note_tree();
524 let note_root = note_tree.root();
525
526 // Insert the previous block header into the block partial blockchain to get the new chain
527 // commitment.
528 // TODO: Consider avoiding the partial blockchain clone by constructing `BlockBody` from its
529 // raw parts, which does not require the partial blockchain.
530 let new_chain_commitment = self.compute_chain_commitment();
531
532 // Construct the block body from the proposed block.
533 let body = BlockBody::from(self);
534
535 // Construct the header.
536 let tx_commitment = body.transaction_commitment();
537 let prev_block_commitment = prev_block_header.commitment();
538
539 // For now we copy the parameters of the previous header, which means the parameters set on
540 // the genesis block will be passed through. Eventually, the contained base fees will be
541 // updated based on the demand in the currently proposed block.
542 let fee_parameters = prev_block_header.fee_parameters().clone();
543
544 // Currently undefined and reserved for future use.
545 // See https://github.com/0xMiden/protocol/issues/1155.
546 let version = 0;
547 let tx_kernel_commitment = TransactionKernel.to_commitment();
548 let header = BlockHeader::new(
549 version,
550 prev_block_commitment,
551 block_num,
552 new_chain_commitment,
553 new_account_root,
554 new_nullifier_root,
555 note_root,
556 tx_commitment,
557 tx_kernel_commitment,
558 next_validator_keys,
559 fee_parameters,
560 timestamp,
561 );
562
563 Ok((header, body))
564 }
565
566 /// Consumes self and returns the non-[`Copy`] parts of the block.
567 #[allow(clippy::type_complexity)]
568 pub fn into_parts(
569 self,
570 ) -> (
571 OrderedBatches,
572 Vec<(AccountId, AccountUpdateWitness)>,
573 Vec<OutputNoteBatch>,
574 BTreeMap<Nullifier, NullifierWitness>,
575 PartialBlockchain,
576 BlockHeader,
577 ) {
578 (
579 self.batches,
580 self.account_updated_witnesses,
581 self.output_note_batches,
582 self.created_nullifiers,
583 self.partial_blockchain,
584 self.prev_block_header,
585 )
586 }
587}
588
589// SERIALIZATION
590// ================================================================================================
591
592impl Serializable for ProposedBlock {
593 fn write_into<W: ByteWriter>(&self, target: &mut W) {
594 self.batches.write_into(target);
595 self.timestamp.write_into(target);
596 self.account_updated_witnesses.write_into(target);
597 self.output_note_batches.write_into(target);
598 self.created_nullifiers.write_into(target);
599 self.partial_blockchain.write_into(target);
600 self.prev_block_header.write_into(target);
601 self.next_validator_keys.write_into(target);
602 }
603}
604
605impl Deserializable for ProposedBlock {
606 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
607 let block = Self {
608 batches: OrderedBatches::read_from(source)?,
609 timestamp: u32::read_from(source)?,
610 account_updated_witnesses: <Vec<(AccountId, AccountUpdateWitness)>>::read_from(source)?,
611 output_note_batches: <Vec<OutputNoteBatch>>::read_from(source)?,
612 created_nullifiers: <BTreeMap<Nullifier, NullifierWitness>>::read_from(source)?,
613 partial_blockchain: PartialBlockchain::read_from(source)?,
614 prev_block_header: BlockHeader::read_from(source)?,
615 next_validator_keys: ValidatorKeys::read_from(source)?,
616 };
617
618 Ok(block)
619 }
620}
621
622// HELPER FUNCTIONS
623// ================================================================================================
624
625fn check_duplicate_batches(batches: &[ProvenBatch]) -> Result<(), ProposedBlockError> {
626 let mut input_note_set = BTreeSet::new();
627
628 for batch in batches {
629 if !input_note_set.insert(batch.id()) {
630 return Err(ProposedBlockError::DuplicateBatch { batch_id: batch.id() });
631 }
632 }
633
634 Ok(())
635}
636
637fn check_timestamp_increases_monotonically(
638 provided_timestamp: u32,
639 prev_block_header: &BlockHeader,
640) -> Result<(), ProposedBlockError> {
641 if provided_timestamp <= prev_block_header.timestamp() {
642 Err(ProposedBlockError::TimestampDoesNotIncreaseMonotonically {
643 provided_timestamp,
644 previous_timestamp: prev_block_header.timestamp(),
645 })
646 } else {
647 Ok(())
648 }
649}
650
651/// Checks whether any of the batches is expired and can no longer be included in this block.
652///
653/// To illustrate, a batch which expired at block 4 cannot be included in block 5, but if it
654/// expires at block 5 then it can still be included in block 5.
655fn check_batch_expiration(
656 batches: &[ProvenBatch],
657 prev_block_header: &BlockHeader,
658) -> Result<(), ProposedBlockError> {
659 let current_block_num = prev_block_header.block_num() + 1;
660
661 for batch in batches {
662 if batch.batch_expiration_block_num() < current_block_num {
663 return Err(ProposedBlockError::ExpiredBatch {
664 batch_id: batch.id(),
665 batch_expiration_block_num: batch.batch_expiration_block_num(),
666 current_block_num,
667 });
668 }
669 }
670
671 Ok(())
672}
673
674/// Check that each nullifier in the block has a proof provided and that the nullifier is
675/// unspent. The proofs are required to update the nullifier tree.
676fn check_nullifiers(
677 nullifier_witnesses: &BTreeMap<Nullifier, NullifierWitness>,
678 block_input_notes: impl Iterator<Item = Nullifier>,
679) -> Result<(), ProposedBlockError> {
680 for block_input_note in block_input_notes {
681 match nullifier_witnesses
682 .get(&block_input_note)
683 .and_then(|x| x.proof().get(&block_input_note.as_word()))
684 {
685 Some(nullifier_value) => {
686 if nullifier_value != EMPTY_WORD {
687 return Err(ProposedBlockError::NullifierSpent(block_input_note));
688 }
689 },
690 // If the nullifier witnesses did not contain a proof for this nullifier or the provided
691 // proof was not for this nullifier, then it's an error.
692 None => return Err(ProposedBlockError::NullifierProofMissing(block_input_note)),
693 }
694 }
695
696 Ok(())
697}
698
699/// Removes the nullifiers from the nullifier witnesses that were erased (i.e. created and consumed
700/// within the block).
701fn remove_erased_nullifiers(
702 nullifier_witnesses: &mut BTreeMap<Nullifier, NullifierWitness>,
703 block_erased_notes: impl Iterator<Item = Nullifier>,
704) {
705 for erased_note in block_erased_notes {
706 // We do not check that the nullifier was actually present to allow the block inputs to
707 // not include a nullifier that is known to belong to an erased note.
708 let _ = nullifier_witnesses.remove(&erased_note);
709 }
710}
711
712/// Checks consistency between the previous block header and the provided partial blockchain.
713///
714/// This checks that:
715/// - the chain length of the partial blockchain is equal to the block number of the previous block
716/// header, i.e. the partial blockchain's latest block is the previous' blocks reference block.
717/// The previous block header will be added to the partial blockchain as part of constructing the
718/// current block.
719/// - the root of the partial blockchain is equivalent to the chain commitment of the previous block
720/// header.
721fn check_reference_block_partial_blockchain_consistency(
722 partial_blockchain: &PartialBlockchain,
723 prev_block_header: &BlockHeader,
724) -> Result<(), ProposedBlockError> {
725 // Make sure that the current partial blockchain has blocks up to prev_block_header - 1, i.e.
726 // its chain length is equal to the block number of the previous block header.
727 if partial_blockchain.chain_length() != prev_block_header.block_num() {
728 return Err(ProposedBlockError::ChainLengthNotEqualToPreviousBlockNumber {
729 chain_length: partial_blockchain.chain_length(),
730 prev_block_num: prev_block_header.block_num(),
731 });
732 }
733
734 let chain_commitment = partial_blockchain.peaks().hash_peaks();
735 if chain_commitment != prev_block_header.chain_commitment() {
736 return Err(ProposedBlockError::ChainRootNotEqualToPreviousBlockChainCommitment {
737 chain_commitment,
738 prev_block_chain_commitment: prev_block_header.chain_commitment(),
739 prev_block_num: prev_block_header.block_num(),
740 });
741 }
742
743 Ok(())
744}
745
746/// Check that each block referenced by a batch in the block has an entry in the partial blockchain,
747/// except if the referenced block is the same as the previous block, referenced by the block.
748fn check_batch_reference_blocks(
749 partial_blockchain: &PartialBlockchain,
750 prev_block_header: &BlockHeader,
751 batches: &[ProvenBatch],
752) -> Result<(), ProposedBlockError> {
753 for batch in batches {
754 let batch_reference_block_num = batch.reference_block_num();
755 if batch_reference_block_num != prev_block_header.block_num()
756 && !partial_blockchain.contains_block(batch.reference_block_num())
757 {
758 return Err(ProposedBlockError::BatchReferenceBlockMissingFromChain {
759 reference_block_num: batch.reference_block_num(),
760 batch_id: batch.id(),
761 });
762 }
763 }
764
765 Ok(())
766}
767
768/// Computes the block's output notes from the batches of notes of each batch in the block.
769///
770/// We pass in `block_output_notes` which is the full set of output notes of the block, with output
771/// notes erased that are consumed by some batch in the block.
772///
773/// The batch output notes of each proven batch however contain all the notes that it creates,
774/// including ones that were potentially erased in `block_output_notes`. This means we have to
775/// make the batch output notes consistent with `block_output_notes` by removing the erased notes.
776/// Then it accurately represents what output notes the batch actually creates as part of the block.
777///
778/// Returns the set of [`OutputNoteBatch`]es that each batch creates.
779fn compute_block_output_notes(
780 batches: &[ProvenBatch],
781 mut block_output_notes: BTreeMap<NoteId, (BatchId, OutputNote)>,
782) -> Vec<OutputNoteBatch> {
783 let mut block_output_note_batches = Vec::with_capacity(batches.len());
784
785 for batch in batches.iter() {
786 let batch_output_notes = compute_batch_output_notes(batch, &mut block_output_notes);
787 block_output_note_batches.push(batch_output_notes);
788 }
789
790 block_output_note_batches
791}
792
793/// Computes the output note of the given batch. This is essentially the batch's output notes minus
794/// all erased notes.
795///
796/// If a note in the batch's output notes is not present in the block output notes map it means it
797/// was erased and should therefore not be added to the batch's output notes. If it is present, it
798/// is added to the set of output notes of this batch.
799///
800/// The output note set is returned.
801fn compute_batch_output_notes(
802 batch: &ProvenBatch,
803 block_output_notes: &mut BTreeMap<NoteId, (BatchId, OutputNote)>,
804) -> OutputNoteBatch {
805 // The len of the batch output notes is an upper bound of how many notes the batch could've
806 // produced so we reserve that much space to avoid reallocation.
807 let mut batch_output_notes = Vec::with_capacity(batch.output_notes().len());
808
809 for (note_idx, original_output_note) in batch.output_notes().iter().enumerate() {
810 // If block_output_notes no longer contains a note it means it was erased and we do not
811 // include it in the output notes of the current batch. We include the original index of the
812 // note in the batch so we can later correctly construct the block note tree. This index is
813 // needed because we want to be able to construct the block note tree in two ways: 1) By
814 // inserting the individual batch note trees (with erased notes removed) as subtrees into an
815 // empty block note tree or 2) by iterating the set `OutputNoteBatch`es. If we did not store
816 // the index, then the second method would assume a contiguous layout of output notes and
817 // result in a different tree than the first method.
818 //
819 // Note that because we disallow duplicate output notes, if this map contains the
820 // original note id, then we can be certain it was created by this batch and should stay
821 // in the tree. In other words, there is no ambiguity where a note originated from.
822 if let Some((_batch_id, output_note)) =
823 block_output_notes.remove(&original_output_note.id())
824 {
825 debug_assert_eq!(
826 _batch_id,
827 batch.id(),
828 "batch that contained the note originally is no longer the batch that contains it according to the provided map"
829 );
830 batch_output_notes.push((note_idx, output_note));
831 }
832 }
833
834 batch_output_notes
835}
836
837// ACCOUNT UPDATE AGGREGATOR
838// ================================================================================================
839
840struct AccountUpdateAggregator {
841 /// The map from each account to the map of each of its updates, where the digest is the state
842 /// commitment from which the contained update starts.
843 /// An invariant of this field is that if the outer map has an entry for some account, the
844 /// inner update map is guaranteed to not be empty as well.
845 updates: BTreeMap<AccountId, BTreeMap<Word, (BatchAccountUpdate, BatchId)>>,
846}
847
848impl AccountUpdateAggregator {
849 fn new() -> Self {
850 Self { updates: BTreeMap::new() }
851 }
852
853 /// Aggregates all updates for the same account and stores each update indexed by its initial
854 /// state commitment so we can easily retrieve them in the next step. This lets us
855 /// chronologically order the updates per account across batches.
856 fn from_batches(batches: &[ProvenBatch]) -> Result<Self, ProposedBlockError> {
857 let mut update_aggregator = AccountUpdateAggregator::new();
858
859 for batch in batches {
860 for (account_id, update) in batch.account_updates() {
861 update_aggregator.insert_update(*account_id, batch.id(), update.clone())?;
862 }
863 }
864
865 Ok(update_aggregator)
866 }
867
868 /// Inserts the update from one batch for a specific account into the map of updates.
869 fn insert_update(
870 &mut self,
871 account_id: AccountId,
872 batch_id: BatchId,
873 update: BatchAccountUpdate,
874 ) -> Result<(), ProposedBlockError> {
875 // As a special case, a NOOP transaction (i.e. one where the initial and final state
876 // commitment is the same) can just be ignored without changing the outcome.
877 // Without this early return, such a transaction would conflict with other state-updating
878 // transactions, because there would be two transactions that update the account from
879 // the same initial state commitment.
880 if update.initial_state_commitment() == update.final_state_commitment() {
881 return Ok(());
882 };
883
884 if let Some((conflicting_update, conflicting_batch_id)) = self
885 .updates
886 .entry(account_id)
887 .or_default()
888 .insert(update.initial_state_commitment(), (update, batch_id))
889 {
890 return Err(ProposedBlockError::ConflictingBatchesUpdateSameAccount {
891 account_id,
892 initial_state_commitment: conflicting_update.initial_state_commitment(),
893 first_batch_id: conflicting_batch_id,
894 second_batch_id: batch_id,
895 });
896 }
897
898 Ok(())
899 }
900
901 /// Consumes self and aggregates the account updates from all contained accounts.
902 /// For each updated account an entry in `account_witnesses` must be present.
903 fn into_update_witnesses(
904 self,
905 mut account_witnesses: BTreeMap<AccountId, AccountWitness>,
906 ) -> Result<Vec<(AccountId, AccountUpdateWitness)>, ProposedBlockError> {
907 let mut account_update_witnesses = Vec::with_capacity(self.updates.len());
908
909 for (account_id, updates_map) in self.updates {
910 let witness = account_witnesses
911 .remove(&account_id)
912 .ok_or(ProposedBlockError::MissingAccountWitness(account_id))?;
913
914 let account_update_witness = Self::aggregate_account(account_id, witness, updates_map)?;
915
916 account_update_witnesses.push((account_id, account_update_witness));
917 }
918
919 Ok(account_update_witnesses)
920 }
921
922 /// Build the update for a single account from the provided map of updates, where each entry is
923 /// the state from which the update starts. This chains updates for this account together in a
924 /// chronological order using the state commitments to link them.
925 fn aggregate_account(
926 account_id: AccountId,
927 initial_state_proof: AccountWitness,
928 mut updates: BTreeMap<Word, (BatchAccountUpdate, BatchId)>,
929 ) -> Result<AccountUpdateWitness, ProposedBlockError> {
930 // The account witness could prove inclusion of a different ID in which case the initial
931 // state commitment of the current ID is the empty word.
932 let initial_state_commitment = if account_id == initial_state_proof.id() {
933 initial_state_proof.state_commitment()
934 } else {
935 Word::empty()
936 };
937
938 let mut details: Option<AccountUpdateDetails> = None;
939
940 let mut current_commitment = initial_state_commitment;
941 while !updates.is_empty() {
942 let (update, _) = updates.remove(¤t_commitment).ok_or_else(|| {
943 ProposedBlockError::InconsistentAccountStateTransition {
944 account_id,
945 state_commitment: current_commitment,
946 remaining_state_commitments: updates.keys().copied().collect(),
947 }
948 })?;
949
950 current_commitment = update.final_state_commitment();
951 let update_details = update.into_update();
952
953 details = Some(match details {
954 None => update_details,
955 Some(details) => details.merge(update_details).map_err(|source| {
956 ProposedBlockError::AccountUpdateError { account_id, source: Box::new(source) }
957 })?,
958 });
959 }
960
961 Ok(AccountUpdateWitness::new(
962 initial_state_commitment,
963 current_commitment,
964 initial_state_proof,
965 details.expect("details should be Some as updates is guaranteed to not be empty"),
966 ))
967 }
968}