Skip to main content

miden_protocol/transaction/
partial_blockchain.rs

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