Skip to main content

miden_protocol/block/
proposed_block.rs

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