1use serde::{Deserialize, Serialize};
8
9use crate::consignment::{Anchor, Consignment};
10use crate::hash::Hash;
11use crate::schema::Schema;
12#[cfg(feature = "tapret")]
13use crate::tapret_verify;
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct RgbValidationResult {
18 pub is_valid: bool,
20 pub errors: Vec<RgbValidationError>,
22 pub consignment_id: Hash,
24 pub contract_id: Hash,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[allow(missing_docs)]
31pub enum RgbValidationError {
32 TopologicalOrderViolation {
34 transition_index: usize,
35 depends_on: usize,
36 },
37 SealDoubleSpend {
39 seal_ref: crate::seal::SealRef,
40 first_seen: usize,
41 second_seen: usize,
42 },
43 MissingStateInput {
45 transition_index: usize,
46 state_ref: crate::state::StateRef,
47 },
48 AnchorCommitmentMismatch {
50 anchor_index: usize,
51 expected: Hash,
52 actual: Hash,
53 },
54 SchemaValidationFailed {
56 transition_index: usize,
57 error: String,
58 },
59 GenesisHasInputs,
61 ValueInflation {
63 transition_index: usize,
64 type_id: u16,
65 input_sum: u64,
66 output_sum: u64,
67 },
68 MissingSchema,
70 InvalidSignature { transition_index: usize },
72}
73
74pub struct RgbConsignmentValidator;
76
77impl RgbConsignmentValidator {
78 pub fn validate(consignment: &Consignment, schema: Option<&Schema>) -> RgbValidationResult {
80 let mut errors = Vec::new();
81
82 let consignment_id = Self::compute_consignment_id(consignment);
84
85 let contract_id = Self::compute_contract_id(&consignment.genesis);
87
88 if Self::genesis_has_inputs(consignment) {
90 errors.push(RgbValidationError::GenesisHasInputs);
91 }
92
93 errors.extend(Self::validate_topological_order(consignment));
95
96 errors.extend(Self::validate_seal_consumption(consignment));
98
99 errors.extend(Self::validate_state_refs(consignment));
101
102 errors.extend(Self::validate_anchor_commitment_binding(consignment));
104
105 if let Some(schema) = schema {
107 errors.extend(Self::validate_schema(consignment, schema));
108 }
109
110 RgbValidationResult {
111 is_valid: errors.is_empty(),
112 errors,
113 consignment_id,
114 contract_id,
115 }
116 }
117
118 fn compute_consignment_id(consignment: &Consignment) -> Hash {
120 use sha2::{Digest, Sha256};
121 let mut hasher = Sha256::new();
122 hasher.update([consignment.version]);
124 hasher.update(consignment.genesis.contract_id.as_bytes());
126 hasher.update(consignment.genesis.schema_id.as_bytes());
127 for tx in &consignment.transitions {
129 hasher.update(tx.transition_id.to_le_bytes());
130 for sig in &tx.signatures {
131 hasher.update(sig);
132 }
133 }
134 for anchor in &consignment.anchors {
136 hasher.update(anchor.commitment.as_bytes());
137 }
138 Hash::new(hasher.finalize().into())
139 }
140
141 fn compute_contract_id(genesis: &crate::genesis::Genesis) -> Hash {
143 genesis.contract_id
144 }
145
146 fn genesis_has_inputs(_consignment: &Consignment) -> bool {
148 false
151 }
152
153 fn validate_topological_order(consignment: &Consignment) -> Vec<RgbValidationError> {
155 let errors = Vec::new();
156
157 let mut state_producers: std::collections::HashMap<String, usize> =
159 std::collections::HashMap::new();
160
161 for (i, _assignment) in consignment.genesis.owned_state.iter().enumerate() {
163 let key = format!("genesis-{}", i);
164 state_producers.insert(key, 0);
165 }
166
167 for (tx_idx, tx) in consignment.transitions.iter().enumerate() {
169 for state_ref in &tx.owned_inputs {
170 let key = format!("{}-{}", state_ref.type_id, state_ref.commitment);
173 if !state_producers.contains_key(&key) && tx_idx > 0 {
174 }
177 }
178
179 for (out_idx, _assignment) in tx.owned_outputs.iter().enumerate() {
181 let key = format!("tx{}-{}", tx_idx, out_idx);
182 state_producers.insert(key, tx_idx + 1);
183 }
184 }
185
186 errors
187 }
188
189 fn validate_seal_consumption(consignment: &Consignment) -> Vec<RgbValidationError> {
191 let mut errors = Vec::new();
192 let mut seal_consumers: std::collections::HashMap<String, usize> =
193 std::collections::HashMap::new();
194
195 for (idx, assignment) in consignment.seal_assignments.iter().enumerate() {
197 let key = hex::encode(&assignment.seal_ref.seal_id);
198 if let Some(&first_idx) = seal_consumers.get(&key) {
199 errors.push(RgbValidationError::SealDoubleSpend {
200 seal_ref: assignment.seal_ref.clone(),
201 first_seen: first_idx,
202 second_seen: idx,
203 });
204 } else {
205 seal_consumers.insert(key, idx);
206 }
207 }
208
209 errors
210 }
211
212 fn validate_state_refs(_consignment: &Consignment) -> Vec<RgbValidationError> {
214 Vec::new()
216 }
217
218 fn validate_anchor_commitment_binding(_consignment: &Consignment) -> Vec<RgbValidationError> {
220 Vec::new()
223 }
224
225 fn validate_schema(consignment: &Consignment, schema: &Schema) -> Vec<RgbValidationError> {
227 let mut errors = Vec::new();
228
229 if consignment.schema_id != consignment.genesis.schema_id {
231 errors.push(RgbValidationError::SchemaValidationFailed {
232 transition_index: 0,
233 error: "Schema ID mismatch between consignment and genesis".to_string(),
234 });
235 }
236
237 for (idx, tx) in consignment.transitions.iter().enumerate() {
239 if let Err(e) = schema.validate_transition(tx) {
240 errors.push(RgbValidationError::SchemaValidationFailed {
241 transition_index: idx,
242 error: e.to_string(),
243 });
244 }
245 }
246
247 errors
248 }
249}
250
251pub struct RgbTapretVerifier;
253
254impl RgbTapretVerifier {
255 pub fn verify_tapret_commitment(
262 tapret_root: [u8; 32],
263 protocol_id: [u8; 32],
264 #[allow(unused_variables)] commitment: Hash,
265 control_block: Option<Vec<u8>>,
266 ) -> bool {
267 if tapret_root == [0u8; 32] || protocol_id == [0u8; 32] {
269 return false;
270 }
271
272 #[cfg(feature = "tapret")]
273 {
274 let expected_tapret =
277 tapret_verify::compute_tap_tweak_hash(protocol_id, Some(tapret_root));
278 if expected_tapret != tapret_root {
279 let opreturn_data: Vec<u8> = protocol_id[..4]
281 .iter()
282 .copied()
283 .chain(commitment.as_bytes().iter().copied())
284 .collect();
285 if !Self::verify_opreturn_commitment(&opreturn_data, protocol_id, commitment) {
286 return false;
287 }
288 }
289 }
290
291 if let Some(cb) = control_block {
293 if cb.len() < 33 {
295 return false;
296 }
297 if cb.len() >= 64 && cb[1..33] != tapret_root {
299 return false;
300 }
301 }
302
303 true
304 }
305
306 pub fn verify_opreturn_commitment(
308 opreturn_data: &[u8],
309 protocol_id: [u8; 32],
310 commitment: Hash,
311 ) -> bool {
312 if opreturn_data.len() < 36 {
314 return false;
315 }
316 if opreturn_data[..4] != protocol_id[..4] {
318 return false;
319 }
320 opreturn_data[4..36] == *commitment.as_bytes()
322 }
323}
324
325pub struct CrossChainValidator;
327
328impl CrossChainValidator {
329 pub fn validate_cross_chain_consistency(anchors: &[Anchor]) -> Result<(), CrossChainError> {
334 if anchors.is_empty() {
335 return Ok(());
336 }
337
338 let first_commitment = anchors[0].commitment;
340 for (i, anchor) in anchors.iter().enumerate().skip(1) {
341 if anchor.commitment != first_commitment {
342 return Err(CrossChainError::CommitmentMismatch {
343 anchor_index: i,
344 expected: first_commitment,
345 actual: anchor.commitment,
346 });
347 }
348 }
349
350 Ok(())
351 }
352}
353
354#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
356#[allow(missing_docs)]
357pub enum CrossChainError {
358 CommitmentMismatch {
360 anchor_index: usize,
361 expected: Hash,
362 actual: Hash,
363 },
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::consignment::Anchor;
370 use crate::genesis::Genesis;
371 use crate::seal::{AnchorRef, SealRef};
372 use crate::state::StateAssignment;
373
374 fn mock_consignment() -> Consignment {
375 Consignment {
376 version: 1,
377 genesis: Genesis {
378 contract_id: Hash::new([0x01; 32]),
379 schema_id: Hash::new([0x02; 32]),
380 global_state: vec![],
381 owned_state: vec![],
382 metadata: vec![],
383 },
384 transitions: vec![],
385 seal_assignments: vec![],
386 anchors: vec![],
387 schema_id: Hash::new([0x02; 32]),
388 }
389 }
390
391 #[test]
392 fn test_rgb_validation_empty_consignment() {
393 let consignment = mock_consignment();
394 let result = RgbConsignmentValidator::validate(&consignment, None);
395 assert!(result.is_valid);
396 assert!(result.errors.is_empty());
397 }
398
399 #[test]
400 fn test_consignment_id_computation() {
401 let consignment = mock_consignment();
402 let id = RgbConsignmentValidator::compute_consignment_id(&consignment);
403 assert_ne!(id.as_bytes(), &[0u8; 32]);
405 }
406
407 #[test]
408 fn test_contract_id_from_genesis() {
409 let consignment = mock_consignment();
410 let contract_id = RgbConsignmentValidator::compute_contract_id(&consignment.genesis);
411 assert_eq!(contract_id, Hash::new([0x01; 32]));
412 }
413
414 #[test]
415 fn test_seal_double_spend_detection() {
416 let mut consignment = mock_consignment();
417
418 let seal = SealRef::new(vec![0xAB; 32], Some(0)).unwrap();
420 let assignment = crate::consignment::SealAssignment::new(
421 seal.clone(),
422 StateAssignment::new(0, seal.clone(), vec![]),
423 vec![],
424 );
425 consignment.seal_assignments.push(assignment.clone());
426 consignment.seal_assignments.push(assignment);
427
428 let result = RgbConsignmentValidator::validate(&consignment, None);
429 assert!(!result.is_valid);
430 assert!(result
431 .errors
432 .iter()
433 .any(|e| matches!(e, RgbValidationError::SealDoubleSpend { .. })));
434 }
435
436 #[test]
437 fn test_tapret_commitment_verification() {
438 let tapret_root = [0x01; 32];
439 let protocol_id = [0x02; 32];
440 let commitment = Hash::new([0x03; 32]);
441
442 assert!(RgbTapretVerifier::verify_tapret_commitment(
443 tapret_root,
444 protocol_id,
445 commitment,
446 None
447 ));
448 }
449
450 #[test]
451 fn test_opreturn_commitment_verification() {
452 let protocol_id: [u8; 32] = [
453 0x01, 0x02, 0x03, 0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
454 0, 0, 0, 0, 0, 0, 0,
455 ];
456 let commitment = Hash::new([0xAB; 32]);
457
458 let mut opreturn_data = vec![0u8; 36];
459 opreturn_data[..4].copy_from_slice(&protocol_id[..4]);
460 opreturn_data[4..].copy_from_slice(commitment.as_bytes());
461
462 assert!(RgbTapretVerifier::verify_opreturn_commitment(
463 &opreturn_data,
464 protocol_id,
465 commitment
466 ));
467 }
468
469 #[test]
470 fn test_opreturn_wrong_protocol() {
471 let protocol_id: [u8; 32] = [
472 0x01, 0x02, 0x03, 0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
473 0, 0, 0, 0, 0, 0, 0,
474 ];
475 let commitment = Hash::new([0xAB; 32]);
476
477 let mut opreturn_data = vec![0u8; 36];
478 opreturn_data[..4].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
479 opreturn_data[4..].copy_from_slice(commitment.as_bytes());
480
481 assert!(!RgbTapretVerifier::verify_opreturn_commitment(
482 &opreturn_data,
483 protocol_id,
484 commitment
485 ));
486 }
487
488 #[test]
489 fn test_cross_chain_consistency_valid() {
490 let anchors = vec![
491 Anchor::new(
492 AnchorRef::new(vec![0x01; 32], 100, vec![]).unwrap(),
493 Hash::new([0xAB; 32]),
494 vec![],
495 vec![],
496 ),
497 Anchor::new(
498 AnchorRef::new(vec![0x02; 32], 200, vec![]).unwrap(),
499 Hash::new([0xAB; 32]),
500 vec![],
501 vec![],
502 ),
503 ];
504
505 assert!(CrossChainValidator::validate_cross_chain_consistency(&anchors).is_ok());
506 }
507
508 #[test]
509 fn test_cross_chain_consistency_mismatch() {
510 let anchors = vec![
511 Anchor::new(
512 AnchorRef::new(vec![0x01; 32], 100, vec![]).unwrap(),
513 Hash::new([0xAB; 32]),
514 vec![],
515 vec![],
516 ),
517 Anchor::new(
518 AnchorRef::new(vec![0x02; 32], 200, vec![]).unwrap(),
519 Hash::new([0xCD; 32]), vec![],
521 vec![],
522 ),
523 ];
524
525 let result = CrossChainValidator::validate_cross_chain_consistency(&anchors);
526 assert!(result.is_err());
527 }
528}