1use alloc::string::String;
25use alloc::vec::Vec;
26
27use crate::commitment::Commitment;
28use crate::commitment_chain::{
29 verify_ordered_commitment_chain, ChainError, ChainVerificationResult,
30};
31use crate::consignment::Consignment;
32use crate::cross_chain::InclusionProof as CrossChainInclusionProof;
33use crate::hash::Hash;
34use crate::right::{Right, RightError, RightId};
35use crate::seal::SealRef;
36use crate::seal_registry::{ChainId, CrossChainSealRegistry, SealConsumption, SealStatus};
37use crate::state_store::{
38 ContractHistory, InMemoryStateStore, StateHistoryStore, StateTransitionRecord,
39};
40
41#[derive(Debug)]
43pub enum ValidationResult {
44 Accepted {
46 history: ContractHistory,
48 rights_count: usize,
50 seals_consumed: usize,
52 },
53 Rejected {
55 reason: ValidationError,
57 },
58}
59
60#[derive(Debug, thiserror::Error)]
62#[allow(missing_docs)]
63pub enum ValidationError {
64 #[error("Empty consignment")]
65 EmptyConsignment,
66 #[error("Commitment chain verification failed: {0}")]
67 CommitmentChainError(#[from] ChainError),
68 #[error("Right validation failed: {0}")]
69 RightValidationError(#[from] RightError),
70 #[error("Double-spend detected")]
71 DoubleSpend(String),
72 #[error("Missing history: contract has incomplete state history")]
73 MissingHistory(String),
74 #[error("Seal assignment error: {0}")]
75 SealAssignmentError(String),
76 #[error("State store error: {0}")]
77 StoreError(String),
78 #[error("Contract ID mismatch: expected {expected}, got {actual}")]
79 ContractIdMismatch { expected: Hash, actual: Hash },
80 #[error("Unsupported consignment version: {version}")]
81 UnsupportedVersion { version: u32 },
82 #[error("Inclusion proof verification failed: {0}")]
83 InclusionProofFailed(String),
84}
85
86#[derive(Clone, Debug)]
91pub struct SealConsumptionEvent {
92 pub chain: ChainId,
94 pub seal: SealRef,
96 pub right: Right,
98 pub inclusion: CrossChainInclusionProof,
100 pub height: u64,
102 pub tx_hash: Hash,
104}
105
106pub struct ValidationClient {
112 store: InMemoryStateStore,
114 seal_registry: CrossChainSealRegistry,
116}
117
118impl ValidationClient {
119 pub fn new() -> Self {
121 Self {
122 store: InMemoryStateStore::new(),
123 seal_registry: CrossChainSealRegistry::new(),
124 }
125 }
126
127 pub fn receive_consignment(
135 &mut self,
136 consignment: &Consignment,
137 anchor_chain: ChainId,
138 ) -> ValidationResult {
139 if let Err(e) = consignment.validate_structure() {
141 return ValidationResult::Rejected {
142 reason: ValidationError::SealAssignmentError(e.to_string()),
143 };
144 }
145
146 let commitments = self.extract_commitments(consignment);
148 let chain_result = match self.verify_commitment_chain(&commitments) {
149 Ok(result) => result,
150 Err(e) => {
151 return ValidationResult::Rejected {
152 reason: ValidationError::CommitmentChainError(e),
153 }
154 }
155 };
156
157 let seals_consumed =
159 match self.verify_seal_consumption(consignment, &chain_result, &anchor_chain) {
160 Ok(count) => count,
161 Err(e) => return ValidationResult::Rejected { reason: e },
162 };
163
164 if let Err(e) = self.update_local_state(consignment, &chain_result) {
166 return ValidationResult::Rejected { reason: e };
167 }
168
169 ValidationResult::Accepted {
170 history: ContractHistory::from_genesis(chain_result.genesis.clone()),
171 rights_count: consignment.seal_assignments.len(),
172 seals_consumed,
173 }
174 }
175
176 pub fn verify_seal_consumption_event(
181 &mut self,
182 event: SealConsumptionEvent,
183 ) -> Result<(), ValidationError> {
184 event
186 .right
187 .verify()
188 .map_err(|e| ValidationError::RightValidationError(e))?;
189
190 match self.seal_registry.check_seal_status(&event.seal) {
192 SealStatus::Unconsumed => {
193 }
195 SealStatus::ConsumedOnChain { chain, .. } => {
196 return Err(ValidationError::DoubleSpend(format!(
197 "Seal already consumed on {:?}",
198 chain
199 )));
200 }
201 SealStatus::DoubleSpent { .. } => {
202 return Err(ValidationError::DoubleSpend(
203 "Seal has been double-spent across chains".to_string(),
204 ));
205 }
206 }
207
208 self.verify_inclusion_proof(&event.inclusion, &event.chain)?;
210
211 let consumption = SealConsumption {
213 chain: event.chain.clone(),
214 seal_ref: event.seal.clone(),
215 right_id: event.right.id.clone(),
216 block_height: event.height,
217 tx_hash: event.tx_hash,
218 recorded_at: 0, };
220
221 if let Err(e) = self.seal_registry.record_consumption(consumption) {
222 return Err(ValidationError::DoubleSpend(format!("{:?}", e)));
223 }
224
225 Ok(())
226 }
227
228 fn extract_commitments(&self, consignment: &Consignment) -> Vec<Commitment> {
235 let mut commitments = Vec::new();
239
240 let genesis_commitment = {
242 let domain = [0u8; 32];
243 let seal = SealRef::new(consignment.genesis.contract_id.as_bytes().to_vec(), None)
244 .unwrap_or_else(|_| SealRef::new(vec![0x01], None).unwrap());
245 Commitment::simple(
246 consignment.genesis.contract_id,
247 Hash::new([0u8; 32]), Hash::new([0u8; 32]),
249 &seal,
250 domain,
251 )
252 };
253 commitments.push(genesis_commitment);
254
255 for (i, assignment) in consignment.seal_assignments.iter().enumerate() {
257 let previous = if i == 0 {
258 commitments[0].hash()
259 } else {
260 commitments[i].hash()
261 };
262
263 let domain = [0u8; 32];
264 let seal = assignment.seal_ref.clone();
265 let commitment = Commitment::simple(
266 consignment.schema_id,
267 previous,
268 Hash::new([0u8; 32]), &seal,
270 domain,
271 );
272 commitments.push(commitment);
273 }
274
275 commitments
276 }
277
278 fn verify_commitment_chain(
280 &self,
281 commitments: &[Commitment],
282 ) -> Result<ChainVerificationResult, ChainError> {
283 if commitments.is_empty() {
284 return Err(ChainError::EmptyChain);
285 }
286
287 verify_ordered_commitment_chain(commitments)
289 }
290
291 fn verify_seal_consumption(
293 &mut self,
294 consignment: &Consignment,
295 _chain_result: &ChainVerificationResult,
296 anchor_chain: &ChainId,
297 ) -> Result<usize, ValidationError> {
298 let mut seals_consumed = 0;
299
300 for seal_assignment in &consignment.seal_assignments {
301 match self
303 .seal_registry
304 .check_seal_status(&seal_assignment.seal_ref)
305 {
306 SealStatus::Unconsumed => {
307 let right_id_bytes: [u8; 32] = {
309 let mut arr = [0u8; 32];
310 let seal_bytes = seal_assignment.seal_ref.to_vec();
311 let len = seal_bytes.len().min(32);
312 arr[..len].copy_from_slice(&seal_bytes[..len]);
313 arr
314 };
315
316 let consumption = SealConsumption {
317 chain: anchor_chain.clone(),
318 seal_ref: seal_assignment.seal_ref.clone(),
319 right_id: RightId(Hash::new(right_id_bytes)),
320 block_height: 0, tx_hash: Hash::new([0u8; 32]), recorded_at: 0,
323 };
324
325 if let Err(e) = self.seal_registry.record_consumption(consumption) {
326 return Err(ValidationError::DoubleSpend(format!("{:?}", e)));
327 }
328
329 seals_consumed += 1;
330 }
331 SealStatus::ConsumedOnChain { chain, .. } => {
332 return Err(ValidationError::DoubleSpend(format!(
333 "Seal already consumed on {:?}",
334 chain
335 )));
336 }
337 SealStatus::DoubleSpent { .. } => {
338 return Err(ValidationError::DoubleSpend(
339 "Seal has been double-spent".to_string(),
340 ));
341 }
342 }
343 }
344
345 Ok(seals_consumed)
346 }
347
348 fn verify_inclusion_proof(
350 &self,
351 inclusion: &CrossChainInclusionProof,
352 chain: &ChainId,
353 ) -> Result<(), ValidationError> {
354 match (inclusion, chain) {
355 (CrossChainInclusionProof::Bitcoin(proof), _) => {
356 if proof.merkle_branch.is_empty() {
358 return Err(ValidationError::InclusionProofFailed(
359 "Empty Merkle branch".to_string(),
360 ));
361 }
362 if proof.block_header.is_empty() {
363 return Err(ValidationError::InclusionProofFailed(
364 "Empty block header".to_string(),
365 ));
366 }
367 }
370 (CrossChainInclusionProof::Ethereum(proof), _) => {
371 if proof.receipt_rlp.is_empty() && proof.merkle_nodes.is_empty() {
372 return Err(ValidationError::InclusionProofFailed(
373 "Empty MPT proof".to_string(),
374 ));
375 }
376 }
378 (CrossChainInclusionProof::Sui(proof), _) => {
379 if !proof.certified {
380 return Err(ValidationError::InclusionProofFailed(
381 "Checkpoint not certified".to_string(),
382 ));
383 }
384 }
386 (CrossChainInclusionProof::Aptos(proof), _) => {
387 if !proof.success {
388 return Err(ValidationError::InclusionProofFailed(
389 "Transaction failed".to_string(),
390 ));
391 }
392 }
394 }
395
396 Ok(())
397 }
398
399 fn update_local_state(
401 &mut self,
402 consignment: &Consignment,
403 chain_result: &ChainVerificationResult,
404 ) -> Result<(), ValidationError> {
405 let contract_id = chain_result.contract_id;
406
407 let mut history = match self.store.load_contract_history(contract_id) {
409 Ok(Some(h)) => h,
410 Ok(None) => ContractHistory::from_genesis(chain_result.genesis.clone()),
411 Err(e) => return Err(ValidationError::StoreError(e.to_string())),
412 };
413
414 for (i, _transition) in consignment.transitions.iter().enumerate() {
416 let previous_hash = if i == 0 {
417 chain_result.genesis.hash()
418 } else if i <= history.transition_count() {
419 history.transitions[i - 1].commitment.hash()
420 } else {
421 chain_result.latest.hash()
422 };
423
424 let seal = if i < consignment.seal_assignments.len() {
425 consignment.seal_assignments[i].seal_ref.clone()
426 } else {
427 SealRef::new(vec![i as u8], None).unwrap()
428 };
429
430 let domain = [0u8; 32];
431 let commitment = Commitment::simple(
432 contract_id,
433 previous_hash,
434 Hash::new([0u8; 32]),
435 &seal,
436 domain,
437 );
438
439 let record = StateTransitionRecord {
440 commitment,
441 seal_ref: seal,
442 rights: Vec::new(),
443 block_height: 0,
444 verified: true,
445 };
446
447 history
448 .add_transition(record)
449 .map_err(|e| ValidationError::StoreError(e.to_string()))?;
450 }
451
452 if let Err(e) = self.store.save_contract_history(contract_id, &history) {
454 return Err(ValidationError::StoreError(e.to_string()));
455 }
456
457 Ok(())
458 }
459
460 pub fn store(&self) -> &InMemoryStateStore {
462 &self.store
463 }
464
465 pub fn seal_registry(&self) -> &CrossChainSealRegistry {
467 &self.seal_registry
468 }
469}
470
471impl Default for ValidationClient {
472 fn default() -> Self {
473 Self::new()
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480 use crate::consignment::Consignment;
481 use crate::genesis::Genesis;
482 use crate::OwnershipProof;
483
484 fn make_test_genesis() -> Genesis {
485 Genesis::new(
486 Hash::new([0xAB; 32]),
487 Hash::new([0x01; 32]),
488 vec![],
489 vec![],
490 vec![],
491 )
492 }
493
494 fn make_test_consignment() -> Consignment {
495 let genesis = make_test_genesis();
496 Consignment::new(genesis, vec![], vec![], vec![], Hash::new([0x01; 32]))
497 }
498
499 #[test]
500 fn test_client_creation() {
501 let client = ValidationClient::new();
502 assert_eq!(client.store().list_contracts().unwrap().len(), 0);
503 assert_eq!(client.seal_registry().total_seals(), 0);
504 }
505
506 #[test]
507 fn test_receive_consignment_empty() {
508 let mut client = ValidationClient::new();
509 let consignment = make_test_consignment();
510
511 let result = client.receive_consignment(&consignment, ChainId::Bitcoin);
512
513 match result {
514 ValidationResult::Accepted {
515 rights_count,
516 seals_consumed,
517 ..
518 } => {
519 assert_eq!(rights_count, 0);
521 assert_eq!(seals_consumed, 0);
522 }
523 ValidationResult::Rejected { reason } => {
524 let _ = reason;
526 }
527 }
528 }
529
530 #[test]
531 fn test_receive_multiple_consignments() {
532 let mut client = ValidationClient::new();
533
534 for i in 0..3 {
535 let mut genesis = make_test_genesis();
536 genesis.contract_id = Hash::new([i + 1; 32]);
537 let consignment =
538 Consignment::new(genesis, vec![], vec![], vec![], Hash::new([0x01; 32]));
539
540 let _ = client.receive_consignment(&consignment, ChainId::Bitcoin);
541 }
542
543 assert_eq!(client.store().list_contracts().unwrap().len(), 3);
545 }
546
547 #[test]
548 fn test_seal_consumption_event_btc() {
549 let mut client = ValidationClient::new();
550
551 let right = Right::new(
552 Hash::new([0xCD; 32]),
553 OwnershipProof {
554 proof: vec![0x01, 0x02, 0x03],
555 owner: vec![0xFF; 32],
556 scheme: None,
557 },
558 &[0x42],
559 );
560
561 let inclusion = CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
562 txid: [0xAB; 32],
563 merkle_branch: vec![[0xCD; 32], [0xEF; 32]],
564 block_header: vec![0x01; 80],
565 block_height: 1000,
566 confirmations: 6,
567 });
568
569 let event = SealConsumptionEvent {
570 chain: ChainId::Bitcoin,
571 seal: SealRef::new(vec![0x01], None).unwrap(),
572 right,
573 inclusion,
574 height: 1000,
575 tx_hash: Hash::new([0xAB; 32]),
576 };
577
578 let result = client.verify_seal_consumption_event(event);
579 assert!(result.is_ok());
580
581 assert_eq!(client.seal_registry().total_seals(), 1);
583 }
584
585 #[test]
586 fn test_seal_consumption_event_double_spend() {
587 let mut client = ValidationClient::new();
588
589 let right = Right::new(
590 Hash::new([0xCD; 32]),
591 OwnershipProof {
592 proof: vec![0x01],
593 owner: vec![0xFF; 32],
594 scheme: None,
595 },
596 &[0x42],
597 );
598
599 let inclusion = CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
600 txid: [0xAB; 32],
601 merkle_branch: vec![[0xCD; 32]],
602 block_header: vec![0x01; 80],
603 block_height: 1000,
604 confirmations: 6,
605 });
606
607 let seal = SealRef::new(vec![0x01], None).unwrap();
608
609 let event1 = SealConsumptionEvent {
610 chain: ChainId::Bitcoin,
611 seal: seal.clone(),
612 right: right.clone(),
613 inclusion: inclusion.clone(),
614 height: 1000,
615 tx_hash: Hash::new([0xAB; 32]),
616 };
617
618 assert!(client.verify_seal_consumption_event(event1).is_ok());
619
620 let right2 = Right::new(
622 Hash::new([0xEF; 32]),
623 OwnershipProof {
624 proof: vec![0x02],
625 owner: vec![0xEE; 32],
626 scheme: None,
627 },
628 &[0x99],
629 );
630
631 let event2 = SealConsumptionEvent {
632 chain: ChainId::Bitcoin,
633 seal: seal.clone(),
634 right: right2,
635 inclusion,
636 height: 1001,
637 tx_hash: Hash::new([0xBC; 32]),
638 };
639
640 let result = client.verify_seal_consumption_event(event2);
641 assert!(result.is_err());
642 assert!(matches!(
643 result.unwrap_err(),
644 ValidationError::DoubleSpend(_)
645 ));
646 }
647
648 #[test]
649 fn test_seal_consumption_cross_chain() {
650 let mut client = ValidationClient::new();
651
652 let right = Right::new(
653 Hash::new([0xCD; 32]),
654 OwnershipProof {
655 proof: vec![0x01],
656 owner: vec![0xFF; 32],
657 scheme: None,
658 },
659 &[0x42],
660 );
661
662 let btc_inclusion =
663 CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
664 txid: [0xAB; 32],
665 merkle_branch: vec![[0xCD; 32]],
666 block_header: vec![0x01; 80],
667 block_height: 1000,
668 confirmations: 6,
669 });
670
671 let eth_inclusion =
672 CrossChainInclusionProof::Ethereum(crate::cross_chain::EthereumMPTProof {
673 tx_hash: [0xAB; 32],
674 receipt_root: [0xCD; 32],
675 receipt_rlp: vec![0x01; 100],
676 merkle_nodes: vec![vec![0xEF; 64]],
677 block_header: vec![0x02; 80],
678 log_index: 0,
679 confirmations: 15,
680 });
681
682 let seal = SealRef::new(vec![0x01], None).unwrap();
683
684 let event_btc = SealConsumptionEvent {
686 chain: ChainId::Bitcoin,
687 seal: seal.clone(),
688 right: right.clone(),
689 inclusion: btc_inclusion,
690 height: 1000,
691 tx_hash: Hash::new([0xAB; 32]),
692 };
693 assert!(client.verify_seal_consumption_event(event_btc).is_ok());
694
695 let right2 = Right::new(
697 Hash::new([0xEF; 32]),
698 OwnershipProof {
699 proof: vec![0x02],
700 owner: vec![0xEE; 32],
701 scheme: None,
702 },
703 &[0x99],
704 );
705
706 let event_eth = SealConsumptionEvent {
707 chain: ChainId::Ethereum,
708 seal: seal.clone(),
709 right: right2,
710 inclusion: eth_inclusion,
711 height: 2000,
712 tx_hash: Hash::new([0xBC; 32]),
713 };
714
715 let result = client.verify_seal_consumption_event(event_eth);
716 assert!(result.is_err());
717 }
718
719 #[test]
720 fn test_seal_consumption_invalid_inclusion() {
721 let mut client = ValidationClient::new();
722
723 let right = Right::new(
724 Hash::new([0xCD; 32]),
725 OwnershipProof {
726 proof: vec![0x01],
727 owner: vec![0xFF; 32],
728 scheme: None,
729 },
730 &[0x42],
731 );
732
733 let inclusion = CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
735 txid: [0xAB; 32],
736 merkle_branch: vec![], block_header: vec![0x01; 80],
738 block_height: 1000,
739 confirmations: 6,
740 });
741
742 let event = SealConsumptionEvent {
743 chain: ChainId::Bitcoin,
744 seal: SealRef::new(vec![0x01], None).unwrap(),
745 right,
746 inclusion,
747 height: 1000,
748 tx_hash: Hash::new([0xAB; 32]),
749 };
750
751 let result = client.verify_seal_consumption_event(event);
752 assert!(result.is_err());
753 assert!(matches!(
754 result.unwrap_err(),
755 ValidationError::InclusionProofFailed(_)
756 ));
757 }
758}