Skip to main content

miden_protocol/transaction/
partial_blockchain.rs

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