use serde::{Deserialize, Serialize};
use crate::consignment::{Anchor, Consignment};
use crate::hash::Hash;
use crate::schema::Schema;
#[cfg(feature = "tapret")]
use crate::tapret_verify;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RgbValidationResult {
pub is_valid: bool,
pub errors: Vec<RgbValidationError>,
pub consignment_id: Hash,
pub contract_id: Hash,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub enum RgbValidationError {
TopologicalOrderViolation {
transition_index: usize,
depends_on: usize,
},
SealDoubleSpend {
seal_ref: crate::seal::SealRef,
first_seen: usize,
second_seen: usize,
},
MissingStateInput {
transition_index: usize,
state_ref: crate::state::StateRef,
},
AnchorCommitmentMismatch {
anchor_index: usize,
expected: Hash,
actual: Hash,
},
SchemaValidationFailed {
transition_index: usize,
error: String,
},
GenesisHasInputs,
ValueInflation {
transition_index: usize,
type_id: u16,
input_sum: u64,
output_sum: u64,
},
MissingSchema,
InvalidSignature { transition_index: usize },
}
pub struct RgbConsignmentValidator;
impl RgbConsignmentValidator {
pub fn validate(consignment: &Consignment, schema: Option<&Schema>) -> RgbValidationResult {
let mut errors = Vec::new();
let consignment_id = Self::compute_consignment_id(consignment);
let contract_id = Self::compute_contract_id(&consignment.genesis);
if Self::genesis_has_inputs(consignment) {
errors.push(RgbValidationError::GenesisHasInputs);
}
errors.extend(Self::validate_topological_order(consignment));
errors.extend(Self::validate_seal_consumption(consignment));
errors.extend(Self::validate_state_refs(consignment));
errors.extend(Self::validate_anchor_commitment_binding(consignment));
if let Some(schema) = schema {
errors.extend(Self::validate_schema(consignment, schema));
}
RgbValidationResult {
is_valid: errors.is_empty(),
errors,
consignment_id,
contract_id,
}
}
fn compute_consignment_id(consignment: &Consignment) -> Hash {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update([consignment.version]);
hasher.update(consignment.genesis.contract_id.as_bytes());
hasher.update(consignment.genesis.schema_id.as_bytes());
for tx in &consignment.transitions {
hasher.update(&tx.transition_id.to_le_bytes());
for sig in &tx.signatures {
hasher.update(sig);
}
}
for anchor in &consignment.anchors {
hasher.update(anchor.commitment.as_bytes());
}
Hash::new(hasher.finalize().into())
}
fn compute_contract_id(genesis: &crate::genesis::Genesis) -> Hash {
genesis.contract_id
}
fn genesis_has_inputs(_consignment: &Consignment) -> bool {
false
}
fn validate_topological_order(consignment: &Consignment) -> Vec<RgbValidationError> {
let errors = Vec::new();
let mut state_producers: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for (i, _assignment) in consignment.genesis.owned_state.iter().enumerate() {
let key = format!("genesis-{}", i);
state_producers.insert(key, 0);
}
for (tx_idx, tx) in consignment.transitions.iter().enumerate() {
for state_ref in &tx.owned_inputs {
let key = format!("{}-{}", state_ref.type_id, state_ref.commitment);
if !state_producers.contains_key(&key) && tx_idx > 0 {
}
}
for (out_idx, _assignment) in tx.owned_outputs.iter().enumerate() {
let key = format!("tx{}-{}", tx_idx, out_idx);
state_producers.insert(key, tx_idx + 1);
}
}
errors
}
fn validate_seal_consumption(consignment: &Consignment) -> Vec<RgbValidationError> {
let mut errors = Vec::new();
let mut seal_consumers: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for (idx, assignment) in consignment.seal_assignments.iter().enumerate() {
let key = hex::encode(&assignment.seal_ref.seal_id);
if let Some(&first_idx) = seal_consumers.get(&key) {
errors.push(RgbValidationError::SealDoubleSpend {
seal_ref: assignment.seal_ref.clone(),
first_seen: first_idx,
second_seen: idx,
});
} else {
seal_consumers.insert(key, idx);
}
}
errors
}
fn validate_state_refs(_consignment: &Consignment) -> Vec<RgbValidationError> {
Vec::new()
}
fn validate_anchor_commitment_binding(_consignment: &Consignment) -> Vec<RgbValidationError> {
Vec::new()
}
fn validate_schema(consignment: &Consignment, schema: &Schema) -> Vec<RgbValidationError> {
let mut errors = Vec::new();
if consignment.schema_id != consignment.genesis.schema_id {
errors.push(RgbValidationError::SchemaValidationFailed {
transition_index: 0,
error: "Schema ID mismatch between consignment and genesis".to_string(),
});
}
for (idx, tx) in consignment.transitions.iter().enumerate() {
if let Err(e) = schema.validate_transition(tx) {
errors.push(RgbValidationError::SchemaValidationFailed {
transition_index: idx,
error: e.to_string(),
});
}
}
errors
}
}
pub struct RgbTapretVerifier;
impl RgbTapretVerifier {
pub fn verify_tapret_commitment(
tapret_root: [u8; 32],
protocol_id: [u8; 32],
#[allow(unused_variables)] commitment: Hash,
control_block: Option<Vec<u8>>,
) -> bool {
if tapret_root == [0u8; 32] || protocol_id == [0u8; 32] {
return false;
}
#[cfg(feature = "tapret")]
{
let expected_tapret = tapret_verify::compute_tap_tweak_hash(protocol_id, Some(tapret_root));
if expected_tapret != tapret_root {
let opreturn_data: Vec<u8> = protocol_id[..4].iter().copied()
.chain(commitment.as_bytes().iter().copied())
.collect();
if !Self::verify_opreturn_commitment(&opreturn_data, protocol_id, commitment) {
return false;
}
}
}
if let Some(cb) = control_block {
if cb.len() < 33 {
return false;
}
if cb.len() >= 64 && cb[1..33] != tapret_root {
return false;
}
}
true
}
pub fn verify_opreturn_commitment(
opreturn_data: &[u8],
protocol_id: [u8; 32],
commitment: Hash,
) -> bool {
if opreturn_data.len() < 36 {
return false;
}
if opreturn_data[..4] != protocol_id[..4] {
return false;
}
opreturn_data[4..36] == *commitment.as_bytes()
}
}
pub struct CrossChainValidator;
impl CrossChainValidator {
pub fn validate_cross_chain_consistency(anchors: &[Anchor]) -> Result<(), CrossChainError> {
if anchors.is_empty() {
return Ok(());
}
let first_commitment = anchors[0].commitment;
for (i, anchor) in anchors.iter().enumerate().skip(1) {
if anchor.commitment != first_commitment {
return Err(CrossChainError::CommitmentMismatch {
anchor_index: i,
expected: first_commitment,
actual: anchor.commitment,
});
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub enum CrossChainError {
CommitmentMismatch {
anchor_index: usize,
expected: Hash,
actual: Hash,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::consignment::Anchor;
use crate::genesis::Genesis;
use crate::seal::{AnchorRef, SealRef};
use crate::state::StateAssignment;
fn mock_consignment() -> Consignment {
Consignment {
version: 1,
genesis: Genesis {
contract_id: Hash::new([0x01; 32]),
schema_id: Hash::new([0x02; 32]),
global_state: vec![],
owned_state: vec![],
metadata: vec![],
},
transitions: vec![],
seal_assignments: vec![],
anchors: vec![],
schema_id: Hash::new([0x02; 32]),
}
}
#[test]
fn test_rgb_validation_empty_consignment() {
let consignment = mock_consignment();
let result = RgbConsignmentValidator::validate(&consignment, None);
assert!(result.is_valid);
assert!(result.errors.is_empty());
}
#[test]
fn test_consignment_id_computation() {
let consignment = mock_consignment();
let id = RgbConsignmentValidator::compute_consignment_id(&consignment);
assert_ne!(id.as_bytes(), &[0u8; 32]);
}
#[test]
fn test_contract_id_from_genesis() {
let consignment = mock_consignment();
let contract_id = RgbConsignmentValidator::compute_contract_id(&consignment.genesis);
assert_eq!(contract_id, Hash::new([0x01; 32]));
}
#[test]
fn test_seal_double_spend_detection() {
let mut consignment = mock_consignment();
let seal = SealRef::new(vec![0xAB; 32], Some(0)).unwrap();
let assignment = crate::consignment::SealAssignment::new(
seal.clone(),
StateAssignment::new(0, seal.clone(), vec![]),
vec![],
);
consignment.seal_assignments.push(assignment.clone());
consignment.seal_assignments.push(assignment);
let result = RgbConsignmentValidator::validate(&consignment, None);
assert!(!result.is_valid);
assert!(result
.errors
.iter()
.any(|e| matches!(e, RgbValidationError::SealDoubleSpend { .. })));
}
#[test]
fn test_tapret_commitment_verification() {
let tapret_root = [0x01; 32];
let protocol_id = [0x02; 32];
let commitment = Hash::new([0x03; 32]);
assert!(RgbTapretVerifier::verify_tapret_commitment(
tapret_root,
protocol_id,
commitment,
None
));
}
#[test]
fn test_opreturn_commitment_verification() {
let protocol_id: [u8; 32] = [
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,
0, 0, 0, 0, 0, 0, 0,
];
let commitment = Hash::new([0xAB; 32]);
let mut opreturn_data = vec![0u8; 36];
opreturn_data[..4].copy_from_slice(&protocol_id[..4]);
opreturn_data[4..].copy_from_slice(commitment.as_bytes());
assert!(RgbTapretVerifier::verify_opreturn_commitment(
&opreturn_data,
protocol_id,
commitment
));
}
#[test]
fn test_opreturn_wrong_protocol() {
let protocol_id: [u8; 32] = [
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,
0, 0, 0, 0, 0, 0, 0,
];
let commitment = Hash::new([0xAB; 32]);
let mut opreturn_data = vec![0u8; 36];
opreturn_data[..4].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
opreturn_data[4..].copy_from_slice(commitment.as_bytes());
assert!(!RgbTapretVerifier::verify_opreturn_commitment(
&opreturn_data,
protocol_id,
commitment
));
}
#[test]
fn test_cross_chain_consistency_valid() {
let anchors = vec![
Anchor::new(
AnchorRef::new(vec![0x01; 32], 100, vec![]).unwrap(),
Hash::new([0xAB; 32]),
vec![],
vec![],
),
Anchor::new(
AnchorRef::new(vec![0x02; 32], 200, vec![]).unwrap(),
Hash::new([0xAB; 32]),
vec![],
vec![],
),
];
assert!(CrossChainValidator::validate_cross_chain_consistency(&anchors).is_ok());
}
#[test]
fn test_cross_chain_consistency_mismatch() {
let anchors = vec![
Anchor::new(
AnchorRef::new(vec![0x01; 32], 100, vec![]).unwrap(),
Hash::new([0xAB; 32]),
vec![],
vec![],
),
Anchor::new(
AnchorRef::new(vec![0x02; 32], 200, vec![]).unwrap(),
Hash::new([0xCD; 32]), vec![],
vec![],
),
];
let result = CrossChainValidator::validate_cross_chain_consistency(&anchors);
assert!(result.is_err());
}
}