miden_node_store/state/view/batch_inputs.rs
1//! Batch input query for the block producer.
2//!
3//! Combines in-memory snapshot data (partial MMR) with database lookups, scoping the latter by
4//! the view's tip so both sources describe the same block height.
5
6use std::collections::BTreeSet;
7
8use miden_node_proto::domain::batch::BatchInputs;
9use miden_protocol::Word;
10use miden_protocol::block::BlockNumber;
11use miden_protocol::transaction::PartialBlockchain;
12
13use super::StateView;
14use crate::errors::GetBatchInputsError;
15
16impl StateView {
17 /// Fetches the inputs for a transaction batch from the database.
18 ///
19 /// ## Inputs
20 ///
21 /// The function takes as input:
22 /// - The tx reference blocks are the set of blocks referenced by transactions in the batch.
23 /// - The unauthenticated note commitments are the set of commitments of unauthenticated notes
24 /// consumed by all transactions in the batch. For these notes, we attempt to find inclusion
25 /// proofs. Not all notes will exist in the DB necessarily, as some notes can be created and
26 /// consumed within the same batch.
27 ///
28 /// ## Outputs
29 ///
30 /// The function will return:
31 /// - A block inclusion proof for all tx reference blocks and for all blocks which are
32 /// referenced by a note inclusion proof.
33 /// - Note inclusion proofs for all notes that were found in the DB.
34 /// - The block header that the batch should reference, i.e. the latest known block.
35 pub async fn get_batch_inputs(
36 &self,
37 tx_reference_blocks: BTreeSet<BlockNumber>,
38 unauthenticated_note_commitments: BTreeSet<Word>,
39 ) -> Result<BatchInputs, GetBatchInputsError> {
40 if tx_reference_blocks.is_empty() {
41 return Err(GetBatchInputsError::TransactionBlockReferencesEmpty);
42 }
43
44 let latest_block_num = self.tip();
45
46 // First we grab note inclusion proofs for the known notes. These proofs only prove that the
47 // note was included in a given block. We then also need to prove that each of those blocks
48 // is included in the chain. The proofs are scoped by the view's tip, so the database cannot
49 // report a note from a block the pinned snapshot cannot prove yet.
50 let note_proofs = self
51 .db
52 .select_note_inclusion_proofs(unauthenticated_note_commitments, latest_block_num)
53 .await
54 .map_err(GetBatchInputsError::SelectNoteInclusionProofError)?;
55
56 // The set of blocks that the notes are included in.
57 let note_blocks = note_proofs.values().map(|proof| proof.location().block_num());
58
59 // Collect all blocks we need to query without duplicates, which is:
60 // - all blocks for which we need to prove note inclusion.
61 // - all blocks referenced by transactions in the batch.
62 let mut blocks: BTreeSet<BlockNumber> = tx_reference_blocks;
63 blocks.extend(note_blocks);
64
65 // Remove the latest block from the to-be-tracked blocks as it will be the reference block
66 // for the batch itself and thus added to the MMR within the batch kernel, so there is no
67 // need to prove its inclusion.
68 blocks.remove(&*latest_block_num);
69
70 // Scoping the blocks doubles as the validation that none lies beyond the view's tip. Scoped
71 // in descending order, so the first failure carries the highest block number.
72 let scoped_blocks = blocks
73 .iter()
74 .rev()
75 .map(|&block| {
76 self.scope_block(block).ok_or(
77 GetBatchInputsError::UnknownTransactionBlockReference {
78 highest_block_num: block,
79 latest_block_num: *latest_block_num,
80 },
81 )
82 })
83 .collect::<Result<Vec<_>, _>>()?;
84
85 // SAFETY:
86 // - The latest block num was retrieved from the view's blockchain from which we will
87 // also retrieve the proofs, so it is guaranteed to exist in that chain.
88 // - Scoping above proved that no block in the set is greater than the latest block number
89 // *and* the latest block num was removed from the set. Therefore only block numbers
90 // smaller than latest block num remain in the set. Therefore all the block numbers are
91 // guaranteed to exist in the chain state at latest block num.
92 let partial_mmr =
93 self.blockchain().partial_mmr_from_blocks(&blocks, *latest_block_num).expect(
94 "latest block num should exist and all blocks in set should be < than latest block",
95 );
96
97 // Fetch the reference block of the batch as part of this query, so we can avoid looking it
98 // up in a separate DB access.
99 let mut headers = self
100 .db
101 .select_block_headers(
102 scoped_blocks.into_iter().chain(std::iter::once(latest_block_num)),
103 )
104 .await
105 .map_err(GetBatchInputsError::SelectBlockHeaderError)?;
106
107 // Find and remove the batch reference block as we don't want to add it to the chain MMR.
108 let header_index = headers
109 .iter()
110 .enumerate()
111 .find_map(|(index, header)| (header.block_num() == *latest_block_num).then_some(index))
112 .expect("DB should have returned the header of the batch reference block");
113
114 // The order doesn't matter for PartialBlockchain::new, so swap remove is fine.
115 let batch_reference_block_header = headers.swap_remove(header_index);
116
117 // SAFETY: This should not error because:
118 // - we're passing exactly the block headers that we've added to the partial MMR,
119 // - so none of the block headers block numbers should exceed the chain length of the
120 // partial MMR,
121 // - and we've added blocks to a BTreeSet, so there can be no duplicates.
122 //
123 // We construct headers and partial MMR in concert, so they are consistent. This is why we
124 // can call the unchecked constructor.
125 let partial_block_chain = PartialBlockchain::new_unchecked(partial_mmr, headers)
126 .expect("partial mmr and block headers should be consistent");
127
128 Ok(BatchInputs {
129 batch_reference_block_header,
130 note_proofs,
131 partial_block_chain,
132 })
133 }
134}