use alloc::string::ToString;
use alloc::vec::Vec;
use miden_objects::DecodeMessageExt;
use miden_protocol::block::{BlockHeader, BlockNumber, BlockSignatures};
use miden_protocol::crypto::merkle::mmr::MmrDelta;
use miden_protocol::protocol_config::ProtocolConfig;
use crate::rpc::domain::MissingFieldHelper;
use crate::rpc::errors::RpcConversionError;
use crate::rpc::{RpcError, generated as proto};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncTarget {
CommittedChainTip,
ProvenChainTip,
}
impl From<SyncTarget> for proto::rpc::FinalityLevel {
fn from(target: SyncTarget) -> Self {
match target {
SyncTarget::CommittedChainTip => Self::Committed,
SyncTarget::ProvenChainTip => Self::Proven,
}
}
}
pub struct ChainMmrInfo {
pub block_from: BlockNumber,
pub block_to: BlockNumber,
pub mmr_delta: MmrDelta,
pub block_header: BlockHeader,
pub protocol_config: Option<ProtocolConfig>,
pub block_signatures: BlockSignatures,
}
impl TryFrom<proto::rpc::SyncChainMmrResponse> for ChainMmrInfo {
type Error = RpcError;
fn try_from(value: proto::rpc::SyncChainMmrResponse) -> Result<Self, Self::Error> {
let block_range = value
.block_range
.ok_or(proto::rpc::SyncChainMmrResponse::missing_field(stringify!(block_range)))?;
let mmr_delta: MmrDelta = value
.mmr_delta
.ok_or(proto::rpc::SyncChainMmrResponse::missing_field(stringify!(mmr_delta)))?
.decode_and_verify()?;
let block_header: BlockHeader = value
.block_header
.ok_or(proto::rpc::SyncChainMmrResponse::missing_field(stringify!(block_header)))?
.decode_and_build_unchecked()?;
let protocol_config: Option<ProtocolConfig> =
value.protocol_config.map(DecodeMessageExt::decode_and_verify).transpose()?;
if let Some(config) = &protocol_config {
let returned = config.to_commitment();
let expected = block_header.protocol_config_commitment();
if returned != expected {
return Err(RpcError::InvalidResponse(format!(
"node returned a protocol configuration with commitment {returned} for a block header that commits to {expected}"
)));
}
}
let signatures = value
.block_signatures
.into_iter()
.map(DecodeMessageExt::decode_and_verify)
.collect::<Result<Vec<_>, _>>()?;
let block_signatures = BlockSignatures::new(signatures)
.map_err(|err| RpcConversionError::InvalidField(err.to_string()))?;
Ok(Self {
block_from: block_range.block_from.into(),
block_to: block_range.block_to.into(),
mmr_delta,
block_header,
protocol_config,
block_signatures,
})
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::asset::AssetId;
use miden_protocol::block::{BlockHeader, FeeParameters};
use miden_protocol::crypto::merkle::mmr::Forest;
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
};
use super::*;
fn protocol_config_for(faucet: u128) -> ProtocolConfig {
ProtocolConfig::current(AssetId::new_fungible(faucet.try_into().unwrap())).unwrap()
}
fn response(
committed: &ProtocolConfig,
returned: Option<&ProtocolConfig>,
) -> proto::rpc::SyncChainMmrResponse {
let mock = BlockHeader::mock(0, None, None, &[]);
let header = BlockHeader::new(
Word::empty(),
0.into(),
Word::empty(),
Word::empty(),
Word::empty(),
Word::empty(),
Word::empty(),
mock.validator_config().clone(),
FeeParameters::new(0),
committed.to_commitment(),
None,
0,
);
proto::rpc::SyncChainMmrResponse {
block_range: Some(proto::rpc::BlockRange { block_from: 0, block_to: 0 }),
mmr_delta: Some(
MmrDelta {
forest: Forest::empty(),
data: Vec::new(),
}
.into(),
),
block_header: Some((&header).into()),
block_signatures: Vec::new(),
protocol_config: returned.map(Into::into),
}
}
#[test]
fn a_protocol_config_must_match_the_header_it_arrives_with() {
let committed = protocol_config_for(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1);
let other = protocol_config_for(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2);
assert_ne!(committed.to_commitment(), other.to_commitment());
let matching = ChainMmrInfo::try_from(response(&committed, Some(&committed))).unwrap();
assert_eq!(matching.protocol_config.unwrap(), committed);
let absent = ChainMmrInfo::try_from(response(&committed, None)).unwrap();
assert!(absent.protocol_config.is_none());
let Err(RpcError::InvalidResponse(message)) =
ChainMmrInfo::try_from(response(&committed, Some(&other)))
else {
panic!("a mismatched configuration must be rejected as an invalid response");
};
assert!(message.contains(&other.to_commitment().to_string()), "{message}");
assert!(message.contains(&committed.to_commitment().to_string()), "{message}");
}
}