Skip to main content

csv_adapter_core/
validator.rs

1//! Consignment Validation Pipeline
2//!
3//! Provides detailed, step-by-step validation of consignments:
4//! 1. Fetch state proof chain
5//! 2. Verify commitment linkage
6//! 3. Verify single consumption of each seal
7//! 4. Verify no conflicting state transitions
8//! 5. Accept or reject based on full validation
9//!
10//! ## Validation Pipeline
11//!
12//! ```text
13//! Consignment Received
14//!   ↓
15//! [1] Structural Validation
16//!   - Version check
17//!   - Schema ID consistency  
18//!   - Required fields present
19//!   ↓
20//! [2] Commitment Chain Validation
21//!   - Genesis → Latest chain integrity
22//!   - No missing commitments
23//!   - No cycles or duplicates
24//!   ↓
25//! [3] Seal Consumption Validation  
26//!   - Each seal consumed at most once
27//!   - Cross-chain double-spend check
28//!   - Seal references match transitions
29//!   ↓
30//! [4] State Transition Validation
31//!   - Inputs satisfied by prior outputs
32//!   - State conservation rules
33//!   - No conflicting transitions
34//!   ↓
35//! [5] Final Acceptance Decision
36//!   - All checks pass → Accept
37//!   - Any check fails → Reject with reason
38//! ```
39
40use alloc::vec::Vec;
41
42use crate::consignment::Consignment;
43use crate::hash::Hash;
44use crate::seal_registry::{ChainId, CrossChainSealRegistry, SealConsumption, SealStatus};
45use crate::state_store::InMemoryStateStore;
46
47/// Detailed validation report.
48#[derive(Debug)]
49pub struct ValidationReport {
50    /// Whether the consignment passed validation
51    pub passed: bool,
52    /// Individual validation step results
53    pub steps: Vec<ValidationStep>,
54    /// Summary of findings
55    pub summary: String,
56}
57
58/// A single validation step result.
59#[derive(Debug)]
60pub struct ValidationStep {
61    /// Name of the validation step
62    pub name: String,
63    /// Whether this step passed
64    pub passed: bool,
65    /// Details of the validation (for debugging)
66    pub details: String,
67}
68
69/// Consignment validator with detailed reporting.
70pub struct ConsignmentValidator {
71    /// State history store
72    store: InMemoryStateStore,
73    /// Cross-chain seal registry  
74    seal_registry: CrossChainSealRegistry,
75    /// Validation report being built
76    report: ValidationReport,
77}
78
79impl ConsignmentValidator {
80    /// Create a new validator.
81    pub fn new() -> Self {
82        Self {
83            store: InMemoryStateStore::new(),
84            seal_registry: CrossChainSealRegistry::new(),
85            report: ValidationReport {
86                passed: true,
87                steps: Vec::new(),
88                summary: String::new(),
89            },
90        }
91    }
92
93    /// Validate a consignment with detailed reporting.
94    pub fn validate_consignment(
95        mut self,
96        consignment: &Consignment,
97        anchor_chain: ChainId,
98    ) -> ValidationReport {
99        // Step 1: Structural validation
100        self.validate_structure(consignment);
101
102        // Step 2: Commitment chain validation
103        self.validate_commitment_chain(consignment);
104
105        // Step 3: Seal consumption validation
106        self.validate_seal_consumption(consignment, &anchor_chain);
107
108        // Step 4: State transition validation
109        self.validate_state_transitions(consignment);
110
111        // Step 5: Generate summary
112        self.generate_summary();
113
114        self.report
115    }
116
117    /// Step 1: Validate consignment structure.
118    fn validate_structure(&mut self, consignment: &Consignment) {
119        let result = consignment.validate_structure();
120        let passed = result.is_ok();
121
122        self.report.steps.push(ValidationStep {
123            name: "Structural Validation".to_string(),
124            passed,
125            details: if passed {
126                "All structural checks passed".to_string()
127            } else {
128                format!("Structural validation failed: {}", result.unwrap_err())
129            },
130        });
131
132        if !passed {
133            self.report.passed = false;
134        }
135    }
136
137    /// Step 2: Validate commitment chain integrity.
138    fn validate_commitment_chain(&mut self, consignment: &Consignment) {
139        // Verify that the anchors form a valid commitment chain.
140        // Each anchor contains a commitment hash and an inclusion proof.
141        // We verify that:
142        // 1. The genesis commitment is consistent with the consignment
143        // 2. Each transition's hash is linked to its anchor's commitment
144        // 3. The commitment chain forms an unbroken sequence
145
146        if consignment.anchors.is_empty() {
147            // No anchors means no on-chain commitments to verify
148            // This is valid for a genesis-only consignment
149            self.report.steps.push(ValidationStep {
150                name: "Commitment Chain Validation".to_string(),
151                passed: true,
152                details: "No anchors — genesis-only consignment".to_string(),
153            });
154            return;
155        }
156
157        // Verify anchor count matches transition count
158        if consignment.anchors.len() != consignment.transitions.len() {
159            self.report.steps.push(ValidationStep {
160                name: "Commitment Chain Validation".to_string(),
161                passed: false,
162                details: format!(
163                    "Anchor count mismatch: {} anchors but {} transitions",
164                    consignment.anchors.len(),
165                    consignment.transitions.len(),
166                ),
167            });
168            self.report.passed = false;
169            return;
170        }
171
172        // Verify each transition's hash is anchored
173        let mut all_valid = true;
174        let mut details = Vec::new();
175
176        for (i, (transition, anchor)) in consignment
177            .transitions
178            .iter()
179            .zip(consignment.anchors.iter())
180            .enumerate()
181        {
182            let tx_hash = transition.hash();
183            if tx_hash != anchor.commitment {
184                all_valid = false;
185                details.push(format!(
186                    "Transition {} hash {} not anchored (got {})",
187                    i,
188                    hex::encode(tx_hash.as_bytes()),
189                    hex::encode(anchor.commitment.as_bytes()),
190                ));
191            }
192        }
193
194        // Verify inclusion proofs are non-empty (basic check; full MPT/Merkle
195        // verification is done by chain-specific verifiers)
196        for (i, anchor) in consignment.anchors.iter().enumerate() {
197            if anchor.inclusion_proof.is_empty() {
198                all_valid = false;
199                details.push(format!("Anchor {} has empty inclusion proof", i));
200            }
201            if anchor.finality_proof.is_empty() {
202                all_valid = false;
203                details.push(format!("Anchor {} has empty finality proof", i));
204            }
205        }
206
207        self.report.steps.push(ValidationStep {
208            name: "Commitment Chain Validation".to_string(),
209            passed: all_valid,
210            details: if all_valid {
211                format!(
212                    "Verified {} commitment(s) anchored on-chain",
213                    consignment.anchors.len(),
214                )
215            } else {
216                details.join("; ")
217            },
218        });
219
220        if !all_valid {
221            self.report.passed = false;
222        }
223    }
224
225    /// Step 3: Validate seal consumption (no double-spends).
226    fn validate_seal_consumption(&mut self, consignment: &Consignment, anchor_chain: &ChainId) {
227        let mut all_passed = true;
228        let mut details = Vec::new();
229
230        for seal_assignment in &consignment.seal_assignments {
231            match self
232                .seal_registry
233                .check_seal_status(&seal_assignment.seal_ref)
234            {
235                SealStatus::Unconsumed => {
236                    // Create a synthetic Right ID for tracking
237                    let right_id = crate::right::RightId(Hash::new(
238                        seal_assignment
239                            .seal_ref
240                            .seal_id
241                            .clone()
242                            .try_into()
243                            .unwrap_or([0u8; 32]),
244                    ));
245
246                    let consumption = SealConsumption {
247                        chain: anchor_chain.clone(),
248                        seal_ref: seal_assignment.seal_ref.clone(),
249                        right_id,
250                        block_height: 0,
251                        tx_hash: Hash::new([0u8; 32]),
252                        recorded_at: 0,
253                    };
254
255                    if let Err(e) = self.seal_registry.record_consumption(consumption) {
256                        all_passed = false;
257                        details.push(format!("Double-spend: {:?}", e));
258                    }
259                }
260                SealStatus::ConsumedOnChain { chain, .. } => {
261                    all_passed = false;
262                    details.push(format!("Seal already consumed on {:?}", chain));
263                }
264                SealStatus::DoubleSpent { consumptions } => {
265                    all_passed = false;
266                    details.push(format!(
267                        "Seal double-spent across {} chains",
268                        consumptions.len()
269                    ));
270                }
271            }
272        }
273
274        self.report.steps.push(ValidationStep {
275            name: "Seal Consumption Validation".to_string(),
276            passed: all_passed,
277            details: if all_passed {
278                format!(
279                    "All {} seals validated successfully",
280                    consignment.seal_assignments.len()
281                )
282            } else {
283                details.join("; ")
284            },
285        });
286
287        if !all_passed {
288            self.report.passed = false;
289        }
290    }
291
292    /// Step 4: Validate state transitions.
293    fn validate_state_transitions(&mut self, consignment: &Consignment) {
294        // Verify state transitions are valid:
295        // 1. Each transition's validation script is non-empty
296        // 2. Each transition's input references point to valid commitments
297        // 3. Seal assignments are consistent with transition outputs
298        let mut all_valid = true;
299        let mut details = Vec::new();
300
301        // Track available commitments from genesis and transition outputs
302        let mut available_commitments: alloc::collections::BTreeSet<Hash> =
303            alloc::collections::BTreeSet::new();
304
305        // Genesis outputs are initially available (indexed by their commitment hash)
306        for _owned in &consignment.genesis.owned_state {
307            available_commitments.insert(consignment.genesis.hash());
308        }
309
310        for (i, transition) in consignment.transitions.iter().enumerate() {
311            // Check validation script is non-empty
312            if transition.validation_script.is_empty() {
313                all_valid = false;
314                details.push(format!("Transition {} has empty validation script", i));
315            }
316
317            // Verify input references point to known commitments
318            for input in &transition.owned_inputs {
319                if !available_commitments.contains(&input.commitment) {
320                    all_valid = false;
321                    details.push(format!(
322                        "Transition {} references unknown commitment {}",
323                        i,
324                        hex::encode(input.commitment.as_bytes()),
325                    ));
326                }
327            }
328
329            // Track transition outputs as available for subsequent transitions
330            available_commitments.insert(transition.hash());
331        }
332
333        // Verify seal assignments reference valid transition outputs
334        for (i, assignment) in consignment.seal_assignments.iter().enumerate() {
335            // The assignment should correspond to a transition output
336            // Check that the assignment's metadata is well-formed
337            if assignment.assignment.data.is_empty() {
338                details.push(format!("Seal assignment {} has empty data", i));
339            }
340        }
341
342        self.report.steps.push(ValidationStep {
343            name: "State Transition Validation".to_string(),
344            passed: all_valid,
345            details: if all_valid {
346                format!(
347                    "Validated {} transitions, {} commitments tracked",
348                    consignment.transitions.len(),
349                    available_commitments.len(),
350                )
351            } else {
352                details.join("; ")
353            },
354        });
355
356        if !all_valid {
357            self.report.passed = false;
358        }
359    }
360
361    /// Generate final summary.
362    fn generate_summary(&mut self) {
363        let passed_count = self.report.steps.iter().filter(|s| s.passed).count();
364        let total_count = self.report.steps.len();
365
366        self.report.summary = if self.report.passed {
367            format!(
368                "Consignment accepted: {}/{} validation steps passed",
369                passed_count, total_count
370            )
371        } else {
372            let failed: Vec<&str> = self
373                .report
374                .steps
375                .iter()
376                .filter(|s| !s.passed)
377                .map(|s| s.name.as_str())
378                .collect();
379            format!(
380                "Consignment rejected: {} steps failed: {}",
381                failed.len(),
382                failed.join(", ")
383            )
384        };
385    }
386
387    /// Get access to the state store.
388    pub fn store(&self) -> &InMemoryStateStore {
389        &self.store
390    }
391
392    /// Get access to the seal registry.
393    pub fn seal_registry(&self) -> &CrossChainSealRegistry {
394        &self.seal_registry
395    }
396}
397
398impl Default for ConsignmentValidator {
399    fn default() -> Self {
400        Self::new()
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::consignment::Consignment;
408    use crate::genesis::Genesis;
409    use crate::state_store::StateHistoryStore;
410
411    fn make_test_consignment() -> Consignment {
412        let genesis = Genesis::new(
413            Hash::new([0xAB; 32]),
414            Hash::new([0x01; 32]),
415            vec![],
416            vec![],
417            vec![],
418        );
419        Consignment::new(genesis, vec![], vec![], vec![], Hash::new([0x01; 32]))
420    }
421
422    #[test]
423    fn test_validator_creation() {
424        let validator = ConsignmentValidator::new();
425        assert_eq!(validator.store().list_contracts().unwrap().len(), 0);
426    }
427
428    #[test]
429    fn test_validate_simple_consignment() {
430        let validator = ConsignmentValidator::new();
431        let consignment = make_test_consignment();
432
433        let report = validator.validate_consignment(&consignment, ChainId::Bitcoin);
434
435        // Should have validation steps
436        assert!(!report.steps.is_empty());
437
438        // All steps should pass for a simple valid consignment
439        for step in &report.steps {
440            assert!(step.passed, "Step '{}' failed: {}", step.name, step.details);
441        }
442    }
443
444    #[test]
445    fn test_validation_report_structure() {
446        let validator = ConsignmentValidator::new();
447        let consignment = make_test_consignment();
448
449        let report = validator.validate_consignment(&consignment, ChainId::Bitcoin);
450
451        // Report should have meaningful content
452        assert!(!report.summary.is_empty());
453        assert!(report.steps.len() >= 3); // At least structural, seal, and transition validation
454    }
455
456    #[test]
457    fn test_validation_steps_are_sequential() {
458        let validator = ConsignmentValidator::new();
459        let consignment = make_test_consignment();
460
461        let report = validator.validate_consignment(&consignment, ChainId::Bitcoin);
462
463        // Steps should be in expected order
464        let step_names: Vec<&str> = report.steps.iter().map(|s| s.name.as_str()).collect();
465
466        assert!(step_names.contains(&"Structural Validation"));
467        assert!(step_names.contains(&"Seal Consumption Validation"));
468        assert!(step_names.contains(&"State Transition Validation"));
469    }
470}