use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::ToString;
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::crypto::merkle::mmr::PartialMmr;
use miden_protocol::transaction::PartialBlockchain;
use miden_protocol::{MAX_INPUT_NOTES_PER_TX, Word};
use miden_tx::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainAnchor {
header: BlockHeader,
chain: PartialBlockchain,
}
impl ChainAnchor {
pub fn new(header: BlockHeader, chain: PartialBlockchain) -> Result<Self, ChainAnchorError> {
if chain.chain_length() != header.block_num() {
return Err(ChainAnchorError::ChainLengthMismatch {
chain_length: chain.chain_length(),
block_num: header.block_num(),
});
}
if chain.peaks().hash_peaks() != header.chain_commitment() {
return Err(ChainAnchorError::ChainCommitmentMismatch {
block_num: header.block_num(),
});
}
if chain.num_tracked_blocks() > MAX_INPUT_NOTES_PER_TX {
return Err(ChainAnchorError::TooManyTrackedBlocks {
count: chain.num_tracked_blocks(),
max: MAX_INPUT_NOTES_PER_TX,
});
}
Ok(Self { header, chain })
}
pub fn block_num(&self) -> BlockNumber {
self.header.block_num()
}
pub fn block_commitment(&self) -> Word {
self.header.commitment()
}
pub fn header(&self) -> &BlockHeader {
&self.header
}
pub fn partial_blockchain(&self) -> &PartialBlockchain {
&self.chain
}
pub fn into_parts(self) -> (BlockHeader, PartialBlockchain) {
(self.header, self.chain)
}
}
impl Serializable for ChainAnchor {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.header.write_into(target);
self.chain.write_into(target);
}
}
impl Deserializable for ChainAnchor {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let header = BlockHeader::read_from(source)?;
let mmr = PartialMmr::read_from(source)?;
let blocks = BTreeMap::<BlockNumber, BlockHeader>::read_from(source)?;
if blocks.len() > MAX_INPUT_NOTES_PER_TX {
return Err(DeserializationError::InvalidValue(
ChainAnchorError::TooManyTrackedBlocks {
count: blocks.len(),
max: MAX_INPUT_NOTES_PER_TX,
}
.to_string(),
));
}
for (block_num, header) in &blocks {
if block_num != &header.block_num() {
return Err(DeserializationError::InvalidValue(format!(
"block map key {block_num} does not match the block number {} of the header it maps to",
header.block_num()
)));
}
mmr.open(header.block_num().as_usize())
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
}
let chain = PartialBlockchain::new(mmr, blocks.into_values())
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
Self::new(header, chain).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[derive(Debug, Error)]
pub enum ChainAnchorError {
#[error(
"partial blockchain length {chain_length} does not match the anchor block number {block_num}"
)]
ChainLengthMismatch {
chain_length: BlockNumber,
block_num: BlockNumber,
},
#[error(
"partial blockchain peaks do not hash to the chain commitment of anchor block {block_num}"
)]
ChainCommitmentMismatch { block_num: BlockNumber },
#[error(
"block {block_num} is not tracked by the anchor's partial blockchain; capture the anchor with the blocks of all authenticated input notes"
)]
BlockNotTracked { block_num: BlockNumber },
#[error("the anchor tracks {count} blocks, more than the {max} a transaction can reference")]
TooManyTrackedBlocks { count: usize, max: usize },
#[error("transaction reference block {requested} does not match the anchor block {anchor}")]
ReferenceBlockMismatch {
requested: BlockNumber,
anchor: BlockNumber,
},
#[error(
"the anchored transaction expires at block {expiration}, which the chain has already reached (sync height {sync_height}); it would be rejected by the network, so re-capture the anchor closer to the tip or raise the request's expiration delta"
)]
AnchoredTransactionExpired {
expiration: BlockNumber,
sync_height: BlockNumber,
},
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::block::BlockHeader;
use miden_protocol::crypto::merkle::mmr::{Mmr, PartialMmr};
use miden_protocol::transaction::PartialBlockchain;
use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
use super::{ChainAnchor, ChainAnchorError};
fn anchor_parts(chain_length: usize, tracked: &[usize]) -> (BlockHeader, PartialBlockchain) {
let mut mmr = Mmr::default();
let mut headers = Vec::with_capacity(chain_length);
for block_num in 0..chain_length {
let header = BlockHeader::mock(
u32::try_from(block_num).unwrap(),
None,
None,
&[],
Word::empty(),
);
mmr.add(header.commitment()).unwrap();
headers.push(header);
}
let peaks = mmr.peaks();
let mut partial_mmr = PartialMmr::from_peaks(peaks.clone());
let mut tracked_headers = Vec::new();
for &pos in tracked {
partial_mmr
.track(pos, mmr.get(pos).unwrap(), mmr.open(pos).unwrap().merkle_path())
.unwrap();
tracked_headers.push(headers[pos].clone());
}
let chain = PartialBlockchain::new(partial_mmr, tracked_headers).unwrap();
let header = BlockHeader::mock(
u32::try_from(chain_length).unwrap(),
Some(peaks.hash_peaks()),
None,
&[],
Word::empty(),
);
(header, chain)
}
#[test]
fn new_accepts_a_consistent_header_and_chain() {
let (header, chain) = anchor_parts(8, &[3]);
let block_num = header.block_num();
let anchor = ChainAnchor::new(header, chain).unwrap();
assert_eq!(anchor.block_num(), block_num);
}
#[test]
fn new_rejects_a_chain_length_that_does_not_match_the_header() {
let (_, chain) = anchor_parts(8, &[3]);
let header =
BlockHeader::mock(9, Some(chain.peaks().hash_peaks()), None, &[], Word::empty());
let err = ChainAnchor::new(header, chain).unwrap_err();
assert!(matches!(err, ChainAnchorError::ChainLengthMismatch { .. }), "got {err:?}");
}
#[test]
fn new_rejects_peaks_that_do_not_hash_to_the_chain_commitment() {
let (_, chain) = anchor_parts(8, &[3]);
let header = BlockHeader::mock(8, None, None, &[], Word::empty());
let err = ChainAnchor::new(header, chain).unwrap_err();
assert!(matches!(err, ChainAnchorError::ChainCommitmentMismatch { .. }), "got {err:?}");
}
#[test]
fn serialization_round_trips() {
let (header, chain) = anchor_parts(8, &[3]);
let anchor = ChainAnchor::new(header, chain).unwrap();
let deserialized = ChainAnchor::read_from_bytes(&anchor.to_bytes()).unwrap();
assert_eq!(anchor, deserialized);
}
#[test]
fn deserialization_rejects_truncated_and_garbage_input() {
let (header, chain) = anchor_parts(8, &[3]);
let bytes = ChainAnchor::new(header, chain).unwrap().to_bytes();
assert!(ChainAnchor::read_from_bytes(&bytes[..bytes.len() - 1]).is_err());
assert!(ChainAnchor::read_from_bytes(&[0xaa; 64]).is_err());
}
#[test]
fn deserialization_rejects_a_tracked_leaf_with_a_missing_sibling() {
use alloc::collections::{BTreeMap, BTreeSet};
use miden_protocol::crypto::merkle::mmr::InOrderIndex;
let mut mmr = Mmr::default();
let mut headers = Vec::new();
for block_num in 0..4u32 {
let header = BlockHeader::mock(block_num, None, None, &[], Word::empty());
mmr.add(header.commitment()).unwrap();
headers.push(header);
}
let peaks = mmr.peaks();
let mut nodes = BTreeMap::new();
nodes.insert(InOrderIndex::from_leaf_pos(3), headers[3].commitment());
let partial_mmr =
PartialMmr::from_parts(peaks.clone(), nodes, BTreeSet::from([3])).unwrap();
let bytes = {
let mut buf = Vec::new();
let header = BlockHeader::mock(4, Some(peaks.hash_peaks()), None, &[], Word::empty());
header.write_into(&mut buf);
PartialBlockchain::new_unchecked(partial_mmr, [headers[3].clone()])
.unwrap()
.write_into(&mut buf);
buf
};
assert!(ChainAnchor::read_from_bytes(&bytes).is_err());
}
#[test]
fn deserialization_rejects_a_block_key_that_disagrees_with_its_header() {
use alloc::collections::BTreeMap;
use miden_protocol::block::BlockNumber;
let (header, chain) = anchor_parts(8, &[3]);
let tracked = chain.get_block(BlockNumber::from(3u32)).unwrap().clone();
let mut blocks = BTreeMap::new();
blocks.insert(BlockNumber::from(0u32), tracked);
let bytes = {
let mut buf = Vec::new();
header.write_into(&mut buf);
chain.mmr().write_into(&mut buf);
blocks.write_into(&mut buf);
buf
};
let err = ChainAnchor::read_from_bytes(&bytes).unwrap_err();
assert!(
matches!(&err, DeserializationError::InvalidValue(msg) if msg.contains("does not match the block number")),
"got {err:?}"
);
}
}