Skip to main content

miden_protocol/transaction/
partial_blockchain.rs

1use alloc::collections::BTreeMap;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4use core::ops::RangeTo;
5
6use crate::block::{BlockHeader, BlockNumber};
7use crate::crypto::merkle::InnerNodeInfo;
8use crate::crypto::merkle::mmr::{MmrPeaks, PartialMmr};
9use crate::errors::PartialBlockchainError;
10use crate::utils::serde::{Deserializable, Serializable};
11
12// PARTIAL BLOCKCHAIN
13// ================================================================================================
14
15/// A partial view into the full [`Blockchain`](crate::block::Blockchain)'s Merkle Mountain Range
16/// (MMR).
17///
18/// It allows for efficient authentication of input notes during transaction execution or
19/// authentication of reference blocks during batch or block execution. Authentication is achieved
20/// by providing inclusion proofs for the notes consumed in the transaction against the partial
21/// blockchain root associated with the transaction's reference block.
22///
23/// [`PartialBlockchain`] contains authentication paths for a limited set of blocks. The intent is
24/// to include only the blocks relevant for execution:
25/// - For transactions: the set of blocks in which all input notes were created.
26/// - For batches: the set of reference blocks of all transactions in the batch and the blocks to
27///   prove any unauthenticated note's inclusion.
28/// - For blocks: the set of reference blocks of all batches in the block and the blocks to prove
29///   any unauthenticated note's inclusion.
30///
31/// # Guarantees
32///
33/// The [`PartialBlockchain`] contains the full authenticated [`BlockHeader`]s of all blocks
34/// it tracks in its partial MMR and users of this type can make this assumption. This is ensured
35/// when using [`PartialBlockchain::new`]. [`PartialBlockchain::new_unchecked`] should only be used
36/// whenever this guarantee can be upheld.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct PartialBlockchain {
39    /// Partial view of the blockchain with authentication paths for the blocks listed below.
40    mmr: PartialMmr,
41    /// A map of block_num |-> block_header for all blocks for which the partial MMR contains
42    /// authentication paths.
43    blocks: BTreeMap<BlockNumber, BlockHeader>,
44}
45
46impl PartialBlockchain {
47    // CONSTRUCTOR
48    // --------------------------------------------------------------------------------------------
49    /// Returns a new [PartialBlockchain] instantiated from the provided partial MMR and a list of
50    /// block headers.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if:
55    /// - block_num for any of the blocks is greater than the chain length implied by the provided
56    ///   partial MMR.
57    /// - The same block appears more than once in the provided list of block headers.
58    /// - The partial MMR does not track authentication paths for any of the specified blocks.
59    /// - Any of the provided block header's commitment is not tracked in the MMR, i.e. its
60    ///   inclusion cannot be verified.
61    pub fn new(
62        mmr: PartialMmr,
63        blocks: impl IntoIterator<Item = BlockHeader>,
64    ) -> Result<Self, PartialBlockchainError> {
65        let partial_chain = Self::new_unchecked(mmr, blocks)?;
66
67        // Verify inclusion of all provided blocks in the partial MMR.
68        for (block_num, block) in partial_chain.blocks.iter() {
69            // SAFETY: new_unchecked returns an error if a block is not tracked in the MMR, so
70            // retrieving a proof here should succeed.
71            let proof = partial_chain
72                .mmr
73                .open(block_num.as_usize())
74                .expect("block should not exceed chain length")
75                .expect("block should be tracked in the partial MMR");
76
77            partial_chain.mmr.peaks().verify(block.commitment(), proof).map_err(|source| {
78                PartialBlockchainError::BlockHeaderCommitmentMismatch {
79                    block_num: *block_num,
80                    block_commitment: block.commitment(),
81                    source,
82                }
83            })?;
84        }
85
86        Ok(partial_chain)
87    }
88
89    /// Returns a new [PartialBlockchain] instantiated from the provided partial MMR and a list of
90    /// block headers.
91    ///
92    /// # Warning
93    ///
94    /// This does not verify that the provided block commitments are in the MMR. Use [`Self::new`]
95    /// to run this verification. This constructor is provided to bypass this check in trusted
96    /// environment because it is relatively expensive.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if:
101    /// - block_num for any of the blocks is greater than the chain length implied by the provided
102    ///   partial MMR.
103    /// - The same block appears more than once in the provided list of block headers.
104    /// - The partial MMR does not track authentication paths for any of the specified blocks.
105    pub fn new_unchecked(
106        mmr: PartialMmr,
107        blocks: impl IntoIterator<Item = BlockHeader>,
108    ) -> Result<Self, PartialBlockchainError> {
109        let chain_length = mmr.forest().num_leaves();
110        let mut block_map = BTreeMap::new();
111        for block in blocks {
112            let block_num = block.block_num();
113            if block.block_num().as_usize() >= chain_length {
114                return Err(PartialBlockchainError::block_num_too_big(chain_length, block_num));
115            }
116
117            // Note that this only checks if a leaf exists at that position but it doesn't
118            // assert that it matches the block's commitment provided in the iterator.
119            if !mmr.is_tracked(block_num.as_usize()) {
120                return Err(PartialBlockchainError::untracked_block(block_num));
121            }
122
123            if block_map.insert(block_num, block).is_some() {
124                return Err(PartialBlockchainError::duplicate_block(block_num));
125            }
126        }
127
128        Ok(Self { mmr, blocks: block_map })
129    }
130
131    // PUBLIC ACCESSORS
132    // --------------------------------------------------------------------------------------------
133
134    /// Returns the underlying [`PartialMmr`].
135    pub fn mmr(&self) -> &PartialMmr {
136        &self.mmr
137    }
138
139    /// Returns peaks of this MMR.
140    pub fn peaks(&self) -> MmrPeaks {
141        self.mmr.peaks()
142    }
143
144    /// Returns total number of blocks contain in the chain described by this MMR.
145    pub fn chain_length(&self) -> BlockNumber {
146        BlockNumber::from(
147            u32::try_from(self.mmr.forest().num_leaves())
148                .expect("partial blockchain should never contain more than u32::MAX blocks"),
149        )
150    }
151
152    /// Returns the number of blocks tracked by this partial blockchain.
153    pub fn num_tracked_blocks(&self) -> usize {
154        self.blocks.len()
155    }
156
157    /// Returns `true` if a block with the given number is present in this partial blockchain.
158    ///
159    /// Note that this only checks whether an entry with the block's number exists in the MMR.
160    pub fn contains_block(&self, block_num: BlockNumber) -> bool {
161        self.blocks.contains_key(&block_num)
162    }
163
164    /// Returns the block header for the specified block, or None if the block is not present in
165    /// this partial blockchain.
166    pub fn get_block(&self, block_num: BlockNumber) -> Option<&BlockHeader> {
167        self.blocks.get(&block_num)
168    }
169
170    /// Returns an iterator over the block headers in this partial blockchain.
171    pub fn block_headers(&self) -> impl Iterator<Item = &BlockHeader> {
172        self.blocks.values()
173    }
174
175    // DATA MUTATORS
176    // --------------------------------------------------------------------------------------------
177
178    /// Appends the provided block header to this partial blockchain. This method assumes that the
179    /// provided block header is for the next block in the chain.
180    ///
181    /// If `track` parameter is set to true, the authentication path for the provided block header
182    /// will be added to this partial blockchain, and the block header will be stored for later
183    /// retrieval.
184    ///
185    /// # Panics
186    ///
187    /// Panics if:
188    /// - The `block_header.block_num` is not equal to the current chain length (i.e., the provided
189    ///   block header is not the next block in the chain).
190    /// - The the chain length exceeds [miden_crypto::merkle::mmr::Forest::MAX_LEAVES].
191    pub fn add_block(&mut self, block_header: &BlockHeader, track: bool) {
192        assert_eq!(block_header.block_num(), self.chain_length());
193        self.mmr
194            .add(block_header.commitment(), track)
195            .expect("partial mmr leaf count exceeds forest leaf bound");
196        if track {
197            self.blocks.insert(block_header.block_num(), block_header.clone());
198        }
199    }
200
201    /// Drop every block header whose number is strictly less than `to.end`.
202    ///
203    /// After the call, all such headers are removed, and each pruned header’s path is `untrack`‑ed
204    /// from the internal [`PartialMmr`], eliminating local authentication data for those leaves
205    /// while leaving the MMR root commitment unchanged.
206    pub fn prune_to(&mut self, to: RangeTo<BlockNumber>) {
207        let kept = self.blocks.split_off(&to.end);
208
209        for block_num in self.blocks.keys() {
210            self.mmr.untrack(block_num.as_usize());
211        }
212        self.blocks = kept;
213    }
214
215    /// Removes a single block header and the associated authentication path from this
216    /// [`PartialBlockchain`].
217    ///
218    /// This does not change the commitment to the underlying MMR, but the current partial MMR
219    /// will no longer track the removed data.
220    pub fn remove(&mut self, block_num: BlockNumber) {
221        if self.blocks.remove(&block_num).is_some() {
222            self.mmr.untrack(block_num.as_usize());
223        }
224    }
225
226    // ITERATORS
227    // --------------------------------------------------------------------------------------------
228
229    /// Returns an iterator over the inner nodes of authentication paths contained in this chain
230    /// MMR.
231    pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
232        self.mmr.inner_nodes(
233            self.blocks
234                .values()
235                .map(|block| (block.block_num().as_usize(), block.commitment())),
236        )
237    }
238
239    // TESTING
240    // --------------------------------------------------------------------------------------------
241
242    /// Returns a mutable reference to the map of block numbers to block headers in this partial
243    /// blockchain.
244    ///
245    /// Allows mutating the inner map for testing purposes.
246    #[cfg(any(feature = "testing", test))]
247    pub fn block_headers_mut(&mut self) -> &mut BTreeMap<BlockNumber, BlockHeader> {
248        &mut self.blocks
249    }
250
251    /// Returns a mutable reference to the partial MMR of this partial blockchain.
252    ///
253    /// Allows mutating the inner partial MMR for testing purposes.
254    #[cfg(any(feature = "testing", test))]
255    pub fn partial_mmr_mut(&mut self) -> &mut PartialMmr {
256        &mut self.mmr
257    }
258}
259
260impl Serializable for PartialBlockchain {
261    fn write_into<W: miden_crypto::utils::ByteWriter>(&self, target: &mut W) {
262        self.mmr.write_into(target);
263        self.blocks.write_into(target);
264    }
265}
266
267impl Deserializable for PartialBlockchain {
268    fn read_from<R: miden_crypto::utils::ByteReader>(
269        source: &mut R,
270    ) -> Result<Self, miden_crypto::utils::DeserializationError> {
271        let mmr = PartialMmr::read_from(source)?;
272        let blocks = BTreeMap::<BlockNumber, BlockHeader>::read_from(source)?;
273        Self::new(mmr, blocks.into_values())
274            .map_err(|err| miden_crypto::utils::DeserializationError::InvalidValue(err.to_string()))
275    }
276}
277
278impl Default for PartialBlockchain {
279    fn default() -> Self {
280        Self::new(PartialMmr::default(), Vec::new())
281            .expect("empty partial blockchain should be valid")
282    }
283}
284
285// TESTS
286// ================================================================================================
287
288#[cfg(test)]
289mod tests {
290    use assert_matches::assert_matches;
291    use rand::SeedableRng;
292    use rand_chacha::ChaCha20Rng;
293
294    use super::PartialBlockchain;
295    use crate::Word;
296    use crate::alloc::vec::Vec;
297    use crate::block::{BlockHeader, BlockNumber, FeeParameters, ValidatorConfig};
298    use crate::crypto::dsa::ecdsa_k256_keccak::SigningKey;
299    use crate::crypto::merkle::mmr::{Mmr, PartialMmr};
300    use crate::errors::PartialBlockchainError;
301    use crate::utils::serde::{Deserializable, DeserializationError, Serializable};
302
303    #[test]
304    fn test_partial_blockchain_add() {
305        // create partial blockchain with 3 blocks - i.e., 2 peaks
306        let mut mmr = Mmr::default();
307        for i in 0..3 {
308            let block_header = int_to_block_header(i);
309            mmr.add(block_header.commitment())
310                .expect("mmr leaf count exceeds forest leaf bound");
311        }
312        let partial_mmr: PartialMmr = mmr.peaks().into();
313        let mut partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
314
315        // add a new block to the partial blockchain, this reduces the number of peaks to 1
316        let block_num = 3;
317        let block_header = int_to_block_header(block_num);
318        mmr.add(block_header.commitment())
319            .expect("mmr leaf count exceeds forest leaf bound");
320        partial_blockchain.add_block(&block_header, true);
321
322        assert_eq!(
323            mmr.open(block_num as usize).unwrap(),
324            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
325        );
326
327        // add one more block to the partial blockchain, the number of peaks is again 2
328        let block_num = 4;
329        let block_header = int_to_block_header(block_num);
330        mmr.add(block_header.commitment())
331            .expect("mmr leaf count exceeds forest leaf bound");
332        partial_blockchain.add_block(&block_header, true);
333
334        assert_eq!(
335            mmr.open(block_num as usize).unwrap(),
336            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
337        );
338
339        // add one more block to the partial blockchain, the number of peaks is still 2
340        let block_num = 5;
341        let block_header = int_to_block_header(block_num);
342        mmr.add(block_header.commitment())
343            .expect("mmr leaf count exceeds forest leaf bound");
344        partial_blockchain.add_block(&block_header, true);
345
346        assert_eq!(
347            mmr.open(block_num as usize).unwrap(),
348            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
349        );
350    }
351
352    #[test]
353    fn partial_blockchain_new_on_invalid_header_fails() {
354        let block_header0 = int_to_block_header(0);
355        let block_header1 = int_to_block_header(1);
356        let block_header2 = int_to_block_header(2);
357
358        let mut mmr = Mmr::default();
359        mmr.add(block_header0.commitment()).unwrap();
360        mmr.add(block_header1.commitment()).unwrap();
361        mmr.add(block_header2.commitment()).unwrap();
362
363        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
364        for i in 0..3 {
365            partial_mmr
366                .track(i, mmr.get(i).unwrap(), mmr.open(i).unwrap().merkle_path())
367                .unwrap();
368        }
369
370        let fake_block_header2 = BlockHeader::mock(2, None, None, &[]);
371
372        assert_ne!(block_header2.commitment(), fake_block_header2.commitment());
373
374        // Construct a PartialBlockchain with an invalid block header.
375        let error = PartialBlockchain::new(
376            partial_mmr,
377            vec![block_header0, block_header1, fake_block_header2.clone()],
378        )
379        .unwrap_err();
380
381        assert_matches!(
382            error,
383            PartialBlockchainError::BlockHeaderCommitmentMismatch {
384                block_commitment,
385                block_num,
386                ..
387            } if block_commitment == fake_block_header2.commitment() && block_num == fake_block_header2.block_num()
388        )
389    }
390
391    #[test]
392    fn partial_blockchain_deserialization_on_invalid_header_fails() {
393        let block_header0 = int_to_block_header(0);
394        let block_header1 = int_to_block_header(1);
395        let block_header2 = int_to_block_header(2);
396
397        let mut mmr = Mmr::default();
398        mmr.add(block_header0.commitment()).unwrap();
399        mmr.add(block_header1.commitment()).unwrap();
400        mmr.add(block_header2.commitment()).unwrap();
401
402        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
403        for i in 0..3 {
404            partial_mmr
405                .track(i, mmr.get(i).unwrap(), mmr.open(i).unwrap().merkle_path())
406                .unwrap();
407        }
408
409        let fake_block_header2 = BlockHeader::mock(2, None, None, &[]);
410
411        assert_ne!(block_header2.commitment(), fake_block_header2.commitment());
412
413        let forged_partial_blockchain = PartialBlockchain::new_unchecked(
414            partial_mmr,
415            vec![block_header0, block_header1, fake_block_header2],
416        )
417        .unwrap();
418        let bytes = forged_partial_blockchain.to_bytes();
419
420        let err = PartialBlockchain::read_from_bytes(&bytes).unwrap_err();
421        assert_matches!(err, DeserializationError::InvalidValue(_));
422    }
423
424    #[test]
425    fn partial_blockchain_new_on_block_number_exceeding_chain_length_fails() {
426        let block_header0 = int_to_block_header(0);
427        let mmr = Mmr::default();
428        let partial_mmr = PartialMmr::from_peaks(mmr.peaks());
429
430        let error = PartialBlockchain::new(partial_mmr, [block_header0]).unwrap_err();
431
432        assert_matches!(error, PartialBlockchainError::BlockNumTooBig {
433          chain_length,
434          block_num,
435        } if chain_length == 0 && block_num == BlockNumber::from(0));
436    }
437
438    #[test]
439    fn partial_blockchain_new_on_untracked_block_number_fails() {
440        let block_header0 = int_to_block_header(0);
441        let block_header1 = int_to_block_header(1);
442
443        let mut mmr = Mmr::default();
444        mmr.add(block_header0.commitment()).unwrap();
445        mmr.add(block_header1.commitment()).unwrap();
446
447        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
448        partial_mmr
449            .track(1, block_header1.commitment(), mmr.open(1).unwrap().merkle_path())
450            .unwrap();
451
452        let error =
453            PartialBlockchain::new(partial_mmr, [block_header0, block_header1]).unwrap_err();
454
455        assert_matches!(error, PartialBlockchainError::UntrackedBlock {
456          block_num,
457        } if block_num == BlockNumber::from(0));
458    }
459
460    #[test]
461    fn partial_blockchain_serialization() {
462        // create partial blockchain with 3 blocks - i.e., 2 peaks
463        let mut mmr = Mmr::default();
464        for i in 0..3 {
465            let block_header = int_to_block_header(i);
466            mmr.add(block_header.commitment()).unwrap();
467        }
468        let partial_mmr: PartialMmr = mmr.peaks().into();
469        let partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
470
471        let bytes = partial_blockchain.to_bytes();
472        let deserialized = PartialBlockchain::read_from_bytes(&bytes).unwrap();
473
474        assert_eq!(partial_blockchain, deserialized);
475    }
476
477    fn int_to_block_header(block_num: impl Into<BlockNumber>) -> BlockHeader {
478        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
479        let validator_config =
480            ValidatorConfig::new(alloc::vec![SigningKey::with_rng(&mut rng).public_key()], 1)
481                .unwrap();
482
483        BlockHeader::new(
484            Word::empty(),
485            block_num.into(),
486            Word::empty(),
487            Word::empty(),
488            Word::empty(),
489            Word::empty(),
490            Word::empty(),
491            validator_config,
492            FeeParameters::new(500),
493            Word::empty(),
494            None,
495            0,
496        )
497    }
498
499    #[test]
500    fn prune_before_and_remove() {
501        let total_blocks = 128;
502        let remove_before = 40;
503
504        let mut full_mmr = Mmr::default();
505        let mut headers = Vec::new();
506        for i in 0..total_blocks {
507            let h = int_to_block_header(i);
508            full_mmr.add(h.commitment()).unwrap();
509            headers.push(h);
510        }
511        let mut partial_mmr: PartialMmr = full_mmr.peaks().into();
512        for i in 0..total_blocks {
513            let i: usize = i as usize;
514            partial_mmr
515                .track(i, full_mmr.get(i).unwrap(), full_mmr.open(i).unwrap().merkle_path())
516                .unwrap();
517        }
518        let mut chain = PartialBlockchain::new(partial_mmr, headers).unwrap();
519        assert_eq!(chain.num_tracked_blocks(), total_blocks as usize);
520
521        chain.remove(BlockNumber::from(2));
522        assert!(!chain.contains_block(2.into()));
523        assert!(!chain.mmr().is_tracked(2));
524        assert_eq!(chain.num_tracked_blocks(), (total_blocks - 1) as usize);
525
526        assert!(chain.contains_block(3.into()));
527
528        chain.prune_to(..40.into());
529        assert_eq!(chain.num_tracked_blocks(), (total_blocks - 40) as usize);
530
531        assert_eq!(chain.block_headers().count(), (total_blocks - remove_before) as usize);
532        for block_num in remove_before..total_blocks {
533            assert!(chain.contains_block(block_num.into()));
534            assert!(chain.mmr().is_tracked(block_num as usize));
535        }
536        for block_num in 0u32..remove_before {
537            assert!(!chain.contains_block(block_num.into()));
538            assert!(!chain.mmr().is_tracked(block_num as usize));
539        }
540    }
541
542    #[test]
543    fn add_block_with_track_adds_to_blocks() {
544        let mut blockchain = PartialBlockchain::default();
545        let header = int_to_block_header(0);
546
547        blockchain.add_block(&header, true);
548
549        assert!(blockchain.contains_block(0.into()));
550        assert_eq!(blockchain.num_tracked_blocks(), 1);
551    }
552
553    #[test]
554    fn add_block_without_track_does_not_add_to_blocks() {
555        let mut blockchain = PartialBlockchain::default();
556        let header = int_to_block_header(0);
557
558        blockchain.add_block(&header, false);
559
560        assert!(!blockchain.contains_block(0.into()));
561        assert_eq!(blockchain.num_tracked_blocks(), 0);
562    }
563
564    #[test]
565    fn prune_to_removes_tracked_blocks() {
566        let mut blockchain = PartialBlockchain::default();
567        // Add 10 blocks with tracking
568        for i in 0..10u32 {
569            let header = int_to_block_header(i);
570            blockchain.add_block(&header, true);
571        }
572        assert_eq!(blockchain.num_tracked_blocks(), 10);
573
574        // Prune to keep only last 4
575        blockchain.prune_to(..6.into());
576
577        assert_eq!(blockchain.num_tracked_blocks(), 4);
578        for i in 0u32..6 {
579            assert!(!blockchain.contains_block(i.into()));
580            // Verify the underlying MMR also untracked the block
581            assert!(!blockchain.mmr().is_tracked(i as usize));
582        }
583        for i in 6u32..10 {
584            assert!(blockchain.contains_block(i.into()));
585            assert!(blockchain.mmr().is_tracked(i as usize));
586        }
587    }
588}