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 = tapret_verify::compute_tap_tweak_hash(protocol_id, Some(tapret_root));
277 if expected_tapret != tapret_root {
278 let opreturn_data: Vec<u8> = protocol_id[..4].iter().copied()
280 .chain(commitment.as_bytes().iter().copied())
281 .collect();
282 if !Self::verify_opreturn_commitment(&opreturn_data, protocol_id, commitment) {
283 return false;
284 }
285 }
286 }
287
288 if let Some(cb) = control_block {
290 if cb.len() < 33 {
292 return false;
293 }
294 if cb.len() >= 64 && cb[1..33] != tapret_root {
296 return false;
297 }
298 }
299
300 true
301 }
302
303 pub fn verify_opreturn_commitment(
305 opreturn_data: &[u8],
306 protocol_id: [u8; 32],
307 commitment: Hash,
308 ) -> bool {
309 if opreturn_data.len() < 36 {
311 return false;
312 }
313 if opreturn_data[..4] != protocol_id[..4] {
315 return false;
316 }
317 opreturn_data[4..36] == *commitment.as_bytes()
319 }
320}
321
322pub struct CrossChainValidator;
324
325impl CrossChainValidator {
326 pub fn validate_cross_chain_consistency(anchors: &[Anchor]) -> Result<(), CrossChainError> {
331 if anchors.is_empty() {
332 return Ok(());
333 }
334
335 let first_commitment = anchors[0].commitment;
337 for (i, anchor) in anchors.iter().enumerate().skip(1) {
338 if anchor.commitment != first_commitment {
339 return Err(CrossChainError::CommitmentMismatch {
340 anchor_index: i,
341 expected: first_commitment,
342 actual: anchor.commitment,
343 });
344 }
345 }
346
347 Ok(())
348 }
349}
350
351#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
353#[allow(missing_docs)]
354pub enum CrossChainError {
355 CommitmentMismatch {
357 anchor_index: usize,
358 expected: Hash,
359 actual: Hash,
360 },
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::consignment::Anchor;
367 use crate::genesis::Genesis;
368 use crate::seal::{AnchorRef, SealRef};
369 use crate::state::StateAssignment;
370
371 fn mock_consignment() -> Consignment {
372 Consignment {
373 version: 1,
374 genesis: Genesis {
375 contract_id: Hash::new([0x01; 32]),
376 schema_id: Hash::new([0x02; 32]),
377 global_state: vec![],
378 owned_state: vec![],
379 metadata: vec![],
380 },
381 transitions: vec![],
382 seal_assignments: vec![],
383 anchors: vec![],
384 schema_id: Hash::new([0x02; 32]),
385 }
386 }
387
388 #[test]
389 fn test_rgb_validation_empty_consignment() {
390 let consignment = mock_consignment();
391 let result = RgbConsignmentValidator::validate(&consignment, None);
392 assert!(result.is_valid);
393 assert!(result.errors.is_empty());
394 }
395
396 #[test]
397 fn test_consignment_id_computation() {
398 let consignment = mock_consignment();
399 let id = RgbConsignmentValidator::compute_consignment_id(&consignment);
400 assert_ne!(id.as_bytes(), &[0u8; 32]);
402 }
403
404 #[test]
405 fn test_contract_id_from_genesis() {
406 let consignment = mock_consignment();
407 let contract_id = RgbConsignmentValidator::compute_contract_id(&consignment.genesis);
408 assert_eq!(contract_id, Hash::new([0x01; 32]));
409 }
410
411 #[test]
412 fn test_seal_double_spend_detection() {
413 let mut consignment = mock_consignment();
414
415 let seal = SealRef::new(vec![0xAB; 32], Some(0)).unwrap();
417 let assignment = crate::consignment::SealAssignment::new(
418 seal.clone(),
419 StateAssignment::new(0, seal.clone(), vec![]),
420 vec![],
421 );
422 consignment.seal_assignments.push(assignment.clone());
423 consignment.seal_assignments.push(assignment);
424
425 let result = RgbConsignmentValidator::validate(&consignment, None);
426 assert!(!result.is_valid);
427 assert!(result
428 .errors
429 .iter()
430 .any(|e| matches!(e, RgbValidationError::SealDoubleSpend { .. })));
431 }
432
433 #[test]
434 fn test_tapret_commitment_verification() {
435 let tapret_root = [0x01; 32];
436 let protocol_id = [0x02; 32];
437 let commitment = Hash::new([0x03; 32]);
438
439 assert!(RgbTapretVerifier::verify_tapret_commitment(
440 tapret_root,
441 protocol_id,
442 commitment,
443 None
444 ));
445 }
446
447 #[test]
448 fn test_opreturn_commitment_verification() {
449 let protocol_id: [u8; 32] = [
450 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,
451 0, 0, 0, 0, 0, 0, 0,
452 ];
453 let commitment = Hash::new([0xAB; 32]);
454
455 let mut opreturn_data = vec![0u8; 36];
456 opreturn_data[..4].copy_from_slice(&protocol_id[..4]);
457 opreturn_data[4..].copy_from_slice(commitment.as_bytes());
458
459 assert!(RgbTapretVerifier::verify_opreturn_commitment(
460 &opreturn_data,
461 protocol_id,
462 commitment
463 ));
464 }
465
466 #[test]
467 fn test_opreturn_wrong_protocol() {
468 let protocol_id: [u8; 32] = [
469 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,
470 0, 0, 0, 0, 0, 0, 0,
471 ];
472 let commitment = Hash::new([0xAB; 32]);
473
474 let mut opreturn_data = vec![0u8; 36];
475 opreturn_data[..4].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
476 opreturn_data[4..].copy_from_slice(commitment.as_bytes());
477
478 assert!(!RgbTapretVerifier::verify_opreturn_commitment(
479 &opreturn_data,
480 protocol_id,
481 commitment
482 ));
483 }
484
485 #[test]
486 fn test_cross_chain_consistency_valid() {
487 let anchors = vec![
488 Anchor::new(
489 AnchorRef::new(vec![0x01; 32], 100, vec![]).unwrap(),
490 Hash::new([0xAB; 32]),
491 vec![],
492 vec![],
493 ),
494 Anchor::new(
495 AnchorRef::new(vec![0x02; 32], 200, vec![]).unwrap(),
496 Hash::new([0xAB; 32]),
497 vec![],
498 vec![],
499 ),
500 ];
501
502 assert!(CrossChainValidator::validate_cross_chain_consistency(&anchors).is_ok());
503 }
504
505 #[test]
506 fn test_cross_chain_consistency_mismatch() {
507 let anchors = vec![
508 Anchor::new(
509 AnchorRef::new(vec![0x01; 32], 100, vec![]).unwrap(),
510 Hash::new([0xAB; 32]),
511 vec![],
512 vec![],
513 ),
514 Anchor::new(
515 AnchorRef::new(vec![0x02; 32], 200, vec![]).unwrap(),
516 Hash::new([0xCD; 32]), vec![],
518 vec![],
519 ),
520 ];
521
522 let result = CrossChainValidator::validate_cross_chain_consistency(&anchors);
523 assert!(result.is_err());
524 }
525}