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 =
277                tapret_verify::compute_tap_tweak_hash(protocol_id, Some(tapret_root));
278            if expected_tapret != tapret_root {
279                // The tapret_root should contain the commitment. Verify via OP_RETURN fallback.
280                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 a control block is provided, verify its structure
292        if let Some(cb) = control_block {
293            // Control block must be at least 33 bytes (internal key) + 32 bytes (merkle path per level)
294            if cb.len() < 33 {
295                return false;
296            }
297            // First 32 bytes of control block should match the tapret root
298            if cb.len() >= 64 && cb[1..33] != tapret_root {
299                return false;
300            }
301        }
302
303        true
304    }
305
306    /// Verify an OP_RETURN commitment (RGB fallback)
307    pub fn verify_opreturn_commitment(
308        opreturn_data: &[u8],
309        protocol_id: [u8; 32],
310        commitment: Hash,
311    ) -> bool {
312        // OP_RETURN format: [protocol_id (4 bytes)] [commitment hash (32 bytes)]
313        if opreturn_data.len() < 36 {
314            return false;
315        }
316        // Check protocol ID prefix
317        if opreturn_data[..4] != protocol_id[..4] {
318            return false;
319        }
320        // Check commitment hash
321        opreturn_data[4..36] == *commitment.as_bytes()
322    }
323}
324
325/// Cross-chain consignment validator
326pub struct CrossChainValidator;
327
328impl CrossChainValidator {
329    /// Validate a consignment that spans multiple chains
330    ///
331    /// Ensures that commitments are consistent across all chains
332    /// and that each chain's proof is valid.
333    pub fn validate_cross_chain_consistency(anchors: &[Anchor]) -> Result<(), CrossChainError> {
334        if anchors.is_empty() {
335            return Ok(());
336        }
337
338        // All anchors should have the same commitment hash
339        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/// Cross-chain validation error
355#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
356#[allow(missing_docs)]
357pub enum CrossChainError {
358    /// Commitment hash doesn't match between source and destination chains
359    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        // ID should be non-zero
404        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        // Add duplicate seal assignments
419        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]), // Different commitment
520                vec![],
521                vec![],
522            ),
523        ];
524
525        let result = CrossChainValidator::validate_cross_chain_consistency(&anchors);
526        assert!(result.is_err());
527    }
528}