miden_protocol/transaction/
partial_blockchain.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct PartialBlockchain {
40 mmr: PartialMmr,
42 blocks: BTreeMap<BlockNumber, BlockHeader>,
45}
46
47impl PartialBlockchain {
48 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 for (block_num, block) in partial_chain.blocks.iter() {
70 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 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 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 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 pub fn mmr(&self) -> &PartialMmr {
139 &self.mmr
140 }
141
142 pub fn peaks(&self) -> MmrPeaks {
144 self.mmr.peaks()
145 }
146
147 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 pub fn num_tracked_blocks(&self) -> usize {
157 self.blocks.len()
158 }
159
160 pub fn contains_block(&self, block_num: BlockNumber) -> bool {
164 self.blocks.contains_key(&block_num)
165 }
166
167 pub fn get_block(&self, block_num: BlockNumber) -> Option<&BlockHeader> {
170 self.blocks.get(&block_num)
171 }
172
173 pub fn block_headers(&self) -> impl Iterator<Item = &BlockHeader> {
175 self.blocks.values()
176 }
177
178 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 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 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 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 #[cfg(any(feature = "testing", test))]
245 pub fn block_headers_mut(&mut self) -> &mut BTreeMap<BlockNumber, BlockHeader> {
246 &mut self.blocks
247 }
248
249 #[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#[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 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 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 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 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 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 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 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 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 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}