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};
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::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
302    use crate::utils::serde::{Deserializable, DeserializationError, Serializable};
303
304    #[test]
305    fn test_partial_blockchain_add() {
306        // create partial blockchain with 3 blocks - i.e., 2 peaks
307        let mut mmr = Mmr::default();
308        for i in 0..3 {
309            let block_header = int_to_block_header(i);
310            mmr.add(block_header.commitment())
311                .expect("mmr leaf count exceeds forest leaf bound");
312        }
313        let partial_mmr: PartialMmr = mmr.peaks().into();
314        let mut partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
315
316        // add a new block to the partial blockchain, this reduces the number of peaks to 1
317        let block_num = 3;
318        let block_header = int_to_block_header(block_num);
319        mmr.add(block_header.commitment())
320            .expect("mmr leaf count exceeds forest leaf bound");
321        partial_blockchain.add_block(&block_header, true);
322
323        assert_eq!(
324            mmr.open(block_num as usize).unwrap(),
325            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
326        );
327
328        // add one more block to the partial blockchain, the number of peaks is again 2
329        let block_num = 4;
330        let block_header = int_to_block_header(block_num);
331        mmr.add(block_header.commitment())
332            .expect("mmr leaf count exceeds forest leaf bound");
333        partial_blockchain.add_block(&block_header, true);
334
335        assert_eq!(
336            mmr.open(block_num as usize).unwrap(),
337            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
338        );
339
340        // add one more block to the partial blockchain, the number of peaks is still 2
341        let block_num = 5;
342        let block_header = int_to_block_header(block_num);
343        mmr.add(block_header.commitment())
344            .expect("mmr leaf count exceeds forest leaf bound");
345        partial_blockchain.add_block(&block_header, true);
346
347        assert_eq!(
348            mmr.open(block_num as usize).unwrap(),
349            partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
350        );
351    }
352
353    #[test]
354    fn partial_blockchain_new_on_invalid_header_fails() {
355        let block_header0 = int_to_block_header(0);
356        let block_header1 = int_to_block_header(1);
357        let block_header2 = int_to_block_header(2);
358
359        let mut mmr = Mmr::default();
360        mmr.add(block_header0.commitment()).unwrap();
361        mmr.add(block_header1.commitment()).unwrap();
362        mmr.add(block_header2.commitment()).unwrap();
363
364        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
365        for i in 0..3 {
366            partial_mmr
367                .track(i, mmr.get(i).unwrap(), mmr.open(i).unwrap().merkle_path())
368                .unwrap();
369        }
370
371        let fake_block_header2 = BlockHeader::mock(2, None, None, &[], Word::empty());
372
373        assert_ne!(block_header2.commitment(), fake_block_header2.commitment());
374
375        // Construct a PartialBlockchain with an invalid block header.
376        let error = PartialBlockchain::new(
377            partial_mmr,
378            vec![block_header0, block_header1, fake_block_header2.clone()],
379        )
380        .unwrap_err();
381
382        assert_matches!(
383            error,
384            PartialBlockchainError::BlockHeaderCommitmentMismatch {
385                block_commitment,
386                block_num,
387                ..
388            } if block_commitment == fake_block_header2.commitment() && block_num == fake_block_header2.block_num()
389        )
390    }
391
392    #[test]
393    fn partial_blockchain_deserialization_on_invalid_header_fails() {
394        let block_header0 = int_to_block_header(0);
395        let block_header1 = int_to_block_header(1);
396        let block_header2 = int_to_block_header(2);
397
398        let mut mmr = Mmr::default();
399        mmr.add(block_header0.commitment()).unwrap();
400        mmr.add(block_header1.commitment()).unwrap();
401        mmr.add(block_header2.commitment()).unwrap();
402
403        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
404        for i in 0..3 {
405            partial_mmr
406                .track(i, mmr.get(i).unwrap(), mmr.open(i).unwrap().merkle_path())
407                .unwrap();
408        }
409
410        let fake_block_header2 = BlockHeader::mock(2, None, None, &[], Word::empty());
411
412        assert_ne!(block_header2.commitment(), fake_block_header2.commitment());
413
414        let forged_partial_blockchain = PartialBlockchain::new_unchecked(
415            partial_mmr,
416            vec![block_header0, block_header1, fake_block_header2],
417        )
418        .unwrap();
419        let bytes = forged_partial_blockchain.to_bytes();
420
421        let err = PartialBlockchain::read_from_bytes(&bytes).unwrap_err();
422        assert_matches!(err, DeserializationError::InvalidValue(_));
423    }
424
425    #[test]
426    fn partial_blockchain_new_on_block_number_exceeding_chain_length_fails() {
427        let block_header0 = int_to_block_header(0);
428        let mmr = Mmr::default();
429        let partial_mmr = PartialMmr::from_peaks(mmr.peaks());
430
431        let error = PartialBlockchain::new(partial_mmr, [block_header0]).unwrap_err();
432
433        assert_matches!(error, PartialBlockchainError::BlockNumTooBig {
434          chain_length,
435          block_num,
436        } if chain_length == 0 && block_num == BlockNumber::from(0));
437    }
438
439    #[test]
440    fn partial_blockchain_new_on_untracked_block_number_fails() {
441        let block_header0 = int_to_block_header(0);
442        let block_header1 = int_to_block_header(1);
443
444        let mut mmr = Mmr::default();
445        mmr.add(block_header0.commitment()).unwrap();
446        mmr.add(block_header1.commitment()).unwrap();
447
448        let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
449        partial_mmr
450            .track(1, block_header1.commitment(), mmr.open(1).unwrap().merkle_path())
451            .unwrap();
452
453        let error =
454            PartialBlockchain::new(partial_mmr, [block_header0, block_header1]).unwrap_err();
455
456        assert_matches!(error, PartialBlockchainError::UntrackedBlock {
457          block_num,
458        } if block_num == BlockNumber::from(0));
459    }
460
461    #[test]
462    fn partial_blockchain_serialization() {
463        // create partial blockchain with 3 blocks - i.e., 2 peaks
464        let mut mmr = Mmr::default();
465        for i in 0..3 {
466            let block_header = int_to_block_header(i);
467            mmr.add(block_header.commitment()).unwrap();
468        }
469        let partial_mmr: PartialMmr = mmr.peaks().into();
470        let partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
471
472        let bytes = partial_blockchain.to_bytes();
473        let deserialized = PartialBlockchain::read_from_bytes(&bytes).unwrap();
474
475        assert_eq!(partial_blockchain, deserialized);
476    }
477
478    fn int_to_block_header(block_num: impl Into<BlockNumber>) -> BlockHeader {
479        let fee_parameters =
480            FeeParameters::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500);
481        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
482        let validator_key = SigningKey::with_rng(&mut rng).public_key();
483
484        BlockHeader::new(
485            0,
486            Word::empty(),
487            block_num.into(),
488            Word::empty(),
489            Word::empty(),
490            Word::empty(),
491            Word::empty(),
492            Word::empty(),
493            Word::empty(),
494            validator_key,
495            fee_parameters,
496            0,
497        )
498    }
499
500    #[test]
501    fn prune_before_and_remove() {
502        let total_blocks = 128;
503        let remove_before = 40;
504
505        let mut full_mmr = Mmr::default();
506        let mut headers = Vec::new();
507        for i in 0..total_blocks {
508            let h = int_to_block_header(i);
509            full_mmr.add(h.commitment()).unwrap();
510            headers.push(h);
511        }
512        let mut partial_mmr: PartialMmr = full_mmr.peaks().into();
513        for i in 0..total_blocks {
514            let i: usize = i as usize;
515            partial_mmr
516                .track(i, full_mmr.get(i).unwrap(), full_mmr.open(i).unwrap().merkle_path())
517                .unwrap();
518        }
519        let mut chain = PartialBlockchain::new(partial_mmr, headers).unwrap();
520        assert_eq!(chain.num_tracked_blocks(), total_blocks as usize);
521
522        chain.remove(BlockNumber::from(2));
523        assert!(!chain.contains_block(2.into()));
524        assert!(!chain.mmr().is_tracked(2));
525        assert_eq!(chain.num_tracked_blocks(), (total_blocks - 1) as usize);
526
527        assert!(chain.contains_block(3.into()));
528
529        chain.prune_to(..40.into());
530        assert_eq!(chain.num_tracked_blocks(), (total_blocks - 40) as usize);
531
532        assert_eq!(chain.block_headers().count(), (total_blocks - remove_before) as usize);
533        for block_num in remove_before..total_blocks {
534            assert!(chain.contains_block(block_num.into()));
535            assert!(chain.mmr().is_tracked(block_num as usize));
536        }
537        for block_num in 0u32..remove_before {
538            assert!(!chain.contains_block(block_num.into()));
539            assert!(!chain.mmr().is_tracked(block_num as usize));
540        }
541    }
542
543    #[test]
544    fn add_block_with_track_adds_to_blocks() {
545        let mut blockchain = PartialBlockchain::default();
546        let header = int_to_block_header(0);
547
548        blockchain.add_block(&header, true);
549
550        assert!(blockchain.contains_block(0.into()));
551        assert_eq!(blockchain.num_tracked_blocks(), 1);
552    }
553
554    #[test]
555    fn add_block_without_track_does_not_add_to_blocks() {
556        let mut blockchain = PartialBlockchain::default();
557        let header = int_to_block_header(0);
558
559        blockchain.add_block(&header, false);
560
561        assert!(!blockchain.contains_block(0.into()));
562        assert_eq!(blockchain.num_tracked_blocks(), 0);
563    }
564
565    #[test]
566    fn prune_to_removes_tracked_blocks() {
567        let mut blockchain = PartialBlockchain::default();
568        // Add 10 blocks with tracking
569        for i in 0..10u32 {
570            let header = int_to_block_header(i);
571            blockchain.add_block(&header, true);
572        }
573        assert_eq!(blockchain.num_tracked_blocks(), 10);
574
575        // Prune to keep only last 4
576        blockchain.prune_to(..6.into());
577
578        assert_eq!(blockchain.num_tracked_blocks(), 4);
579        for i in 0u32..6 {
580            assert!(!blockchain.contains_block(i.into()));
581            // Verify the underlying MMR also untracked the block
582            assert!(!blockchain.mmr().is_tracked(i as usize));
583        }
584        for i in 6u32..10 {
585            assert!(blockchain.contains_block(i.into()));
586            assert!(blockchain.mmr().is_tracked(i as usize));
587        }
588    }
589}