use alloc::vec::Vec;
use crate::commitment::Commitment;
use crate::hash::Hash;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainVerificationResult {
pub chain: Vec<Commitment>,
pub genesis: Commitment,
pub latest: Commitment,
pub length: usize,
pub contract_id: Hash,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[allow(missing_docs)]
pub enum ChainError {
#[error("Empty commitment chain")]
EmptyChain,
#[error("Chain does not start at genesis (first commitment has non-zero previous_commitment)")]
NotGenesis,
#[error(
"Commitment chain broken at index {index}: expected previous {expected}, got {actual}"
)]
BrokenChain {
index: usize,
expected: Hash,
actual: Hash,
},
#[error("Commitment at index {index} has inconsistent contract_id: expected {expected}, got {actual}")]
ContractIdMismatch {
index: usize,
expected: Hash,
actual: Hash,
},
#[error("Duplicate commitment found at index {index}")]
DuplicateCommitment { index: usize },
#[error("Cycle detected in commitment chain at index {index}")]
CycleDetected { index: usize },
}
pub fn verify_commitment_chain(
commitments: &[Commitment],
latest_commitment_hash: Hash,
) -> Result<ChainVerificationResult, ChainError> {
if commitments.is_empty() {
return Err(ChainError::EmptyChain);
}
let mut commitment_map: alloc::collections::BTreeMap<Hash, &Commitment> =
alloc::collections::BTreeMap::new();
for commitment in commitments {
let hash = commitment.hash();
commitment_map.insert(hash, commitment);
}
let mut chain: Vec<Commitment> = Vec::new();
let mut seen: alloc::collections::BTreeSet<Hash> = alloc::collections::BTreeSet::new();
let mut current_hash = latest_commitment_hash;
loop {
if seen.contains(¤t_hash) {
return Err(ChainError::CycleDetected { index: chain.len() });
}
seen.insert(current_hash);
let commitment =
commitment_map
.get(¤t_hash)
.ok_or_else(|| ChainError::BrokenChain {
index: chain.len(),
expected: current_hash,
actual: Hash::new([0u8; 32]),
})?;
if chain.iter().any(|c: &Commitment| c.hash() == current_hash) {
return Err(ChainError::DuplicateCommitment { index: chain.len() });
}
chain.push((**commitment).clone());
let previous = commitment.previous_commitment;
if previous == Hash::new([0u8; 32]) {
break;
}
current_hash = previous;
}
let genesis = chain.last().ok_or(ChainError::EmptyChain)?;
if genesis.previous_commitment != Hash::new([0u8; 32]) {
return Err(ChainError::NotGenesis);
}
let contract_id = genesis.contract_id;
for (i, commitment) in chain.iter().enumerate() {
if commitment.contract_id != contract_id {
return Err(ChainError::ContractIdMismatch {
index: i,
expected: contract_id,
actual: commitment.contract_id,
});
}
}
chain.reverse();
let genesis_commitment = chain.first().unwrap().clone();
let latest_commitment = chain.last().unwrap().clone();
let chain_length = chain.len();
Ok(ChainVerificationResult {
chain,
genesis: genesis_commitment,
latest: latest_commitment,
length: chain_length,
contract_id,
})
}
pub fn verify_ordered_commitment_chain(
ordered_commitments: &[Commitment],
) -> Result<ChainVerificationResult, ChainError> {
if ordered_commitments.is_empty() {
return Err(ChainError::EmptyChain);
}
for i in 1..ordered_commitments.len() {
let current = &ordered_commitments[i];
let previous = &ordered_commitments[i - 1];
let expected_previous = previous.hash();
if current.previous_commitment != expected_previous {
return Err(ChainError::BrokenChain {
index: i,
expected: expected_previous,
actual: current.previous_commitment,
});
}
if current.contract_id != previous.contract_id {
return Err(ChainError::ContractIdMismatch {
index: i,
expected: previous.contract_id,
actual: current.contract_id,
});
}
}
if ordered_commitments.first().unwrap().previous_commitment != Hash::new([0u8; 32]) {
return Err(ChainError::NotGenesis);
}
let mut seen = alloc::collections::BTreeSet::new();
for (i, commitment) in ordered_commitments.iter().enumerate() {
let hash = commitment.hash();
if !seen.insert(hash) {
return Err(ChainError::DuplicateCommitment { index: i });
}
}
let genesis = ordered_commitments.first().unwrap().clone();
let latest = ordered_commitments.last().unwrap().clone();
let contract_id = genesis.contract_id;
Ok(ChainVerificationResult {
chain: ordered_commitments.to_vec(),
genesis,
latest,
length: ordered_commitments.len(),
contract_id,
})
}
pub fn verify_commitment_link(
previous_commitment: &Commitment,
current_commitment: &Commitment,
) -> Result<(), ChainError> {
let expected = previous_commitment.hash();
let actual = current_commitment.previous_commitment;
if expected != actual {
return Err(ChainError::BrokenChain {
index: 0,
expected,
actual,
});
}
if previous_commitment.contract_id != current_commitment.contract_id {
return Err(ChainError::ContractIdMismatch {
index: 0,
expected: previous_commitment.contract_id,
actual: current_commitment.contract_id,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commitment::Commitment;
use crate::seal::SealRef;
fn make_genesis_commitment(contract_id: Hash) -> Commitment {
let domain = [0u8; 32];
let seal = SealRef::new(vec![0x01], None).unwrap();
Commitment::simple(
contract_id,
Hash::new([0u8; 32]), Hash::new([0u8; 32]),
&seal,
domain,
)
}
fn make_commitment(contract_id: Hash, previous_commitment: Hash, seal_id: u8) -> Commitment {
let domain = [0u8; 32];
let seal = SealRef::new(vec![seal_id], None).unwrap();
Commitment::simple(
contract_id,
previous_commitment,
Hash::new([0u8; 32]),
&seal,
domain,
)
}
#[test]
fn test_verify_ordered_chain_valid() {
let contract_id = Hash::new([0xAB; 32]);
let genesis = make_genesis_commitment(contract_id);
let c1 = make_commitment(contract_id, genesis.hash(), 0x02);
let c2 = make_commitment(contract_id, c1.hash(), 0x03);
let result = verify_ordered_commitment_chain(&[genesis.clone(), c1.clone(), c2.clone()]);
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.length, 3);
assert_eq!(result.genesis.hash(), genesis.hash());
assert_eq!(result.latest.hash(), c2.hash());
assert_eq!(result.contract_id, contract_id);
}
#[test]
fn test_verify_ordered_chain_broken_link() {
let contract_id = Hash::new([0xAB; 32]);
let genesis = make_genesis_commitment(contract_id);
let c1 = make_commitment(contract_id, genesis.hash(), 0x02);
let c2 = make_commitment(contract_id, Hash::new([0xFF; 32]), 0x03);
let result = verify_ordered_commitment_chain(&[genesis, c1, c2]);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ChainError::BrokenChain { .. }
));
}
#[test]
fn test_verify_ordered_chain_not_genesis() {
let contract_id = Hash::new([0xAB; 32]);
let c1 = make_commitment(contract_id, Hash::new([0x01; 32]), 0x01);
let c2 = make_commitment(contract_id, c1.hash(), 0x02);
let result = verify_ordered_commitment_chain(&[c1, c2]);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ChainError::NotGenesis));
}
#[test]
fn test_verify_ordered_chain_contract_id_mismatch() {
let contract_id_1 = Hash::new([0xAB; 32]);
let contract_id_2 = Hash::new([0xCD; 32]);
let genesis = make_genesis_commitment(contract_id_1);
let c1 = make_commitment(contract_id_2, genesis.hash(), 0x02);
let result = verify_ordered_commitment_chain(&[genesis, c1]);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ChainError::ContractIdMismatch { .. }
));
}
#[test]
fn test_verify_ordered_chain_empty() {
let result = verify_ordered_commitment_chain(&[]);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ChainError::EmptyChain));
}
#[test]
fn test_verify_ordered_chain_single_genesis() {
let contract_id = Hash::new([0xAB; 32]);
let genesis = make_genesis_commitment(contract_id);
let result = verify_ordered_commitment_chain(&[genesis.clone()]);
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.length, 1);
assert_eq!(result.genesis.hash(), genesis.hash());
assert_eq!(result.latest.hash(), genesis.hash());
}
#[test]
fn test_verify_ordered_chain_duplicates() {
let contract_id = Hash::new([0xAB; 32]);
let genesis = make_genesis_commitment(contract_id);
let c1 = make_commitment(contract_id, genesis.hash(), 0x02);
let result = verify_ordered_commitment_chain(&[genesis.clone(), c1.clone(), c1.clone()]);
assert!(result.is_err()); }
#[test]
fn test_verify_commitment_link_valid() {
let contract_id = Hash::new([0xAB; 32]);
let genesis = make_genesis_commitment(contract_id);
let c1 = make_commitment(contract_id, genesis.hash(), 0x02);
assert!(verify_commitment_link(&genesis, &c1).is_ok());
}
#[test]
fn test_verify_commitment_link_broken() {
let contract_id = Hash::new([0xAB; 32]);
let genesis = make_genesis_commitment(contract_id);
let c1 = make_commitment(contract_id, Hash::new([0xFF; 32]), 0x02);
assert!(verify_commitment_link(&genesis, &c1).is_err());
}
#[test]
fn test_long_chain_verification() {
let contract_id = Hash::new([0xAB; 32]);
let mut commitments = Vec::new();
let genesis = make_genesis_commitment(contract_id);
commitments.push(genesis.clone());
let mut previous = genesis;
for i in 1..50 {
let c = make_commitment(contract_id, previous.hash(), (i + 1) as u8);
commitments.push(c.clone());
previous = c;
}
let result = verify_ordered_commitment_chain(&commitments);
assert!(result.is_ok());
assert_eq!(result.unwrap().length, 50);
}
}