miden_protocol/transaction/
partial_blockchain.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct PartialBlockchain {
39 mmr: PartialMmr,
41 blocks: BTreeMap<BlockNumber, BlockHeader>,
44}
45
46impl PartialBlockchain {
47 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 for (block_num, block) in partial_chain.blocks.iter() {
69 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 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 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 pub fn mmr(&self) -> &PartialMmr {
136 &self.mmr
137 }
138
139 pub fn peaks(&self) -> MmrPeaks {
141 self.mmr.peaks()
142 }
143
144 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 pub fn num_tracked_blocks(&self) -> usize {
154 self.blocks.len()
155 }
156
157 pub fn contains_block(&self, block_num: BlockNumber) -> bool {
161 self.blocks.contains_key(&block_num)
162 }
163
164 pub fn get_block(&self, block_num: BlockNumber) -> Option<&BlockHeader> {
167 self.blocks.get(&block_num)
168 }
169
170 pub fn block_headers(&self) -> impl Iterator<Item = &BlockHeader> {
172 self.blocks.values()
173 }
174
175 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 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 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 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 #[cfg(any(feature = "testing", test))]
247 pub fn block_headers_mut(&mut self) -> &mut BTreeMap<BlockNumber, BlockHeader> {
248 &mut self.blocks
249 }
250
251 #[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#[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 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 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 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 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 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 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 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 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 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}