chio-web3 0.1.2

Chio web3 settlement, anchoring, and contract-surface artifacts
Documentation
use std::collections::HashSet;

use serde::{Deserialize, Serialize};

use crate::error::Web3ContractError;
use crate::identity::{validate_web3_identity_binding, SignedWeb3IdentityBinding};
use crate::validation::{ensure_non_empty, ensure_unique_strings};

pub const CHIO_WEB3_TRUST_PROFILE_SCHEMA: &str = "chio.web3-trust-profile.v1";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Web3SettlementPath {
    DualSignature,
    MerkleProof,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Web3DisputePolicy {
    OffChainArbitration,
    TimeoutRefund,
    BondSlash,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Web3FinalityMode {
    OptimisticL2,
    L1Finalized,
    SolanaConfirmed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Web3RegulatedRole {
    Operator,
    Custodian,
    PaymentInstitution,
    OracleOperator,
    Arbitrator,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3DisputeWindow {
    pub settlement_path: Web3SettlementPath,
    pub challenge_window_secs: u64,
    pub recovery_window_secs: u64,
    pub dispute_policy: Web3DisputePolicy,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3ChainFinalityRule {
    pub chain_id: String,
    pub mode: Web3FinalityMode,
    pub min_confirmations: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3RegulatedRoleAssumption {
    pub role: Web3RegulatedRole,
    pub actor_id: String,
    pub responsibility: String,
    pub custody_boundary_explicit: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3TrustProfile {
    pub schema: String,
    pub profile_id: String,
    pub chio_contract_version: String,
    pub primary_chain_id: String,
    pub secondary_chain_ids: Vec<String>,
    pub operator_binding: SignedWeb3IdentityBinding,
    pub proof_bundle_required: bool,
    pub dispute_windows: Vec<Web3DisputeWindow>,
    pub finality_rules: Vec<Web3ChainFinalityRule>,
    pub regulated_roles: Vec<Web3RegulatedRoleAssumption>,
    pub custody_boundary_note: String,
    pub local_policy_activation_required: bool,
}

pub fn validate_web3_trust_profile(profile: &Web3TrustProfile) -> Result<(), Web3ContractError> {
    if profile.schema != CHIO_WEB3_TRUST_PROFILE_SCHEMA {
        return Err(Web3ContractError::UnsupportedSchema(profile.schema.clone()));
    }
    ensure_non_empty(&profile.profile_id, "web3_trust_profile.profile_id")?;
    ensure_non_empty(
        &profile.chio_contract_version,
        "web3_trust_profile.chio_contract_version",
    )?;
    ensure_non_empty(
        &profile.primary_chain_id,
        "web3_trust_profile.primary_chain_id",
    )?;
    ensure_non_empty(
        &profile.custody_boundary_note,
        "web3_trust_profile.custody_boundary_note",
    )?;
    validate_web3_identity_binding(&profile.operator_binding)?;
    ensure_unique_strings(
        &profile.secondary_chain_ids,
        "web3_trust_profile.secondary_chain_ids",
    )?;
    if profile
        .secondary_chain_ids
        .iter()
        .any(|chain_id| chain_id == &profile.primary_chain_id)
    {
        return Err(Web3ContractError::DuplicateValue(
            profile.primary_chain_id.clone(),
        ));
    }

    let mut known_chains = HashSet::new();
    known_chains.insert(profile.primary_chain_id.as_str());
    for chain_id in &profile.secondary_chain_ids {
        known_chains.insert(chain_id.as_str());
    }
    for chain_id in &profile.operator_binding.certificate.chain_scope {
        known_chains.insert(chain_id.as_str());
    }
    if !profile
        .operator_binding
        .certificate
        .chain_scope
        .iter()
        .any(|chain_id| chain_id == &profile.primary_chain_id)
    {
        return Err(Web3ContractError::InvalidBinding(format!(
            "binding does not cover primary chain {}",
            profile.primary_chain_id
        )));
    }
    for chain_id in &profile.secondary_chain_ids {
        if !profile
            .operator_binding
            .certificate
            .chain_scope
            .iter()
            .any(|candidate| candidate == chain_id)
        {
            return Err(Web3ContractError::InvalidBinding(format!(
                "binding does not cover secondary chain {}",
                chain_id
            )));
        }
    }

    if !profile.local_policy_activation_required {
        return Err(Web3ContractError::InvalidBinding(
            "web3 trust profile must require explicit local policy activation".to_string(),
        ));
    }

    if profile.dispute_windows.is_empty() {
        return Err(Web3ContractError::MissingField(
            "web3_trust_profile.dispute_windows",
        ));
    }
    let mut seen_paths = HashSet::new();
    for window in &profile.dispute_windows {
        if !seen_paths.insert(window.settlement_path) {
            return Err(Web3ContractError::DuplicateValue(format!(
                "web3_trust_profile.dispute_windows:{:?}",
                window.settlement_path
            )));
        }
        if window.challenge_window_secs == 0 || window.recovery_window_secs == 0 {
            return Err(Web3ContractError::InvalidBinding(format!(
                "dispute window {:?} must have non-zero durations",
                window.settlement_path
            )));
        }
    }
    if profile.finality_rules.is_empty() {
        return Err(Web3ContractError::MissingField(
            "web3_trust_profile.finality_rules",
        ));
    }
    let mut seen_finality = HashSet::new();
    for rule in &profile.finality_rules {
        ensure_non_empty(&rule.chain_id, "web3_trust_profile.finality_rules.chain_id")?;
        if rule.min_confirmations == 0 {
            return Err(Web3ContractError::InvalidBinding(format!(
                "finality rule {} must require at least one confirmation",
                rule.chain_id
            )));
        }
        if !seen_finality.insert(rule.chain_id.as_str()) {
            return Err(Web3ContractError::DuplicateValue(rule.chain_id.clone()));
        }
    }
    for chain_id in [&profile.primary_chain_id]
        .into_iter()
        .chain(profile.secondary_chain_ids.iter())
    {
        if !seen_finality.contains(chain_id.as_str()) {
            return Err(Web3ContractError::UnknownReference(chain_id.clone()));
        }
    }

    if profile.regulated_roles.is_empty() {
        return Err(Web3ContractError::MissingField(
            "web3_trust_profile.regulated_roles",
        ));
    }
    let mut saw_custodian = false;
    for role in &profile.regulated_roles {
        ensure_non_empty(
            &role.actor_id,
            "web3_trust_profile.regulated_roles.actor_id",
        )?;
        ensure_non_empty(
            &role.responsibility,
            "web3_trust_profile.regulated_roles.responsibility",
        )?;
        if !role.custody_boundary_explicit {
            return Err(Web3ContractError::InvalidBinding(format!(
                "regulated role {:?} must keep custody boundary explicit",
                role.role
            )));
        }
        if role.role == Web3RegulatedRole::Custodian {
            saw_custodian = true;
        }
    }
    if !saw_custodian {
        return Err(Web3ContractError::InvalidBinding(
            "web3 trust profile must record at least one custodian role".to_string(),
        ));
    }

    Ok(())
}