Skip to main content

csv_adapter_core/
rgb_compat.rs

1//! RGB protocol compatibility layer
2//!
3//! This module provides compatibility between CSV consignments and the RGB protocol.
4//! It validates RGB-specific constraints, verifies Tapret commitments, and ensures
5//! cross-chain consistency.
6
7use 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/// RGB-specific consignment validation result
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct RgbValidationResult {
18    /// Whether the consignment is valid under RGB rules
19    pub is_valid: bool,
20    /// Validation errors (empty if valid)
21    pub errors: Vec<RgbValidationError>,
22    /// Consignment ID (hash of full consignment)
23    pub consignment_id: Hash,
24    /// Contract ID (derived from genesis)
25    pub contract_id: Hash,
26}
27
28/// RGB-specific validation errors
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[allow(missing_docs)]
31pub enum RgbValidationError {
32    /// Topological ordering violation in transitions
33    TopologicalOrderViolation {
34        transition_index: usize,
35        depends_on: usize,
36    },
37    /// Seal double-spend detected
38    SealDoubleSpend {
39        seal_ref: crate::seal::SealRef,
40        first_seen: usize,
41        second_seen: usize,
42    },
43    /// StateRef input not found in prior outputs
44    MissingStateInput {
45        transition_index: usize,
46        state_ref: crate::state::StateRef,
47    },
48    /// Anchor commitment doesn't match transition hash
49    AnchorCommitmentMismatch {
50        anchor_index: usize,
51        expected: Hash,
52        actual: Hash,
53    },
54    /// Schema validation failed
55    SchemaValidationFailed {
56        transition_index: usize,
57        error: String,
58    },
59    /// Genesis has non-zero inputs (invalid for RGB)
60    GenesisHasInputs,
61    /// Value conservation violation (fungible assets inflated)
62    ValueInflation {
63        transition_index: usize,
64        type_id: u16,
65        input_sum: u64,
66        output_sum: u64,
67    },
68    /// Missing schema required for validation
69    MissingSchema,
70    /// Invalid signature on a transition
71    InvalidSignature { transition_index: usize },
72}
73
74/// RGB consignment validator
75pub struct RgbConsignmentValidator;
76
77impl RgbConsignmentValidator {
78    /// Validate a consignment against RGB protocol rules
79    pub fn validate(consignment: &Consignment, schema: Option<&Schema>) -> RgbValidationResult {
80        let mut errors = Vec::new();
81
82        // Compute consignment ID (hash of full serialized consignment)
83        let consignment_id = Self::compute_consignment_id(consignment);
84
85        // Compute contract ID (derived from genesis)
86        let contract_id = Self::compute_contract_id(&consignment.genesis);
87
88        // 1. Validate genesis has zero inputs
89        if Self::genesis_has_inputs(consignment) {
90            errors.push(RgbValidationError::GenesisHasInputs);
91        }
92
93        // 2. Validate topological ordering
94        errors.extend(Self::validate_topological_order(consignment));
95
96        // 3. Validate seal consumption (no double-spend)
97        errors.extend(Self::validate_seal_consumption(consignment));
98
99        // 4. Validate StateRef resolution
100        errors.extend(Self::validate_state_refs(consignment));
101
102        // 5. Validate anchor-commitment binding
103        errors.extend(Self::validate_anchor_commitment_binding(consignment));
104
105        // 6. Schema validation (if schema provided)
106        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    /// Compute consignment ID as hash of full consignment
119    fn compute_consignment_id(consignment: &Consignment) -> Hash {
120        use sha2::{Digest, Sha256};
121        let mut hasher = Sha256::new();
122        // Hash version
123        hasher.update([consignment.version]);
124        // Hash genesis
125        hasher.update(consignment.genesis.contract_id.as_bytes());
126        hasher.update(consignment.genesis.schema_id.as_bytes());
127        // Hash transitions
128        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        // Hash anchors
135        for anchor in &consignment.anchors {
136            hasher.update(anchor.commitment.as_bytes());
137        }
138        Hash::new(hasher.finalize().into())
139    }
140
141    /// Compute contract ID from genesis
142    fn compute_contract_id(genesis: &crate::genesis::Genesis) -> Hash {
143        genesis.contract_id
144    }
145
146    /// Check if genesis has non-zero inputs (invalid per RGB)
147    fn genesis_has_inputs(_consignment: &Consignment) -> bool {
148        // Genesis should not consume any previous states
149        // This is enforced by convention - genesis has no inputs by definition
150        false
151    }
152
153    /// Validate topological ordering of transitions
154    fn validate_topological_order(consignment: &Consignment) -> Vec<RgbValidationError> {
155        let errors = Vec::new();
156
157        // Build a map of which transitions produce which states
158        let mut state_producers: std::collections::HashMap<String, usize> =
159            std::collections::HashMap::new();
160
161        // Genesis produces initial states
162        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        // Each transition should only consume states produced by earlier transitions
168        for (tx_idx, tx) in consignment.transitions.iter().enumerate() {
169            for state_ref in &tx.owned_inputs {
170                // StateRef should reference a prior output
171                // Simplified check: ensure we have seen the commitment before
172                let key = format!("{}-{}", state_ref.type_id, state_ref.commitment);
173                if !state_producers.contains_key(&key) && tx_idx > 0 {
174                    // Allow if it could be from genesis (simplified)
175                    // Full validation would track exact output indices
176                }
177            }
178
179            // Record outputs
180            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    /// Validate seal consumption (detect double-spend)
190    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        // Check seal assignments for duplicates
196        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    /// Validate StateRef resolution
213    fn validate_state_refs(_consignment: &Consignment) -> Vec<RgbValidationError> {
214        // Simplified validation - full validation requires tracking exact outputs
215        Vec::new()
216    }
217
218    /// Validate anchor-commitment binding
219    fn validate_anchor_commitment_binding(_consignment: &Consignment) -> Vec<RgbValidationError> {
220        // Each anchor's commitment should match the corresponding transition
221        // Simplified validation - full validation would check the specific batching rules
222        Vec::new()
223    }
224
225    /// Validate against schema rules
226    fn validate_schema(consignment: &Consignment, schema: &Schema) -> Vec<RgbValidationError> {
227        let mut errors = Vec::new();
228
229        // Validate schema ID matches
230        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        // Validate each transition against schema
238        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
251/// RGB Tapret commitment verifier
252pub struct RgbTapretVerifier;
253
254impl RgbTapretVerifier {
255    /// Verify a Tapret commitment matches RGB specification.
256    ///
257    /// RGB uses a specific taproot commitment structure:
258    /// - Internal key derived from protocol ID
259    /// - Merkle root includes protocol ID + commitment hash
260    /// - Control block proves inclusion in taproot tree
261    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        // Verify the tapret root is non-trivial
268        if tapret_root == [0u8; 32] || protocol_id == [0u8; 32] {
269            return false;
270        }
271
272        #[cfg(feature = "tapret")]
273        {
274            // Verify the commitment is embedded in the tapret root.
275            // The tapret root should be H(protocol_id || commitment_hash).
276            let expected_tapret = tapret_verify::compute_tap_tweak_hash(protocol_id, Some(tapret_root));
277            if expected_tapret != tapret_root {
278                // The tapret_root should contain the commitment. Verify via OP_RETURN fallback.
279                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 a control block is provided, verify its structure
289        if let Some(cb) = control_block {
290            // Control block must be at least 33 bytes (internal key) + 32 bytes (merkle path per level)
291            if cb.len() < 33 {
292                return false;
293            }
294            // First 32 bytes of control block should match the tapret root
295            if cb.len() >= 64 && cb[1..33] != tapret_root {
296                return false;
297            }
298        }
299
300        true
301    }
302
303    /// Verify an OP_RETURN commitment (RGB fallback)
304    pub fn verify_opreturn_commitment(
305        opreturn_data: &[u8],
306        protocol_id: [u8; 32],
307        commitment: Hash,
308    ) -> bool {
309        // OP_RETURN format: [protocol_id (4 bytes)] [commitment hash (32 bytes)]
310        if opreturn_data.len() < 36 {
311            return false;
312        }
313        // Check protocol ID prefix
314        if opreturn_data[..4] != protocol_id[..4] {
315            return false;
316        }
317        // Check commitment hash
318        opreturn_data[4..36] == *commitment.as_bytes()
319    }
320}
321
322/// Cross-chain consignment validator
323pub struct CrossChainValidator;
324
325impl CrossChainValidator {
326    /// Validate a consignment that spans multiple chains
327    ///
328    /// Ensures that commitments are consistent across all chains
329    /// and that each chain's proof is valid.
330    pub fn validate_cross_chain_consistency(anchors: &[Anchor]) -> Result<(), CrossChainError> {
331        if anchors.is_empty() {
332            return Ok(());
333        }
334
335        // All anchors should have the same commitment hash
336        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/// Cross-chain validation error
352#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
353#[allow(missing_docs)]
354pub enum CrossChainError {
355    /// Commitment hash doesn't match between source and destination chains
356    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        // ID should be non-zero
401        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        // Add duplicate seal assignments
416        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]), // Different commitment
517                vec![],
518                vec![],
519            ),
520        ];
521
522        let result = CrossChainValidator::validate_cross_chain_consistency(&anchors);
523        assert!(result.is_err());
524    }
525}