use alloc::string::String;
use alloc::vec::Vec;
use crate::commitment::Commitment;
use crate::commitment_chain::{
verify_ordered_commitment_chain, ChainError, ChainVerificationResult,
};
use crate::consignment::Consignment;
use crate::cross_chain::InclusionProof as CrossChainInclusionProof;
use crate::hash::Hash;
use crate::right::{Right, RightError, RightId};
use crate::seal::SealRef;
use crate::seal_registry::{ChainId, CrossChainSealRegistry, SealConsumption, SealStatus};
use crate::state_store::{
ContractHistory, InMemoryStateStore, StateHistoryStore, StateTransitionRecord,
};
#[derive(Debug)]
pub enum ValidationResult {
Accepted {
history: ContractHistory,
rights_count: usize,
seals_consumed: usize,
},
Rejected {
reason: ValidationError,
},
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum ValidationError {
#[error("Empty consignment")]
EmptyConsignment,
#[error("Commitment chain verification failed: {0}")]
CommitmentChainError(#[from] ChainError),
#[error("Right validation failed: {0}")]
RightValidationError(#[from] RightError),
#[error("Double-spend detected")]
DoubleSpend(String),
#[error("Missing history: contract has incomplete state history")]
MissingHistory(String),
#[error("Seal assignment error: {0}")]
SealAssignmentError(String),
#[error("State store error: {0}")]
StoreError(String),
#[error("Contract ID mismatch: expected {expected}, got {actual}")]
ContractIdMismatch { expected: Hash, actual: Hash },
#[error("Unsupported consignment version: {version}")]
UnsupportedVersion { version: u32 },
#[error("Inclusion proof verification failed: {0}")]
InclusionProofFailed(String),
}
#[derive(Clone, Debug)]
pub struct SealConsumptionEvent {
pub chain: ChainId,
pub seal: SealRef,
pub right: Right,
pub inclusion: CrossChainInclusionProof,
pub height: u64,
pub tx_hash: Hash,
}
pub struct ValidationClient {
store: InMemoryStateStore,
seal_registry: CrossChainSealRegistry,
}
impl ValidationClient {
pub fn new() -> Self {
Self {
store: InMemoryStateStore::new(),
seal_registry: CrossChainSealRegistry::new(),
}
}
pub fn receive_consignment(
&mut self,
consignment: &Consignment,
anchor_chain: ChainId,
) -> ValidationResult {
if let Err(e) = consignment.validate_structure() {
return ValidationResult::Rejected {
reason: ValidationError::SealAssignmentError(e.to_string()),
};
}
let commitments = self.extract_commitments(consignment);
let chain_result = match self.verify_commitment_chain(&commitments) {
Ok(result) => result,
Err(e) => {
return ValidationResult::Rejected {
reason: ValidationError::CommitmentChainError(e),
}
}
};
let seals_consumed =
match self.verify_seal_consumption(consignment, &chain_result, &anchor_chain) {
Ok(count) => count,
Err(e) => return ValidationResult::Rejected { reason: e },
};
if let Err(e) = self.update_local_state(consignment, &chain_result) {
return ValidationResult::Rejected { reason: e };
}
ValidationResult::Accepted {
history: ContractHistory::from_genesis(chain_result.genesis.clone()),
rights_count: consignment.seal_assignments.len(),
seals_consumed,
}
}
pub fn verify_seal_consumption_event(
&mut self,
event: SealConsumptionEvent,
) -> Result<(), ValidationError> {
event
.right
.verify()
.map_err(ValidationError::RightValidationError)?;
match self.seal_registry.check_seal_status(&event.seal) {
SealStatus::Unconsumed => {
}
SealStatus::ConsumedOnChain { chain, .. } => {
return Err(ValidationError::DoubleSpend(format!(
"Seal already consumed on {:?}",
chain
)));
}
SealStatus::DoubleSpent { .. } => {
return Err(ValidationError::DoubleSpend(
"Seal has been double-spent across chains".to_string(),
));
}
}
self.verify_inclusion_proof(&event.inclusion, &event.chain)?;
let consumption = SealConsumption {
chain: event.chain.clone(),
seal_ref: event.seal.clone(),
right_id: event.right.id.clone(),
block_height: event.height,
tx_hash: event.tx_hash,
recorded_at: 0, };
if let Err(e) = self.seal_registry.record_consumption(consumption) {
return Err(ValidationError::DoubleSpend(format!("{:?}", e)));
}
Ok(())
}
fn extract_commitments(&self, consignment: &Consignment) -> Vec<Commitment> {
let mut commitments = Vec::new();
let genesis_commitment = {
let domain = [0u8; 32];
let seal = SealRef::new(consignment.genesis.contract_id.as_bytes().to_vec(), None)
.unwrap_or_else(|_| SealRef::new(vec![0x01], None).unwrap());
Commitment::simple(
consignment.genesis.contract_id,
Hash::new([0u8; 32]), Hash::new([0u8; 32]),
&seal,
domain,
)
};
commitments.push(genesis_commitment);
for (i, assignment) in consignment.seal_assignments.iter().enumerate() {
let previous = if i == 0 {
commitments[0].hash()
} else {
commitments[i].hash()
};
let domain = [0u8; 32];
let seal = assignment.seal_ref.clone();
let commitment = Commitment::simple(
consignment.schema_id,
previous,
Hash::new([0u8; 32]), &seal,
domain,
);
commitments.push(commitment);
}
commitments
}
fn verify_commitment_chain(
&self,
commitments: &[Commitment],
) -> Result<ChainVerificationResult, ChainError> {
if commitments.is_empty() {
return Err(ChainError::EmptyChain);
}
verify_ordered_commitment_chain(commitments)
}
fn verify_seal_consumption(
&mut self,
consignment: &Consignment,
_chain_result: &ChainVerificationResult,
anchor_chain: &ChainId,
) -> Result<usize, ValidationError> {
let mut seals_consumed = 0;
for seal_assignment in &consignment.seal_assignments {
match self
.seal_registry
.check_seal_status(&seal_assignment.seal_ref)
{
SealStatus::Unconsumed => {
let right_id_bytes: [u8; 32] = {
let mut arr = [0u8; 32];
let seal_bytes = seal_assignment.seal_ref.to_vec();
let len = seal_bytes.len().min(32);
arr[..len].copy_from_slice(&seal_bytes[..len]);
arr
};
let consumption = SealConsumption {
chain: anchor_chain.clone(),
seal_ref: seal_assignment.seal_ref.clone(),
right_id: RightId(Hash::new(right_id_bytes)),
block_height: 0, tx_hash: Hash::new([0u8; 32]), recorded_at: 0,
};
if let Err(e) = self.seal_registry.record_consumption(consumption) {
return Err(ValidationError::DoubleSpend(format!("{:?}", e)));
}
seals_consumed += 1;
}
SealStatus::ConsumedOnChain { chain, .. } => {
return Err(ValidationError::DoubleSpend(format!(
"Seal already consumed on {:?}",
chain
)));
}
SealStatus::DoubleSpent { .. } => {
return Err(ValidationError::DoubleSpend(
"Seal has been double-spent".to_string(),
));
}
}
}
Ok(seals_consumed)
}
fn verify_inclusion_proof(
&self,
inclusion: &CrossChainInclusionProof,
chain: &ChainId,
) -> Result<(), ValidationError> {
match (inclusion, chain) {
(CrossChainInclusionProof::Bitcoin(proof), _) => {
if proof.merkle_branch.is_empty() {
return Err(ValidationError::InclusionProofFailed(
"Empty Merkle branch".to_string(),
));
}
if proof.block_header.is_empty() {
return Err(ValidationError::InclusionProofFailed(
"Empty block header".to_string(),
));
}
}
(CrossChainInclusionProof::Ethereum(proof), _) => {
if proof.receipt_rlp.is_empty() && proof.merkle_nodes.is_empty() {
return Err(ValidationError::InclusionProofFailed(
"Empty MPT proof".to_string(),
));
}
}
(CrossChainInclusionProof::Sui(proof), _) => {
if !proof.certified {
return Err(ValidationError::InclusionProofFailed(
"Checkpoint not certified".to_string(),
));
}
}
(CrossChainInclusionProof::Aptos(proof), _) => {
if !proof.success {
return Err(ValidationError::InclusionProofFailed(
"Transaction failed".to_string(),
));
}
}
}
Ok(())
}
fn update_local_state(
&mut self,
consignment: &Consignment,
chain_result: &ChainVerificationResult,
) -> Result<(), ValidationError> {
let contract_id = chain_result.contract_id;
let mut history = match self.store.load_contract_history(contract_id) {
Ok(Some(h)) => h,
Ok(None) => ContractHistory::from_genesis(chain_result.genesis.clone()),
Err(e) => return Err(ValidationError::StoreError(e.to_string())),
};
for (i, _transition) in consignment.transitions.iter().enumerate() {
let previous_hash = if i == 0 {
chain_result.genesis.hash()
} else if i <= history.transition_count() {
history.transitions[i - 1].commitment.hash()
} else {
chain_result.latest.hash()
};
let seal = if i < consignment.seal_assignments.len() {
consignment.seal_assignments[i].seal_ref.clone()
} else {
SealRef::new(vec![i as u8], None).unwrap()
};
let domain = [0u8; 32];
let commitment = Commitment::simple(
contract_id,
previous_hash,
Hash::new([0u8; 32]),
&seal,
domain,
);
let record = StateTransitionRecord {
commitment,
seal_ref: seal,
rights: Vec::new(),
block_height: 0,
verified: true,
};
history
.add_transition(record)
.map_err(|e| ValidationError::StoreError(e.to_string()))?;
}
if let Err(e) = self.store.save_contract_history(contract_id, &history) {
return Err(ValidationError::StoreError(e.to_string()));
}
Ok(())
}
pub fn store(&self) -> &InMemoryStateStore {
&self.store
}
pub fn seal_registry(&self) -> &CrossChainSealRegistry {
&self.seal_registry
}
}
impl Default for ValidationClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::consignment::Consignment;
use crate::genesis::Genesis;
use crate::OwnershipProof;
fn make_test_genesis() -> Genesis {
Genesis::new(
Hash::new([0xAB; 32]),
Hash::new([0x01; 32]),
vec![],
vec![],
vec![],
)
}
fn make_test_consignment() -> Consignment {
let genesis = make_test_genesis();
Consignment::new(genesis, vec![], vec![], vec![], Hash::new([0x01; 32]))
}
#[test]
fn test_client_creation() {
let client = ValidationClient::new();
assert_eq!(client.store().list_contracts().unwrap().len(), 0);
assert_eq!(client.seal_registry().total_seals(), 0);
}
#[test]
fn test_receive_consignment_empty() {
let mut client = ValidationClient::new();
let consignment = make_test_consignment();
let result = client.receive_consignment(&consignment, ChainId::Bitcoin);
match result {
ValidationResult::Accepted {
rights_count,
seals_consumed,
..
} => {
assert_eq!(rights_count, 0);
assert_eq!(seals_consumed, 0);
}
ValidationResult::Rejected { reason } => {
let _ = reason;
}
}
}
#[test]
fn test_receive_multiple_consignments() {
let mut client = ValidationClient::new();
for i in 0..3 {
let mut genesis = make_test_genesis();
genesis.contract_id = Hash::new([i + 1; 32]);
let consignment =
Consignment::new(genesis, vec![], vec![], vec![], Hash::new([0x01; 32]));
let _ = client.receive_consignment(&consignment, ChainId::Bitcoin);
}
assert_eq!(client.store().list_contracts().unwrap().len(), 3);
}
#[test]
fn test_seal_consumption_event_btc() {
let mut client = ValidationClient::new();
let right = Right::new(
Hash::new([0xCD; 32]),
OwnershipProof {
proof: vec![0x01, 0x02, 0x03],
owner: vec![0xFF; 32],
scheme: None,
},
&[0x42],
);
let inclusion = CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
txid: [0xAB; 32],
merkle_branch: vec![[0xCD; 32], [0xEF; 32]],
block_header: vec![0x01; 80],
block_height: 1000,
confirmations: 6,
});
let event = SealConsumptionEvent {
chain: ChainId::Bitcoin,
seal: SealRef::new(vec![0x01], None).unwrap(),
right,
inclusion,
height: 1000,
tx_hash: Hash::new([0xAB; 32]),
};
let result = client.verify_seal_consumption_event(event);
assert!(result.is_ok());
assert_eq!(client.seal_registry().total_seals(), 1);
}
#[test]
fn test_seal_consumption_event_double_spend() {
let mut client = ValidationClient::new();
let right = Right::new(
Hash::new([0xCD; 32]),
OwnershipProof {
proof: vec![0x01],
owner: vec![0xFF; 32],
scheme: None,
},
&[0x42],
);
let inclusion = CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
txid: [0xAB; 32],
merkle_branch: vec![[0xCD; 32]],
block_header: vec![0x01; 80],
block_height: 1000,
confirmations: 6,
});
let seal = SealRef::new(vec![0x01], None).unwrap();
let event1 = SealConsumptionEvent {
chain: ChainId::Bitcoin,
seal: seal.clone(),
right: right.clone(),
inclusion: inclusion.clone(),
height: 1000,
tx_hash: Hash::new([0xAB; 32]),
};
assert!(client.verify_seal_consumption_event(event1).is_ok());
let right2 = Right::new(
Hash::new([0xEF; 32]),
OwnershipProof {
proof: vec![0x02],
owner: vec![0xEE; 32],
scheme: None,
},
&[0x99],
);
let event2 = SealConsumptionEvent {
chain: ChainId::Bitcoin,
seal: seal.clone(),
right: right2,
inclusion,
height: 1001,
tx_hash: Hash::new([0xBC; 32]),
};
let result = client.verify_seal_consumption_event(event2);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ValidationError::DoubleSpend(_)
));
}
#[test]
fn test_seal_consumption_cross_chain() {
let mut client = ValidationClient::new();
let right = Right::new(
Hash::new([0xCD; 32]),
OwnershipProof {
proof: vec![0x01],
owner: vec![0xFF; 32],
scheme: None,
},
&[0x42],
);
let btc_inclusion =
CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
txid: [0xAB; 32],
merkle_branch: vec![[0xCD; 32]],
block_header: vec![0x01; 80],
block_height: 1000,
confirmations: 6,
});
let eth_inclusion =
CrossChainInclusionProof::Ethereum(crate::cross_chain::EthereumMPTProof {
tx_hash: [0xAB; 32],
receipt_root: [0xCD; 32],
receipt_rlp: vec![0x01; 100],
merkle_nodes: vec![vec![0xEF; 64]],
block_header: vec![0x02; 80],
log_index: 0,
confirmations: 15,
});
let seal = SealRef::new(vec![0x01], None).unwrap();
let event_btc = SealConsumptionEvent {
chain: ChainId::Bitcoin,
seal: seal.clone(),
right: right.clone(),
inclusion: btc_inclusion,
height: 1000,
tx_hash: Hash::new([0xAB; 32]),
};
assert!(client.verify_seal_consumption_event(event_btc).is_ok());
let right2 = Right::new(
Hash::new([0xEF; 32]),
OwnershipProof {
proof: vec![0x02],
owner: vec![0xEE; 32],
scheme: None,
},
&[0x99],
);
let event_eth = SealConsumptionEvent {
chain: ChainId::Ethereum,
seal: seal.clone(),
right: right2,
inclusion: eth_inclusion,
height: 2000,
tx_hash: Hash::new([0xBC; 32]),
};
let result = client.verify_seal_consumption_event(event_eth);
assert!(result.is_err());
}
#[test]
fn test_seal_consumption_invalid_inclusion() {
let mut client = ValidationClient::new();
let right = Right::new(
Hash::new([0xCD; 32]),
OwnershipProof {
proof: vec![0x01],
owner: vec![0xFF; 32],
scheme: None,
},
&[0x42],
);
let inclusion = CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
txid: [0xAB; 32],
merkle_branch: vec![], block_header: vec![0x01; 80],
block_height: 1000,
confirmations: 6,
});
let event = SealConsumptionEvent {
chain: ChainId::Bitcoin,
seal: SealRef::new(vec![0x01], None).unwrap(),
right,
inclusion,
height: 1000,
tx_hash: Hash::new([0xAB; 32]),
};
let result = client.verify_seal_consumption_event(event);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ValidationError::InclusionProofFailed(_)
));
}
}