miden_objects/transaction/
partial_blockchain.rs1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3use core::ops::RangeTo;
4
5use crate::PartialBlockchainError;
6use crate::block::{BlockHeader, BlockNumber};
7use crate::crypto::merkle::{InnerNodeInfo, MmrPeaks, PartialMmr};
8use crate::utils::serde::{Deserializable, Serializable};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct PartialBlockchain {
37 mmr: PartialMmr,
39 blocks: BTreeMap<BlockNumber, BlockHeader>,
42}
43
44impl PartialBlockchain {
45 pub fn new(
60 mmr: PartialMmr,
61 blocks: impl IntoIterator<Item = BlockHeader>,
62 ) -> Result<Self, PartialBlockchainError> {
63 let partial_chain = Self::new_unchecked(mmr, blocks)?;
64
65 for (block_num, block) in partial_chain.blocks.iter() {
67 let proof = partial_chain
70 .mmr
71 .open(block_num.as_usize())
72 .expect("block should not exceed chain length")
73 .expect("block should be tracked in the partial MMR");
74
75 partial_chain.mmr.peaks().verify(block.commitment(), proof).map_err(|source| {
76 PartialBlockchainError::BlockHeaderCommitmentMismatch {
77 block_num: *block_num,
78 block_commitment: block.commitment(),
79 source,
80 }
81 })?;
82 }
83
84 Ok(partial_chain)
85 }
86
87 pub fn new_unchecked(
104 mmr: PartialMmr,
105 blocks: impl IntoIterator<Item = BlockHeader>,
106 ) -> Result<Self, PartialBlockchainError> {
107 let chain_length = mmr.forest().num_leaves();
108 let mut block_map = BTreeMap::new();
109 for block in blocks {
110 let block_num = block.block_num();
111 if block.block_num().as_usize() >= chain_length {
112 return Err(PartialBlockchainError::block_num_too_big(chain_length, block_num));
113 }
114
115 if !mmr.is_tracked(block_num.as_usize()) {
118 return Err(PartialBlockchainError::untracked_block(block_num));
119 }
120
121 if block_map.insert(block_num, block).is_some() {
122 return Err(PartialBlockchainError::duplicate_block(block_num));
123 }
124 }
125
126 Ok(Self { mmr, blocks: block_map })
127 }
128
129 pub fn mmr(&self) -> &PartialMmr {
134 &self.mmr
135 }
136
137 pub fn peaks(&self) -> MmrPeaks {
139 self.mmr.peaks()
140 }
141
142 pub fn chain_length(&self) -> BlockNumber {
144 BlockNumber::from(
145 u32::try_from(self.mmr.forest().num_leaves())
146 .expect("partial blockchain should never contain more than u32::MAX blocks"),
147 )
148 }
149
150 pub fn num_tracked_blocks(&self) -> usize {
152 self.blocks.len()
153 }
154
155 pub fn contains_block(&self, block_num: BlockNumber) -> bool {
159 self.blocks.contains_key(&block_num)
160 }
161
162 pub fn get_block(&self, block_num: BlockNumber) -> Option<&BlockHeader> {
165 self.blocks.get(&block_num)
166 }
167
168 pub fn block_headers(&self) -> impl Iterator<Item = &BlockHeader> {
170 self.blocks.values()
171 }
172
173 pub fn add_block(&mut self, block_header: BlockHeader, track: bool) {
186 assert_eq!(block_header.block_num(), self.chain_length());
187 self.mmr.add(block_header.commitment(), track);
188 }
189
190 pub fn prune_to(&mut self, to: RangeTo<BlockNumber>) {
196 let kept = self.blocks.split_off(&to.end);
197
198 for block_num in self.blocks.keys() {
199 self.mmr.untrack(block_num.as_usize());
200 }
201 self.blocks = kept;
202 }
203
204 pub fn remove(&mut self, block_num: BlockNumber) {
210 if self.blocks.remove(&block_num).is_some() {
211 self.mmr.untrack(block_num.as_usize());
212 }
213 }
214
215 pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
221 self.mmr.inner_nodes(
222 self.blocks
223 .values()
224 .map(|block| (block.block_num().as_usize(), block.commitment())),
225 )
226 }
227
228 #[cfg(any(feature = "testing", test))]
236 pub fn block_headers_mut(&mut self) -> &mut BTreeMap<BlockNumber, BlockHeader> {
237 &mut self.blocks
238 }
239
240 #[cfg(any(feature = "testing", test))]
244 pub fn partial_mmr_mut(&mut self) -> &mut PartialMmr {
245 &mut self.mmr
246 }
247}
248
249impl Serializable for PartialBlockchain {
250 fn write_into<W: miden_crypto::utils::ByteWriter>(&self, target: &mut W) {
251 self.mmr.write_into(target);
252 self.blocks.write_into(target);
253 }
254}
255
256impl Deserializable for PartialBlockchain {
257 fn read_from<R: miden_crypto::utils::ByteReader>(
258 source: &mut R,
259 ) -> Result<Self, miden_crypto::utils::DeserializationError> {
260 let mmr = PartialMmr::read_from(source)?;
261 let blocks = BTreeMap::<BlockNumber, BlockHeader>::read_from(source)?;
262 Ok(Self { mmr, blocks })
263 }
264}
265
266impl Default for PartialBlockchain {
267 fn default() -> Self {
268 Self::new(PartialMmr::default(), Vec::new())
269 .expect("empty partial blockchain should be valid")
270 }
271}
272
273#[cfg(test)]
277mod tests {
278 use assert_matches::assert_matches;
279 use miden_core::utils::{Deserializable, Serializable};
280
281 use super::PartialBlockchain;
282 use crate::alloc::vec::Vec;
283 use crate::block::{BlockHeader, BlockNumber, FeeParameters};
284 use crate::crypto::merkle::{Mmr, PartialMmr};
285 use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
286 use crate::{PartialBlockchainError, Word};
287
288 #[test]
289 fn test_partial_blockchain_add() {
290 let mut mmr = Mmr::default();
292 for i in 0..3 {
293 let block_header = int_to_block_header(i);
294 mmr.add(block_header.commitment());
295 }
296 let partial_mmr: PartialMmr = mmr.peaks().into();
297 let mut partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
298
299 let block_num = 3;
301 let bock_header = int_to_block_header(block_num);
302 mmr.add(bock_header.commitment());
303 partial_blockchain.add_block(bock_header, true);
304
305 assert_eq!(
306 mmr.open(block_num as usize).unwrap(),
307 partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
308 );
309
310 let block_num = 4;
312 let bock_header = int_to_block_header(block_num);
313 mmr.add(bock_header.commitment());
314 partial_blockchain.add_block(bock_header, true);
315
316 assert_eq!(
317 mmr.open(block_num as usize).unwrap(),
318 partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
319 );
320
321 let block_num = 5;
323 let bock_header = int_to_block_header(block_num);
324 mmr.add(bock_header.commitment());
325 partial_blockchain.add_block(bock_header, true);
326
327 assert_eq!(
328 mmr.open(block_num as usize).unwrap(),
329 partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
330 );
331 }
332
333 #[test]
334 fn partial_blockchain_new_on_invalid_header_fails() {
335 let block_header0 = int_to_block_header(0);
336 let block_header1 = int_to_block_header(1);
337 let block_header2 = int_to_block_header(2);
338
339 let mut mmr = Mmr::default();
340 mmr.add(block_header0.commitment());
341 mmr.add(block_header1.commitment());
342 mmr.add(block_header2.commitment());
343
344 let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
345 for i in 0..3 {
346 partial_mmr
347 .track(i, mmr.get(i).unwrap(), &mmr.open(i).unwrap().merkle_path)
348 .unwrap();
349 }
350
351 let fake_block_header2 = BlockHeader::mock(2, None, None, &[], Word::empty());
352
353 assert_ne!(block_header2.commitment(), fake_block_header2.commitment());
354
355 let error = PartialBlockchain::new(
357 partial_mmr,
358 vec![block_header0, block_header1, fake_block_header2.clone()],
359 )
360 .unwrap_err();
361
362 assert_matches!(
363 error,
364 PartialBlockchainError::BlockHeaderCommitmentMismatch {
365 block_commitment,
366 block_num,
367 ..
368 } if block_commitment == fake_block_header2.commitment() && block_num == fake_block_header2.block_num()
369 )
370 }
371
372 #[test]
373 fn partial_blockchain_new_on_block_number_exceeding_chain_length_fails() {
374 let block_header0 = int_to_block_header(0);
375 let mmr = Mmr::default();
376 let partial_mmr = PartialMmr::from_peaks(mmr.peaks());
377
378 let error = PartialBlockchain::new(partial_mmr, [block_header0]).unwrap_err();
379
380 assert_matches!(error, PartialBlockchainError::BlockNumTooBig {
381 chain_length,
382 block_num,
383 } if chain_length == 0 && block_num == BlockNumber::from(0));
384 }
385
386 #[test]
387 fn partial_blockchain_new_on_untracked_block_number_fails() {
388 let block_header0 = int_to_block_header(0);
389 let block_header1 = int_to_block_header(1);
390
391 let mut mmr = Mmr::default();
392 mmr.add(block_header0.commitment());
393 mmr.add(block_header1.commitment());
394
395 let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
396 partial_mmr
397 .track(1, block_header1.commitment(), &mmr.open(1).unwrap().merkle_path)
398 .unwrap();
399
400 let error =
401 PartialBlockchain::new(partial_mmr, [block_header0, block_header1]).unwrap_err();
402
403 assert_matches!(error, PartialBlockchainError::UntrackedBlock {
404 block_num,
405 } if block_num == BlockNumber::from(0));
406 }
407
408 #[test]
409 fn partial_blockchain_serialization() {
410 let mut mmr = Mmr::default();
412 for i in 0..3 {
413 let block_header = int_to_block_header(i);
414 mmr.add(block_header.commitment());
415 }
416 let partial_mmr: PartialMmr = mmr.peaks().into();
417 let partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
418
419 let bytes = partial_blockchain.to_bytes();
420 let deserialized = PartialBlockchain::read_from_bytes(&bytes).unwrap();
421
422 assert_eq!(partial_blockchain, deserialized);
423 }
424
425 fn int_to_block_header(block_num: impl Into<BlockNumber>) -> BlockHeader {
426 let fee_parameters =
427 FeeParameters::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500)
428 .expect("native asset ID should be a fungible faucet ID");
429
430 BlockHeader::new(
431 0,
432 Word::empty(),
433 block_num.into(),
434 Word::empty(),
435 Word::empty(),
436 Word::empty(),
437 Word::empty(),
438 Word::empty(),
439 Word::empty(),
440 Word::empty(),
441 fee_parameters,
442 0,
443 )
444 }
445
446 #[test]
447 fn prune_before_and_remove() {
448 let total_blocks = 128;
449 let remove_before = 40;
450
451 let mut full_mmr = Mmr::default();
452 let mut headers = Vec::new();
453 for i in 0..total_blocks {
454 let h = int_to_block_header(i);
455 full_mmr.add(h.commitment());
456 headers.push(h);
457 }
458 let mut partial_mmr: PartialMmr = full_mmr.peaks().into();
459 for i in 0..total_blocks {
460 let i: usize = i as usize;
461 partial_mmr
462 .track(i, full_mmr.get(i).unwrap(), &full_mmr.open(i).unwrap().merkle_path)
463 .unwrap();
464 }
465 let mut chain = PartialBlockchain::new(partial_mmr, headers).unwrap();
466 assert_eq!(chain.num_tracked_blocks(), total_blocks as usize);
467
468 chain.remove(BlockNumber::from(2));
469 assert!(!chain.contains_block(2.into()));
470 assert!(!chain.mmr().is_tracked(2));
471 assert_eq!(chain.num_tracked_blocks(), (total_blocks - 1) as usize);
472
473 assert!(chain.contains_block(3.into()));
474
475 chain.prune_to(..40.into());
476 assert_eq!(chain.num_tracked_blocks(), (total_blocks - 40) as usize);
477
478 assert_eq!(chain.block_headers().count(), (total_blocks - remove_before) as usize);
479 for block_num in remove_before..total_blocks {
480 assert!(chain.contains_block(block_num.into()));
481 assert!(chain.mmr().is_tracked(block_num as usize));
482 }
483 for block_num in 0u32..remove_before {
484 assert!(!chain.contains_block(block_num.into()));
485 assert!(!chain.mmr().is_tracked(block_num as usize));
486 }
487 }
488}