miden_protocol/transaction/
partial_blockchain.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct PartialBlockchain {
38 mmr: PartialMmr,
40 blocks: BTreeMap<BlockNumber, BlockHeader>,
43}
44
45impl PartialBlockchain {
46 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 for (block_num, block) in partial_chain.blocks.iter() {
68 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 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 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 pub fn mmr(&self) -> &PartialMmr {
135 &self.mmr
136 }
137
138 pub fn peaks(&self) -> MmrPeaks {
140 self.mmr.peaks()
141 }
142
143 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 pub fn num_tracked_blocks(&self) -> usize {
153 self.blocks.len()
154 }
155
156 pub fn contains_block(&self, block_num: BlockNumber) -> bool {
160 self.blocks.contains_key(&block_num)
161 }
162
163 pub fn get_block(&self, block_num: BlockNumber) -> Option<&BlockHeader> {
166 self.blocks.get(&block_num)
167 }
168
169 pub fn block_headers(&self) -> impl Iterator<Item = &BlockHeader> {
171 self.blocks.values()
172 }
173
174 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 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 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 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 #[cfg(any(feature = "testing", test))]
241 pub fn block_headers_mut(&mut self) -> &mut BTreeMap<BlockNumber, BlockHeader> {
242 &mut self.blocks
243 }
244
245 #[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#[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 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 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 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 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 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 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 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 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 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}