use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::ops::RangeTo;
use crate::PartialBlockchainError;
use crate::block::{BlockHeader, BlockNumber};
use crate::crypto::merkle::{InnerNodeInfo, MmrPeaks, PartialMmr};
use crate::utils::serde::{Deserializable, Serializable};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartialBlockchain {
mmr: PartialMmr,
blocks: BTreeMap<BlockNumber, BlockHeader>,
}
impl PartialBlockchain {
pub fn new(
mmr: PartialMmr,
blocks: impl IntoIterator<Item = BlockHeader>,
) -> Result<Self, PartialBlockchainError> {
let partial_chain = Self::new_unchecked(mmr, blocks)?;
for (block_num, block) in partial_chain.blocks.iter() {
let proof = partial_chain
.mmr
.open(block_num.as_usize())
.expect("block should not exceed chain length")
.expect("block should be tracked in the partial MMR");
partial_chain.mmr.peaks().verify(block.commitment(), proof).map_err(|source| {
PartialBlockchainError::BlockHeaderCommitmentMismatch {
block_num: *block_num,
block_commitment: block.commitment(),
source,
}
})?;
}
Ok(partial_chain)
}
pub fn new_unchecked(
mmr: PartialMmr,
blocks: impl IntoIterator<Item = BlockHeader>,
) -> Result<Self, PartialBlockchainError> {
let chain_length = mmr.forest().num_leaves();
let mut block_map = BTreeMap::new();
for block in blocks {
let block_num = block.block_num();
if block.block_num().as_usize() >= chain_length {
return Err(PartialBlockchainError::block_num_too_big(chain_length, block_num));
}
if !mmr.is_tracked(block_num.as_usize()) {
return Err(PartialBlockchainError::untracked_block(block_num));
}
if block_map.insert(block_num, block).is_some() {
return Err(PartialBlockchainError::duplicate_block(block_num));
}
}
Ok(Self { mmr, blocks: block_map })
}
pub fn mmr(&self) -> &PartialMmr {
&self.mmr
}
pub fn peaks(&self) -> MmrPeaks {
self.mmr.peaks()
}
pub fn chain_length(&self) -> BlockNumber {
BlockNumber::from(
u32::try_from(self.mmr.forest().num_leaves())
.expect("partial blockchain should never contain more than u32::MAX blocks"),
)
}
pub fn num_tracked_blocks(&self) -> usize {
self.blocks.len()
}
pub fn contains_block(&self, block_num: BlockNumber) -> bool {
self.blocks.contains_key(&block_num)
}
pub fn get_block(&self, block_num: BlockNumber) -> Option<&BlockHeader> {
self.blocks.get(&block_num)
}
pub fn block_headers(&self) -> impl Iterator<Item = &BlockHeader> {
self.blocks.values()
}
pub fn add_block(&mut self, block_header: BlockHeader, track: bool) {
assert_eq!(block_header.block_num(), self.chain_length());
self.mmr.add(block_header.commitment(), track);
}
pub fn prune_to(&mut self, to: RangeTo<BlockNumber>) {
let kept = self.blocks.split_off(&to.end);
for block_num in self.blocks.keys() {
self.mmr.untrack(block_num.as_usize());
}
self.blocks = kept;
}
pub fn remove(&mut self, block_num: BlockNumber) {
if self.blocks.remove(&block_num).is_some() {
self.mmr.untrack(block_num.as_usize());
}
}
pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
self.mmr.inner_nodes(
self.blocks
.values()
.map(|block| (block.block_num().as_usize(), block.commitment())),
)
}
#[cfg(any(feature = "testing", test))]
pub fn block_headers_mut(&mut self) -> &mut BTreeMap<BlockNumber, BlockHeader> {
&mut self.blocks
}
#[cfg(any(feature = "testing", test))]
pub fn partial_mmr_mut(&mut self) -> &mut PartialMmr {
&mut self.mmr
}
}
impl Serializable for PartialBlockchain {
fn write_into<W: miden_crypto::utils::ByteWriter>(&self, target: &mut W) {
self.mmr.write_into(target);
self.blocks.write_into(target);
}
}
impl Deserializable for PartialBlockchain {
fn read_from<R: miden_crypto::utils::ByteReader>(
source: &mut R,
) -> Result<Self, miden_crypto::utils::DeserializationError> {
let mmr = PartialMmr::read_from(source)?;
let blocks = BTreeMap::<BlockNumber, BlockHeader>::read_from(source)?;
Ok(Self { mmr, blocks })
}
}
impl Default for PartialBlockchain {
fn default() -> Self {
Self::new(PartialMmr::default(), Vec::new())
.expect("empty partial blockchain should be valid")
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_core::utils::{Deserializable, Serializable};
use super::PartialBlockchain;
use crate::alloc::vec::Vec;
use crate::block::{BlockHeader, BlockNumber, FeeParameters};
use crate::crypto::merkle::{Mmr, PartialMmr};
use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
use crate::{PartialBlockchainError, Word};
#[test]
fn test_partial_blockchain_add() {
let mut mmr = Mmr::default();
for i in 0..3 {
let block_header = int_to_block_header(i);
mmr.add(block_header.commitment());
}
let partial_mmr: PartialMmr = mmr.peaks().into();
let mut partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
let block_num = 3;
let bock_header = int_to_block_header(block_num);
mmr.add(bock_header.commitment());
partial_blockchain.add_block(bock_header, true);
assert_eq!(
mmr.open(block_num as usize).unwrap(),
partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
);
let block_num = 4;
let bock_header = int_to_block_header(block_num);
mmr.add(bock_header.commitment());
partial_blockchain.add_block(bock_header, true);
assert_eq!(
mmr.open(block_num as usize).unwrap(),
partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
);
let block_num = 5;
let bock_header = int_to_block_header(block_num);
mmr.add(bock_header.commitment());
partial_blockchain.add_block(bock_header, true);
assert_eq!(
mmr.open(block_num as usize).unwrap(),
partial_blockchain.mmr.open(block_num as usize).unwrap().unwrap()
);
}
#[test]
fn partial_blockchain_new_on_invalid_header_fails() {
let block_header0 = int_to_block_header(0);
let block_header1 = int_to_block_header(1);
let block_header2 = int_to_block_header(2);
let mut mmr = Mmr::default();
mmr.add(block_header0.commitment());
mmr.add(block_header1.commitment());
mmr.add(block_header2.commitment());
let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
for i in 0..3 {
partial_mmr
.track(i, mmr.get(i).unwrap(), &mmr.open(i).unwrap().merkle_path)
.unwrap();
}
let fake_block_header2 = BlockHeader::mock(2, None, None, &[], Word::empty());
assert_ne!(block_header2.commitment(), fake_block_header2.commitment());
let error = PartialBlockchain::new(
partial_mmr,
vec![block_header0, block_header1, fake_block_header2.clone()],
)
.unwrap_err();
assert_matches!(
error,
PartialBlockchainError::BlockHeaderCommitmentMismatch {
block_commitment,
block_num,
..
} if block_commitment == fake_block_header2.commitment() && block_num == fake_block_header2.block_num()
)
}
#[test]
fn partial_blockchain_new_on_block_number_exceeding_chain_length_fails() {
let block_header0 = int_to_block_header(0);
let mmr = Mmr::default();
let partial_mmr = PartialMmr::from_peaks(mmr.peaks());
let error = PartialBlockchain::new(partial_mmr, [block_header0]).unwrap_err();
assert_matches!(error, PartialBlockchainError::BlockNumTooBig {
chain_length,
block_num,
} if chain_length == 0 && block_num == BlockNumber::from(0));
}
#[test]
fn partial_blockchain_new_on_untracked_block_number_fails() {
let block_header0 = int_to_block_header(0);
let block_header1 = int_to_block_header(1);
let mut mmr = Mmr::default();
mmr.add(block_header0.commitment());
mmr.add(block_header1.commitment());
let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks());
partial_mmr
.track(1, block_header1.commitment(), &mmr.open(1).unwrap().merkle_path)
.unwrap();
let error =
PartialBlockchain::new(partial_mmr, [block_header0, block_header1]).unwrap_err();
assert_matches!(error, PartialBlockchainError::UntrackedBlock {
block_num,
} if block_num == BlockNumber::from(0));
}
#[test]
fn partial_blockchain_serialization() {
let mut mmr = Mmr::default();
for i in 0..3 {
let block_header = int_to_block_header(i);
mmr.add(block_header.commitment());
}
let partial_mmr: PartialMmr = mmr.peaks().into();
let partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
let bytes = partial_blockchain.to_bytes();
let deserialized = PartialBlockchain::read_from_bytes(&bytes).unwrap();
assert_eq!(partial_blockchain, deserialized);
}
fn int_to_block_header(block_num: impl Into<BlockNumber>) -> BlockHeader {
let fee_parameters =
FeeParameters::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500)
.expect("native asset ID should be a fungible faucet ID");
BlockHeader::new(
0,
Word::empty(),
block_num.into(),
Word::empty(),
Word::empty(),
Word::empty(),
Word::empty(),
Word::empty(),
Word::empty(),
Word::empty(),
fee_parameters,
0,
)
}
#[test]
fn prune_before_and_remove() {
let total_blocks = 128;
let remove_before = 40;
let mut full_mmr = Mmr::default();
let mut headers = Vec::new();
for i in 0..total_blocks {
let h = int_to_block_header(i);
full_mmr.add(h.commitment());
headers.push(h);
}
let mut partial_mmr: PartialMmr = full_mmr.peaks().into();
for i in 0..total_blocks {
let i: usize = i as usize;
partial_mmr
.track(i, full_mmr.get(i).unwrap(), &full_mmr.open(i).unwrap().merkle_path)
.unwrap();
}
let mut chain = PartialBlockchain::new(partial_mmr, headers).unwrap();
assert_eq!(chain.num_tracked_blocks(), total_blocks as usize);
chain.remove(BlockNumber::from(2));
assert!(!chain.contains_block(2.into()));
assert!(!chain.mmr().is_tracked(2));
assert_eq!(chain.num_tracked_blocks(), (total_blocks - 1) as usize);
assert!(chain.contains_block(3.into()));
chain.prune_to(..40.into());
assert_eq!(chain.num_tracked_blocks(), (total_blocks - 40) as usize);
assert_eq!(chain.block_headers().count(), (total_blocks - remove_before) as usize);
for block_num in remove_before..total_blocks {
assert!(chain.contains_block(block_num.into()));
assert!(chain.mmr().is_tracked(block_num as usize));
}
for block_num in 0u32..remove_before {
assert!(!chain.contains_block(block_num.into()));
assert!(!chain.mmr().is_tracked(block_num as usize));
}
}
}