miden_objects/transaction/
partial_blockchain.rs

1use alloc::{collections::BTreeMap, vec::Vec};
2
3use crate::{
4    PartialBlockchainError,
5    block::{BlockHeader, BlockNumber},
6    crypto::merkle::{InnerNodeInfo, MmrPeaks, PartialMmr},
7    utils::serde::{Deserializable, Serializable},
8};
9
10// PARTIAL BLOCKCHAIN
11// ================================================================================================
12
13/// A partial view into the full [`Blockchain`](crate::block::Blockchain)'s Merkle Mountain Range
14/// (MMR).
15///
16/// It allows for efficient authentication of input notes during transaction execution or
17/// authentication of reference blocks during batch or block execution. Authentication is achieved
18/// by providing inclusion proofs for the notes consumed in the transaction against the partial
19/// blockchain root associated with the transaction's reference block.
20///
21/// [`PartialBlockchain`] contains authentication paths for a limited set of blocks. The intent is
22/// to include only the blocks relevant for execution:
23/// - For transactions: the set of blocks in which all input notes were created.
24/// - For batches: the set of reference blocks of all transactions in the batch and the blocks to
25///   prove any unauthenticated note's inclusion.
26/// - For blocks: the set of reference blocks of all batches in the block and the blocks to prove
27///   any unauthenticated note's inclusion.
28///
29/// # Guarantees
30///
31/// The [`PartialBlockchain`] contains the full authenticated [`BlockHeader`]s of all blocks it
32/// tracks in its partial MMR and users of this type can make this assumption. This is ensured when
33/// using [`PartialBlockchain::new`]. [`PartialBlockchain::new_unchecked`] should only be used
34/// whenever this guarantee can be upheld.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct PartialBlockchain {
37    /// Partial view of the blockchain with authentication paths for the blocks listed below.
38    mmr: PartialMmr,
39    /// A map of block_num |-> block_header for all blocks for which the partial MMR contains
40    /// authentication paths.
41    blocks: BTreeMap<BlockNumber, BlockHeader>,
42}
43
44impl PartialBlockchain {
45    // CONSTRUCTOR
46    // --------------------------------------------------------------------------------------------
47    /// Returns a new [PartialBlockchain] instantiated from the provided partial MMR and a list of
48    /// block headers.
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if:
53    /// - block_num for any of the blocks is greater than the chain length implied by the provided
54    ///   partial MMR.
55    /// - The same block appears more than once in the provided list of block headers.
56    /// - The partial MMR does not track authentication paths for any of the specified blocks.
57    /// - Any of the provided block header's commitment is not tracked in the MMR, i.e. its
58    ///   inclusion cannot be verified.
59    pub fn new(
60        mmr: PartialMmr,
61        blocks: impl IntoIterator<Item = BlockHeader>,
62    ) -> Result<Self, PartialBlockchainError> {
63        let partial_chain = Self::new_unchecked(mmr, blocks)?;
64
65        // Verify inclusion of all provided blocks in the partial MMR.
66        for (block_num, block) in partial_chain.blocks.iter() {
67            // SAFETY: new_unchecked returns an error if a block is not tracked in the MMR, so
68            // retrieving a proof here should succeed.
69            let proof = partial_chain
70                .mmr
71                .open(block_num.as_usize())
72                .expect("block should not exceed chain length")
73                .expect("block should be tracked in the partial MMR");
74
75            partial_chain.mmr.peaks().verify(block.commitment(), proof).map_err(|source| {
76                PartialBlockchainError::BlockHeaderCommitmentMismatch {
77                    block_num: *block_num,
78                    block_commitment: block.commitment(),
79                    source,
80                }
81            })?;
82        }
83
84        Ok(partial_chain)
85    }
86
87    /// Returns a new [PartialBlockchain] instantiated from the provided partial MMR and a list of
88    /// block headers.
89    ///
90    /// # Warning
91    ///
92    /// This does not verify that the provided block commitments are in the MMR. Use [`Self::new`]
93    /// to run this verification. This constructor is provided to bypass this check in trusted
94    /// environment because it is relatively expensive.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if:
99    /// - block_num for any of the blocks is greater than the chain length implied by the provided
100    ///   partial MMR.
101    /// - The same block appears more than once in the provided list of block headers.
102    /// - The partial MMR does not track authentication paths for any of the specified blocks.
103    pub fn new_unchecked(
104        mmr: PartialMmr,
105        blocks: impl IntoIterator<Item = BlockHeader>,
106    ) -> Result<Self, PartialBlockchainError> {
107        let chain_length = mmr.forest();
108        let mut block_map = BTreeMap::new();
109        for block in blocks {
110            let block_num = block.block_num();
111            if block.block_num().as_usize() >= chain_length {
112                return Err(PartialBlockchainError::block_num_too_big(chain_length, block_num));
113            }
114
115            // Note that this only checks if a leaf exists at that position but it doesn't
116            // assert that it matches the block's commitment provided in the iterator.
117            if !mmr.is_tracked(block_num.as_usize()) {
118                return Err(PartialBlockchainError::untracked_block(block_num));
119            }
120
121            if block_map.insert(block_num, block).is_some() {
122                return Err(PartialBlockchainError::duplicate_block(block_num));
123            }
124        }
125
126        Ok(Self { mmr, blocks: block_map })
127    }
128
129    // PUBLIC ACCESSORS
130    // --------------------------------------------------------------------------------------------
131
132    /// Returns the underlying [`PartialMmr`].
133    pub fn mmr(&self) -> &PartialMmr {
134        &self.mmr
135    }
136
137    /// Returns peaks of this MMR.
138    pub fn peaks(&self) -> MmrPeaks {
139        self.mmr.peaks()
140    }
141
142    /// Returns total number of blocks contain in the chain described by this MMR.
143    pub fn chain_length(&self) -> BlockNumber {
144        BlockNumber::from(
145            u32::try_from(self.mmr.forest())
146                .expect("partial blockchain should never contain more than u32::MAX blocks"),
147        )
148    }
149
150    /// Returns `true` if a block with the given number is present in this partial blockchain.
151    ///
152    /// Note that this only checks whether an entry with the block's number exists in the MMR.
153    pub fn contains_block(&self, block_num: BlockNumber) -> bool {
154        self.blocks.contains_key(&block_num)
155    }
156
157    /// Returns the block header for the specified block, or None if the block is not present in
158    /// this partial blockchain.
159    pub fn get_block(&self, block_num: BlockNumber) -> Option<&BlockHeader> {
160        self.blocks.get(&block_num)
161    }
162
163    /// Returns an iterator over the block headers in this partial blockchain.
164    pub fn block_headers(&self) -> impl Iterator<Item = &BlockHeader> {
165        self.blocks.values()
166    }
167
168    // DATA MUTATORS
169    // --------------------------------------------------------------------------------------------
170
171    /// Appends the provided block header to this partial blockchain. This method assumes that the
172    /// provided block header is for the next block in the chain.
173    ///
174    /// If `track` parameter is set to true, the authentication path for the provided block header
175    /// will be added to this partial blockchain.
176    ///
177    /// # Panics
178    /// Panics if the `block_header.block_num` is not equal to the current chain length (i.e., the
179    /// provided block header is not the next block in the chain).
180    pub fn add_block(&mut self, block_header: BlockHeader, track: bool) {
181        assert_eq!(block_header.block_num(), self.chain_length());
182        self.mmr.add(block_header.commitment(), track);
183    }
184
185    // ITERATORS
186    // --------------------------------------------------------------------------------------------
187
188    /// Returns an iterator over the inner nodes of authentication paths contained in this chain
189    /// MMR.
190    pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
191        self.mmr.inner_nodes(
192            self.blocks
193                .values()
194                .map(|block| (block.block_num().as_usize(), block.commitment())),
195        )
196    }
197
198    // TESTING
199    // --------------------------------------------------------------------------------------------
200
201    /// Returns a mutable reference to the map of block numbers to block headers in this partial
202    /// blockchain.
203    ///
204    /// Allows mutating the inner map for testing purposes.
205    #[cfg(any(feature = "testing", test))]
206    pub fn block_headers_mut(&mut self) -> &mut BTreeMap<BlockNumber, BlockHeader> {
207        &mut self.blocks
208    }
209
210    /// Returns a mutable reference to the partial MMR of this partial blockchain.
211    ///
212    /// Allows mutating the inner partial MMR for testing purposes.
213    #[cfg(any(feature = "testing", test))]
214    pub fn partial_mmr_mut(&mut self) -> &mut PartialMmr {
215        &mut self.mmr
216    }
217}
218
219impl Serializable for PartialBlockchain {
220    fn write_into<W: miden_crypto::utils::ByteWriter>(&self, target: &mut W) {
221        self.mmr.write_into(target);
222        self.blocks.write_into(target);
223    }
224}
225
226impl Deserializable for PartialBlockchain {
227    fn read_from<R: miden_crypto::utils::ByteReader>(
228        source: &mut R,
229    ) -> Result<Self, miden_crypto::utils::DeserializationError> {
230        let mmr = PartialMmr::read_from(source)?;
231        let blocks = BTreeMap::<BlockNumber, BlockHeader>::read_from(source)?;
232        Ok(Self { mmr, blocks })
233    }
234}
235
236impl Default for PartialBlockchain {
237    fn default() -> Self {
238        // TODO: Replace with PartialMmr::default after https://github.com/0xMiden/crypto/pull/409/files
239        // is available for use.
240        let empty_mmr = PartialMmr::from_peaks(
241            MmrPeaks::new(0, Vec::new()).expect("empty MmrPeaks should be valid"),
242        );
243
244        Self::new(empty_mmr, Vec::new()).expect("empty partial blockchain should be valid")
245    }
246}
247
248// TESTS
249// ================================================================================================
250
251#[cfg(test)]
252mod tests {
253    use assert_matches::assert_matches;
254    use vm_core::utils::{Deserializable, Serializable};
255
256    use super::PartialBlockchain;
257    use crate::{
258        Digest, PartialBlockchainError,
259        alloc::vec::Vec,
260        block::{BlockHeader, BlockNumber},
261        crypto::merkle::{Mmr, PartialMmr},
262    };
263
264    #[test]
265    fn test_partial_blockchain_add() {
266        // create partial blockchain with 3 blocks - i.e., 2 peaks
267        let mut mmr = Mmr::default();
268        for i in 0..3 {
269            let block_header = int_to_block_header(i);
270            mmr.add(block_header.commitment());
271        }
272        let partial_mmr: PartialMmr = mmr.peaks().into();
273        let mut partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
274
275        // add a new block to the partial blockchain, this reduces the number of peaks to 1
276        let block_num = 3;
277        let bock_header = int_to_block_header(block_num);
278        mmr.add(bock_header.commitment());
279        partial_blockchain.add_block(bock_header, true);
280
281        assert_eq!(
282            mmr.open(block_num as usize).unwrap(),
283            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
284        );
285
286        // add one more block to the partial blockchain, the number of peaks is again 2
287        let block_num = 4;
288        let bock_header = int_to_block_header(block_num);
289        mmr.add(bock_header.commitment());
290        partial_blockchain.add_block(bock_header, true);
291
292        assert_eq!(
293            mmr.open(block_num as usize).unwrap(),
294            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
295        );
296
297        // add one more block to the partial blockchain, the number of peaks is still 2
298        let block_num = 5;
299        let bock_header = int_to_block_header(block_num);
300        mmr.add(bock_header.commitment());
301        partial_blockchain.add_block(bock_header, true);
302
303        assert_eq!(
304            mmr.open(block_num as usize).unwrap(),
305            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
306        );
307    }
308
309    #[test]
310    fn partial_blockchain_new_on_invalid_header_fails() {
311        let block_header0 = int_to_block_header(0);
312        let block_header1 = int_to_block_header(1);
313        let block_header2 = int_to_block_header(2);
314
315        let mut mmr = Mmr::default();
316        mmr.add(block_header0.commitment());
317        mmr.add(block_header1.commitment());
318        mmr.add(block_header2.commitment());
319
320        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
321        for i in 0..3 {
322            partial_mmr
323                .track(i, mmr.get(i).unwrap(), &mmr.open(i).unwrap().merkle_path)
324                .unwrap();
325        }
326
327        let fake_block_header2 = BlockHeader::mock(2, None, None, &[], Digest::default());
328
329        assert_ne!(block_header2.commitment(), fake_block_header2.commitment());
330
331        // Construct a PartialBlockchain with an invalid block header.
332        let error = PartialBlockchain::new(
333            partial_mmr,
334            vec![block_header0, block_header1, fake_block_header2.clone()],
335        )
336        .unwrap_err();
337
338        assert_matches!(
339            error,
340            PartialBlockchainError::BlockHeaderCommitmentMismatch {
341                block_commitment,
342                block_num,
343                ..
344            } if block_commitment == fake_block_header2.commitment() && block_num == fake_block_header2.block_num()
345        )
346    }
347
348    #[test]
349    fn partial_blockchain_new_on_block_number_exceeding_chain_length_fails() {
350        let block_header0 = int_to_block_header(0);
351        let mmr = Mmr::default();
352        let partial_mmr = PartialMmr::from_peaks(mmr.peaks());
353
354        let error = PartialBlockchain::new(partial_mmr, [block_header0]).unwrap_err();
355
356        assert_matches!(error, PartialBlockchainError::BlockNumTooBig {
357          chain_length,
358          block_num,
359        } if chain_length == 0 && block_num == BlockNumber::from(0));
360    }
361
362    #[test]
363    fn partial_blockchain_new_on_untracked_block_number_fails() {
364        let block_header0 = int_to_block_header(0);
365        let block_header1 = int_to_block_header(1);
366
367        let mut mmr = Mmr::default();
368        mmr.add(block_header0.commitment());
369        mmr.add(block_header1.commitment());
370
371        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
372        partial_mmr
373            .track(1, block_header1.commitment(), &mmr.open(1).unwrap().merkle_path)
374            .unwrap();
375
376        let error =
377            PartialBlockchain::new(partial_mmr, [block_header0, block_header1]).unwrap_err();
378
379        assert_matches!(error, PartialBlockchainError::UntrackedBlock {
380          block_num,
381        } if block_num == BlockNumber::from(0));
382    }
383
384    #[test]
385    fn partial_blockchain_serialization() {
386        // create partial blockchain with 3 blocks - i.e., 2 peaks
387        let mut mmr = Mmr::default();
388        for i in 0..3 {
389            let block_header = int_to_block_header(i);
390            mmr.add(block_header.commitment());
391        }
392        let partial_mmr: PartialMmr = mmr.peaks().into();
393        let partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
394
395        let bytes = partial_blockchain.to_bytes();
396        let deserialized = PartialBlockchain::read_from_bytes(&bytes).unwrap();
397
398        assert_eq!(partial_blockchain, deserialized);
399    }
400
401    fn int_to_block_header(block_num: impl Into<BlockNumber>) -> BlockHeader {
402        BlockHeader::new(
403            0,
404            Digest::default(),
405            block_num.into(),
406            Digest::default(),
407            Digest::default(),
408            Digest::default(),
409            Digest::default(),
410            Digest::default(),
411            Digest::default(),
412            Digest::default(),
413            0,
414        )
415    }
416}