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