Skip to main content

csv_adapter_core/
client.rs

1//! Client-Side Validation Engine
2//!
3//! The client receives consignments and seal consumption proofs from peers,
4//! verifies them locally, and accepts or rejects state transitions.
5//!
6//! ## Flow
7//!
8//! ```text
9//! Client receives consignment from peer:
10//!   │
11//!   ├─ Bitcoin anchor?  → Map UTXO spend → Right(id, commitment, owner, nullifier=None)
12//!   ├─ Sui anchor?      → Map object deletion → Right(id, commitment, owner, nullifier=None)
13//!   ├─ Aptos anchor?    → Map resource destruction → Right(id, commitment, owner, nullifier=None)
14//!   └─ Ethereum anchor? → Map nullifier registration → Right(id, commitment, owner, nullifier=Some(hash))
15//!         │
16//!         ▼
17//!   Client validates uniformly:
18//!     1. Each Right.verify() passes
19//!     2. Commitment chain integrity (genesis → present)
20//!     3. No seal double-consumption (cross-chain registry)
21//!     4. Accept or reject the consignment
22//! ```
23
24use 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/// Result of consignment validation.
42#[derive(Debug)]
43pub enum ValidationResult {
44    /// Consignment is valid and has been accepted
45    Accepted {
46        /// The validated contract history
47        history: ContractHistory,
48        /// Number of Rights validated
49        rights_count: usize,
50        /// Number of seals consumed
51        seals_consumed: usize,
52    },
53    /// Consignment was rejected due to validation failure
54    Rejected {
55        /// Reason for rejection
56        reason: ValidationError,
57    },
58}
59
60/// Errors that can occur during consignment validation.
61#[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/// Seal consumption event — the atomic unit of client-side validation.
87///
88/// When a seal is consumed on any chain, this event is created.
89/// The client verifies this event and accepts or rejects the state transition.
90#[derive(Clone, Debug)]
91pub struct SealConsumptionEvent {
92    /// Which chain enforced the consumption
93    pub chain: ChainId,
94    /// The seal that was consumed
95    pub seal: SealRef,
96    /// The Right after consumption (new owner, etc.)
97    pub right: Right,
98    /// Inclusion proof (chain-specific)
99    pub inclusion: CrossChainInclusionProof,
100    /// Block/checkpoint height
101    pub height: u64,
102    /// Transaction hash that consumed the seal
103    pub tx_hash: Hash,
104}
105
106/// Client-side validation engine.
107///
108/// Receives consignments and seal consumption proofs,
109/// verifies them against local state and the cross-chain registry,
110/// and accepts or rejects state transitions.
111pub struct ValidationClient {
112    /// Persistent state history store
113    store: InMemoryStateStore,
114    /// Cross-chain seal registry — prevents double-spend across all chains
115    seal_registry: CrossChainSealRegistry,
116}
117
118impl ValidationClient {
119    /// Create a new validation client.
120    pub fn new() -> Self {
121        Self {
122            store: InMemoryStateStore::new(),
123            seal_registry: CrossChainSealRegistry::new(),
124        }
125    }
126
127    /// Receive and validate a consignment from a peer.
128    ///
129    /// This is the main entry point. It:
130    /// 1. Validates consignment structure
131    /// 2. Extracts commitments and verifies the chain
132    /// 3. Maps anchors to Rights and verifies seal consumption
133    /// 4. Updates local state if valid
134    pub fn receive_consignment(
135        &mut self,
136        consignment: &Consignment,
137        anchor_chain: ChainId,
138    ) -> ValidationResult {
139        // Step 1: Structural validation
140        if let Err(e) = consignment.validate_structure() {
141            return ValidationResult::Rejected {
142                reason: ValidationError::SealAssignmentError(e.to_string()),
143            };
144        }
145
146        // Step 2: Extract and verify commitment chain
147        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        // Step 3: Verify seal consumption
158        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        // Step 4: Update local state
165        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    /// Receive and verify a seal consumption event from any chain.
177    ///
178    /// This is the cross-chain portability entry point.
179    /// Any client can verify any chain's seal consumption proof.
180    pub fn verify_seal_consumption_event(
181        &mut self,
182        event: SealConsumptionEvent,
183    ) -> Result<(), ValidationError> {
184        // Step 1: Verify the Right itself
185        event
186            .right
187            .verify()
188            .map_err(ValidationError::RightValidationError)?;
189
190        // Step 2: Check seal not already consumed (cross-chain)
191        match self.seal_registry.check_seal_status(&event.seal) {
192            SealStatus::Unconsumed => {
193                // OK — first consumption
194            }
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        // Step 3: Verify inclusion proof (chain-specific)
209        self.verify_inclusion_proof(&event.inclusion, &event.chain)?;
210
211        // Step 4: Record in cross-chain registry
212        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, // Would be current timestamp
219        };
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    /// Extract commitments from a consignment.
229    ///
230    /// Commitments come from:
231    /// - Genesis (the root commitment)
232    /// - Anchors (each anchor contains a commitment)
233    /// - Transitions (each transition has a payload hash that links to a commitment)
234    fn extract_commitments(&self, consignment: &Consignment) -> Vec<Commitment> {
235        // In a full implementation, commitments would be extracted from
236        // the consignment's anchors and transitions.
237        // For now, we construct synthetic commitments from the seal assignments.
238        let mut commitments = Vec::new();
239
240        // The genesis provides the root commitment
241        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]), // Genesis has no previous commitment
248                Hash::new([0u8; 32]),
249                &seal,
250                domain,
251            )
252        };
253        commitments.push(genesis_commitment);
254
255        // Each seal assignment represents a state transition with a commitment
256        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]), // Would come from transition payload
269                &seal,
270                domain,
271            );
272            commitments.push(commitment);
273        }
274
275        commitments
276    }
277
278    /// Verify the commitment chain.
279    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        // Use the ordered chain verifier — commitments are extracted in order
288        verify_ordered_commitment_chain(commitments)
289    }
290
291    /// Verify seal consumption for all seal assignments in the consignment.
292    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            // Check if seal has already been consumed
302            match self
303                .seal_registry
304                .check_seal_status(&seal_assignment.seal_ref)
305            {
306                SealStatus::Unconsumed => {
307                    // Seal is fresh — record consumption
308                    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,               // Would come from anchor
321                        tx_hash: Hash::new([0u8; 32]), // Would come from anchor
322                        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    /// Verify an inclusion proof from any chain.
349    fn verify_inclusion_proof(
350        &self,
351        inclusion: &CrossChainInclusionProof,
352        chain: &ChainId,
353    ) -> Result<(), ValidationError> {
354        match (inclusion, chain) {
355            (CrossChainInclusionProof::Bitcoin(proof), _) => {
356                // Verify Merkle branch is non-empty and structurally valid
357                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                // In production: verify Merkle root matches block header
368                // verify_merkle_proof(txid, &proof.merkle_branch) == header.merkle_root
369            }
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                // In production: verify MPT proof via alloy-trie
377            }
378            (CrossChainInclusionProof::Sui(proof), _) => {
379                if !proof.certified {
380                    return Err(ValidationError::InclusionProofFailed(
381                        "Checkpoint not certified".to_string(),
382                    ));
383                }
384                // In production: verify checkpoint certification
385            }
386            (CrossChainInclusionProof::Aptos(proof), _) => {
387                if !proof.success {
388                    return Err(ValidationError::InclusionProofFailed(
389                        "Transaction failed".to_string(),
390                    ));
391                }
392                // In production: verify HotStuff ledger signatures
393            }
394        }
395
396        Ok(())
397    }
398
399    /// Update local state with validated consignment data.
400    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        // Load or create contract history
408        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        // Add transitions to history
415        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        // Save updated history
453        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    /// Get the state history store.
461    pub fn store(&self) -> &InMemoryStateStore {
462        &self.store
463    }
464
465    /// Get the cross-chain seal registry.
466    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                // Empty consignment with no seal assignments is valid
520                assert_eq!(rights_count, 0);
521                assert_eq!(seals_consumed, 0);
522            }
523            ValidationResult::Rejected { reason } => {
524                // Rejection is also acceptable (depends on implementation details)
525                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        // Should have 3 contracts tracked
544        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        // Seal should now be in registry
582        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        // Try to consume same seal again
621        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        // Consume on Bitcoin
685        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        // Try to consume on Ethereum (cross-chain double-spend)
696        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        // Empty Merkle branch should fail
734        let inclusion = CrossChainInclusionProof::Bitcoin(crate::cross_chain::BitcoinMerkleProof {
735            txid: [0xAB; 32],
736            merkle_branch: vec![], // Empty!
737            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}