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