1use 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#[derive(Debug)]
49pub struct ValidationReport {
50 pub passed: bool,
52 pub steps: Vec<ValidationStep>,
54 pub summary: String,
56}
57
58#[derive(Debug)]
60pub struct ValidationStep {
61 pub name: String,
63 pub passed: bool,
65 pub details: String,
67}
68
69pub struct ConsignmentValidator {
71 store: InMemoryStateStore,
73 seal_registry: CrossChainSealRegistry,
75 report: ValidationReport,
77}
78
79impl ConsignmentValidator {
80 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 pub fn validate_consignment(
95 mut self,
96 consignment: &Consignment,
97 anchor_chain: ChainId,
98 ) -> ValidationReport {
99 self.validate_structure(consignment);
101
102 self.validate_commitment_chain(consignment);
104
105 self.validate_seal_consumption(consignment, &anchor_chain);
107
108 self.validate_state_transitions(consignment);
110
111 self.generate_summary();
113
114 self.report
115 }
116
117 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 fn validate_commitment_chain(&mut self, consignment: &Consignment) {
139 if consignment.anchors.is_empty() {
147 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 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 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 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 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 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 fn validate_state_transitions(&mut self, consignment: &Consignment) {
294 let mut all_valid = true;
299 let mut details = Vec::new();
300
301 let mut available_commitments: alloc::collections::BTreeSet<Hash> =
303 alloc::collections::BTreeSet::new();
304
305 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 if transition.validation_script.is_empty() {
313 all_valid = false;
314 details.push(format!("Transition {} has empty validation script", i));
315 }
316
317 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 available_commitments.insert(transition.hash());
331 }
332
333 for (i, assignment) in consignment.seal_assignments.iter().enumerate() {
335 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 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 pub fn store(&self) -> &InMemoryStateStore {
389 &self.store
390 }
391
392 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 assert!(!report.steps.is_empty());
437
438 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 assert!(!report.summary.is_empty());
453 assert!(report.steps.len() >= 3); }
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 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}