miden_objects/transaction/
partial_blockchain.rs

1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3use core::ops::RangeTo;
4
5use crate::PartialBlockchainError;
6use crate::block::{BlockHeader, BlockNumber};
7use crate::crypto::merkle::{InnerNodeInfo, MmrPeaks, PartialMmr};
8use crate::utils::serde::{Deserializable, Serializable};
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().num_leaves();
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().num_leaves())
146                .expect("partial blockchain should never contain more than u32::MAX blocks"),
147        )
148    }
149
150    /// Returns the number of blocks tracked by this partial blockchain.
151    pub fn num_tracked_blocks(&self) -> usize {
152        self.blocks.len()
153    }
154
155    /// Returns `true` if a block with the given number is present in this partial blockchain.
156    ///
157    /// Note that this only checks whether an entry with the block's number exists in the MMR.
158    pub fn contains_block(&self, block_num: BlockNumber) -> bool {
159        self.blocks.contains_key(&block_num)
160    }
161
162    /// Returns the block header for the specified block, or None if the block is not present in
163    /// this partial blockchain.
164    pub fn get_block(&self, block_num: BlockNumber) -> Option<&BlockHeader> {
165        self.blocks.get(&block_num)
166    }
167
168    /// Returns an iterator over the block headers in this partial blockchain.
169    pub fn block_headers(&self) -> impl Iterator<Item = &BlockHeader> {
170        self.blocks.values()
171    }
172
173    // DATA MUTATORS
174    // --------------------------------------------------------------------------------------------
175
176    /// Appends the provided block header to this partial blockchain. This method assumes that the
177    /// provided block header is for the next block in the chain.
178    ///
179    /// If `track` parameter is set to true, the authentication path for the provided block header
180    /// will be added to this partial blockchain.
181    ///
182    /// # Panics
183    /// Panics if the `block_header.block_num` is not equal to the current chain length (i.e., the
184    /// provided block header is not the next block in the chain).
185    pub fn add_block(&mut self, block_header: BlockHeader, track: bool) {
186        assert_eq!(block_header.block_num(), self.chain_length());
187        self.mmr.add(block_header.commitment(), track);
188    }
189
190    /// Drop every block header whose number is strictly less than `to.end`.
191    ///
192    /// After the call, all such headers are removed, and each pruned header’s path is `untrack`‑ed
193    /// from the internal [`PartialMmr`], eliminating local authentication data for those leaves
194    /// while leaving the MMR root commitment unchanged.
195    pub fn prune_to(&mut self, to: RangeTo<BlockNumber>) {
196        let kept = self.blocks.split_off(&to.end);
197
198        for block_num in self.blocks.keys() {
199            self.mmr.untrack(block_num.as_usize());
200        }
201        self.blocks = kept;
202    }
203
204    /// Removes a single block header and the associated authentication path from this
205    /// [`PartialBlockchain`].
206    ///
207    /// This does not change the commitment to the underlying MMR, but the current partial MMR
208    /// will no longer track the removed data.
209    pub fn remove(&mut self, block_num: BlockNumber) {
210        if self.blocks.remove(&block_num).is_some() {
211            self.mmr.untrack(block_num.as_usize());
212        }
213    }
214
215    // ITERATORS
216    // --------------------------------------------------------------------------------------------
217
218    /// Returns an iterator over the inner nodes of authentication paths contained in this chain
219    /// MMR.
220    pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
221        self.mmr.inner_nodes(
222            self.blocks
223                .values()
224                .map(|block| (block.block_num().as_usize(), block.commitment())),
225        )
226    }
227
228    // TESTING
229    // --------------------------------------------------------------------------------------------
230
231    /// Returns a mutable reference to the map of block numbers to block headers in this partial
232    /// blockchain.
233    ///
234    /// Allows mutating the inner map for testing purposes.
235    #[cfg(any(feature = "testing", test))]
236    pub fn block_headers_mut(&mut self) -> &mut BTreeMap<BlockNumber, BlockHeader> {
237        &mut self.blocks
238    }
239
240    /// Returns a mutable reference to the partial MMR of this partial blockchain.
241    ///
242    /// Allows mutating the inner partial MMR for testing purposes.
243    #[cfg(any(feature = "testing", test))]
244    pub fn partial_mmr_mut(&mut self) -> &mut PartialMmr {
245        &mut self.mmr
246    }
247}
248
249impl Serializable for PartialBlockchain {
250    fn write_into<W: miden_crypto::utils::ByteWriter>(&self, target: &mut W) {
251        self.mmr.write_into(target);
252        self.blocks.write_into(target);
253    }
254}
255
256impl Deserializable for PartialBlockchain {
257    fn read_from<R: miden_crypto::utils::ByteReader>(
258        source: &mut R,
259    ) -> Result<Self, miden_crypto::utils::DeserializationError> {
260        let mmr = PartialMmr::read_from(source)?;
261        let blocks = BTreeMap::<BlockNumber, BlockHeader>::read_from(source)?;
262        Ok(Self { mmr, blocks })
263    }
264}
265
266impl Default for PartialBlockchain {
267    fn default() -> Self {
268        Self::new(PartialMmr::default(), Vec::new())
269            .expect("empty partial blockchain should be valid")
270    }
271}
272
273// TESTS
274// ================================================================================================
275
276#[cfg(test)]
277mod tests {
278    use assert_matches::assert_matches;
279    use miden_core::utils::{Deserializable, Serializable};
280
281    use super::PartialBlockchain;
282    use crate::alloc::vec::Vec;
283    use crate::block::{BlockHeader, BlockNumber, FeeParameters};
284    use crate::crypto::merkle::{Mmr, PartialMmr};
285    use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
286    use crate::{PartialBlockchainError, Word};
287
288    #[test]
289    fn test_partial_blockchain_add() {
290        // create partial blockchain with 3 blocks - i.e., 2 peaks
291        let mut mmr = Mmr::default();
292        for i in 0..3 {
293            let block_header = int_to_block_header(i);
294            mmr.add(block_header.commitment());
295        }
296        let partial_mmr: PartialMmr = mmr.peaks().into();
297        let mut partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
298
299        // add a new block to the partial blockchain, this reduces the number of peaks to 1
300        let block_num = 3;
301        let bock_header = int_to_block_header(block_num);
302        mmr.add(bock_header.commitment());
303        partial_blockchain.add_block(bock_header, true);
304
305        assert_eq!(
306            mmr.open(block_num as usize).unwrap(),
307            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
308        );
309
310        // add one more block to the partial blockchain, the number of peaks is again 2
311        let block_num = 4;
312        let bock_header = int_to_block_header(block_num);
313        mmr.add(bock_header.commitment());
314        partial_blockchain.add_block(bock_header, true);
315
316        assert_eq!(
317            mmr.open(block_num as usize).unwrap(),
318            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
319        );
320
321        // add one more block to the partial blockchain, the number of peaks is still 2
322        let block_num = 5;
323        let bock_header = int_to_block_header(block_num);
324        mmr.add(bock_header.commitment());
325        partial_blockchain.add_block(bock_header, true);
326
327        assert_eq!(
328            mmr.open(block_num as usize).unwrap(),
329            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
330        );
331    }
332
333    #[test]
334    fn partial_blockchain_new_on_invalid_header_fails() {
335        let block_header0 = int_to_block_header(0);
336        let block_header1 = int_to_block_header(1);
337        let block_header2 = int_to_block_header(2);
338
339        let mut mmr = Mmr::default();
340        mmr.add(block_header0.commitment());
341        mmr.add(block_header1.commitment());
342        mmr.add(block_header2.commitment());
343
344        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
345        for i in 0..3 {
346            partial_mmr
347                .track(i, mmr.get(i).unwrap(), &mmr.open(i).unwrap().merkle_path)
348                .unwrap();
349        }
350
351        let fake_block_header2 = BlockHeader::mock(2, None, None, &[], Word::empty());
352
353        assert_ne!(block_header2.commitment(), fake_block_header2.commitment());
354
355        // Construct a PartialBlockchain with an invalid block header.
356        let error = PartialBlockchain::new(
357            partial_mmr,
358            vec![block_header0, block_header1, fake_block_header2.clone()],
359        )
360        .unwrap_err();
361
362        assert_matches!(
363            error,
364            PartialBlockchainError::BlockHeaderCommitmentMismatch {
365                block_commitment,
366                block_num,
367                ..
368            } if block_commitment == fake_block_header2.commitment() && block_num == fake_block_header2.block_num()
369        )
370    }
371
372    #[test]
373    fn partial_blockchain_new_on_block_number_exceeding_chain_length_fails() {
374        let block_header0 = int_to_block_header(0);
375        let mmr = Mmr::default();
376        let partial_mmr = PartialMmr::from_peaks(mmr.peaks());
377
378        let error = PartialBlockchain::new(partial_mmr, [block_header0]).unwrap_err();
379
380        assert_matches!(error, PartialBlockchainError::BlockNumTooBig {
381          chain_length,
382          block_num,
383        } if chain_length == 0 && block_num == BlockNumber::from(0));
384    }
385
386    #[test]
387    fn partial_blockchain_new_on_untracked_block_number_fails() {
388        let block_header0 = int_to_block_header(0);
389        let block_header1 = int_to_block_header(1);
390
391        let mut mmr = Mmr::default();
392        mmr.add(block_header0.commitment());
393        mmr.add(block_header1.commitment());
394
395        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
396        partial_mmr
397            .track(1, block_header1.commitment(), &mmr.open(1).unwrap().merkle_path)
398            .unwrap();
399
400        let error =
401            PartialBlockchain::new(partial_mmr, [block_header0, block_header1]).unwrap_err();
402
403        assert_matches!(error, PartialBlockchainError::UntrackedBlock {
404          block_num,
405        } if block_num == BlockNumber::from(0));
406    }
407
408    #[test]
409    fn partial_blockchain_serialization() {
410        // create partial blockchain with 3 blocks - i.e., 2 peaks
411        let mut mmr = Mmr::default();
412        for i in 0..3 {
413            let block_header = int_to_block_header(i);
414            mmr.add(block_header.commitment());
415        }
416        let partial_mmr: PartialMmr = mmr.peaks().into();
417        let partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
418
419        let bytes = partial_blockchain.to_bytes();
420        let deserialized = PartialBlockchain::read_from_bytes(&bytes).unwrap();
421
422        assert_eq!(partial_blockchain, deserialized);
423    }
424
425    fn int_to_block_header(block_num: impl Into<BlockNumber>) -> BlockHeader {
426        let fee_parameters =
427            FeeParameters::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500)
428                .expect("native asset ID should be a fungible faucet ID");
429
430        BlockHeader::new(
431            0,
432            Word::empty(),
433            block_num.into(),
434            Word::empty(),
435            Word::empty(),
436            Word::empty(),
437            Word::empty(),
438            Word::empty(),
439            Word::empty(),
440            Word::empty(),
441            fee_parameters,
442            0,
443        )
444    }
445
446    #[test]
447    fn prune_before_and_remove() {
448        let total_blocks = 128;
449        let remove_before = 40;
450
451        let mut full_mmr = Mmr::default();
452        let mut headers = Vec::new();
453        for i in 0..total_blocks {
454            let h = int_to_block_header(i);
455            full_mmr.add(h.commitment());
456            headers.push(h);
457        }
458        let mut partial_mmr: PartialMmr = full_mmr.peaks().into();
459        for i in 0..total_blocks {
460            let i: usize = i as usize;
461            partial_mmr
462                .track(i, full_mmr.get(i).unwrap(), &full_mmr.open(i).unwrap().merkle_path)
463                .unwrap();
464        }
465        let mut chain = PartialBlockchain::new(partial_mmr, headers).unwrap();
466        assert_eq!(chain.num_tracked_blocks(), total_blocks as usize);
467
468        chain.remove(BlockNumber::from(2));
469        assert!(!chain.contains_block(2.into()));
470        assert!(!chain.mmr().is_tracked(2));
471        assert_eq!(chain.num_tracked_blocks(), (total_blocks - 1) as usize);
472
473        assert!(chain.contains_block(3.into()));
474
475        chain.prune_to(..40.into());
476        assert_eq!(chain.num_tracked_blocks(), (total_blocks - 40) as usize);
477
478        assert_eq!(chain.block_headers().count(), (total_blocks - remove_before) as usize);
479        for block_num in remove_before..total_blocks {
480            assert!(chain.contains_block(block_num.into()));
481            assert!(chain.mmr().is_tracked(block_num as usize));
482        }
483        for block_num in 0u32..remove_before {
484            assert!(!chain.contains_block(block_num.into()));
485            assert!(!chain.mmr().is_tracked(block_num as usize));
486        }
487    }
488}