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::validation::ensure_non_empty;

pub const CHIO_WEB3_CHAIN_CONFIGURATION_SCHEMA: &str = "chio.web3-chain-configuration.v1";

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

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3ChainDeployment {
    pub chain_id: String,
    pub network_name: String,
    pub role: Web3ChainRole,
    pub deployment_status: String,
    pub settlement_token_symbol: String,
    pub live_external_addresses: Option<Web3ChainExternalAddresses>,
    pub planned_contract_addresses: Option<Web3ChainContractAddresses>,
    pub deployed_contract_addresses: Option<Web3ChainContractAddresses>,
    pub planned_operator_address: Option<String>,
    pub deployed_operator_address: Option<String>,
    pub deployment_plan: Option<Web3ChainDeploymentPlan>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3ChainExternalAddresses {
    pub settlement_token_address: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3ChainContractAddresses {
    pub root_registry_address: String,
    pub escrow_address: String,
    pub bond_vault_address: String,
    pub identity_registry_address: String,
    pub price_resolver_address: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3ChainDeploymentPlan {
    pub source_manifest_path: String,
    pub source_manifest_sha256: String,
    pub create2_factory_address: Option<String>,
    pub create2_factory_source: String,
    pub salt_namespace: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3ChainGasProfile {
    pub chain_id: String,
    pub publish_root_gas: u64,
    pub dual_sign_settlement_gas: u64,
    pub merkle_settlement_gas: u64,
    pub bond_release_gas: u64,
    pub price_read_gas: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Web3ChainConfiguration {
    pub schema: String,
    pub package_id: String,
    pub primary_chain_id: String,
    pub deployments: Vec<Web3ChainDeployment>,
    pub gas_profiles: Vec<Web3ChainGasProfile>,
}

pub fn validate_web3_chain_configuration(
    configuration: &Web3ChainConfiguration,
) -> Result<(), Web3ContractError> {
    if configuration.schema != CHIO_WEB3_CHAIN_CONFIGURATION_SCHEMA {
        return Err(Web3ContractError::UnsupportedSchema(
            configuration.schema.clone(),
        ));
    }
    ensure_non_empty(
        &configuration.package_id,
        "web3_chain_configuration.package_id",
    )?;
    ensure_non_empty(
        &configuration.primary_chain_id,
        "web3_chain_configuration.primary_chain_id",
    )?;
    if configuration.deployments.is_empty() {
        return Err(Web3ContractError::MissingField(
            "web3_chain_configuration.deployments",
        ));
    }
    if configuration.gas_profiles.is_empty() {
        return Err(Web3ContractError::MissingField(
            "web3_chain_configuration.gas_profiles",
        ));
    }

    let mut deployment_ids = HashSet::new();
    let mut primary_count = 0usize;
    for deployment in &configuration.deployments {
        ensure_non_empty(
            &deployment.chain_id,
            "web3_chain_configuration.deployments.chain_id",
        )?;
        ensure_non_empty(
            &deployment.network_name,
            "web3_chain_configuration.deployments.network_name",
        )?;
        ensure_non_empty(
            &deployment.deployment_status,
            "web3_chain_configuration.deployments.deployment_status",
        )?;
        ensure_non_empty(
            &deployment.settlement_token_symbol,
            "web3_chain_configuration.deployments.settlement_token_symbol",
        )?;
        let template_blocked =
            deployment.deployment_status == "template_blocked_pending_external_assurance";
        ensure_chain_deployment_addresses(deployment, template_blocked)?;
        if !deployment_ids.insert(deployment.chain_id.as_str()) {
            return Err(Web3ContractError::DuplicateValue(
                deployment.chain_id.clone(),
            ));
        }
        if deployment.role == Web3ChainRole::Primary {
            primary_count += 1;
            if deployment.chain_id != configuration.primary_chain_id {
                return Err(Web3ContractError::InvalidBinding(format!(
                    "primary deployment {} does not match primary_chain_id {}",
                    deployment.chain_id, configuration.primary_chain_id
                )));
            }
        }
    }
    if primary_count != 1 {
        return Err(Web3ContractError::InvalidBinding(
            "web3 chain configuration must declare exactly one primary deployment".to_string(),
        ));
    }

    let mut gas_profiles = HashSet::new();
    for gas in &configuration.gas_profiles {
        ensure_non_empty(
            &gas.chain_id,
            "web3_chain_configuration.gas_profiles.chain_id",
        )?;
        if !deployment_ids.contains(gas.chain_id.as_str()) {
            return Err(Web3ContractError::UnknownReference(gas.chain_id.clone()));
        }
        if !gas_profiles.insert(gas.chain_id.as_str()) {
            return Err(Web3ContractError::DuplicateValue(gas.chain_id.clone()));
        }
        for metric in [
            gas.publish_root_gas,
            gas.dual_sign_settlement_gas,
            gas.merkle_settlement_gas,
            gas.bond_release_gas,
            gas.price_read_gas,
        ] {
            if metric == 0 {
                return Err(Web3ContractError::InvalidBinding(format!(
                    "gas profile {} must not contain zero-valued gas assumptions",
                    gas.chain_id
                )));
            }
        }
    }

    Ok(())
}

fn ensure_chain_deployment_addresses(
    deployment: &Web3ChainDeployment,
    template_blocked: bool,
) -> Result<(), Web3ContractError> {
    let Some(external_addresses) = deployment.live_external_addresses.as_ref() else {
        return Err(Web3ContractError::MissingField(
            "web3_chain_configuration.deployments.live_external_addresses",
        ));
    };
    ensure_evm_address(
        &external_addresses.settlement_token_address,
        "web3_chain_configuration.deployments.live_external_addresses.settlement_token_address",
    )?;

    if template_blocked {
        if deployment.deployed_contract_addresses.is_some() {
            return Err(Web3ContractError::InvalidBinding(
                "template-blocked deployments must keep deployed_contract_addresses null"
                    .to_string(),
            ));
        }
        if deployment.deployed_operator_address.is_some() {
            return Err(Web3ContractError::InvalidBinding(
                "template-blocked deployments must keep deployed_operator_address null".to_string(),
            ));
        }
        let Some(planned_addresses) = deployment.planned_contract_addresses.as_ref() else {
            return Err(Web3ContractError::MissingField(
                "web3_chain_configuration.deployments.planned_contract_addresses",
            ));
        };
        ensure_planned_contract_addresses(planned_addresses)?;
        let Some(operator_address) = deployment.planned_operator_address.as_deref() else {
            return Err(Web3ContractError::MissingField(
                "web3_chain_configuration.deployments.planned_operator_address",
            ));
        };
        ensure_evm_address(
            operator_address,
            "web3_chain_configuration.deployments.planned_operator_address",
        )?;
        let Some(plan) = deployment.deployment_plan.as_ref() else {
            return Err(Web3ContractError::MissingField(
                "web3_chain_configuration.deployments.deployment_plan",
            ));
        };
        ensure_deployment_plan(plan)?;
        return Ok(());
    }

    if deployment.planned_contract_addresses.is_some()
        || deployment.planned_operator_address.is_some()
        || deployment.deployment_plan.is_some()
    {
        return Err(Web3ContractError::InvalidBinding(
            "deployed chain configurations must omit planned contract, operator, and deployment-plan fields"
                .to_string(),
        ));
    }
    let Some(deployed_addresses) = deployment.deployed_contract_addresses.as_ref() else {
        return Err(Web3ContractError::MissingField(
            "web3_chain_configuration.deployments.deployed_contract_addresses",
        ));
    };
    ensure_deployed_contract_addresses(deployed_addresses)?;
    let Some(operator_address) = deployment.deployed_operator_address.as_deref() else {
        return Err(Web3ContractError::MissingField(
            "web3_chain_configuration.deployments.deployed_operator_address",
        ));
    };
    ensure_evm_address(
        operator_address,
        "web3_chain_configuration.deployments.deployed_operator_address",
    )
}

fn ensure_planned_contract_addresses(
    addresses: &Web3ChainContractAddresses,
) -> Result<(), Web3ContractError> {
    ensure_evm_address(
        &addresses.root_registry_address,
        "web3_chain_configuration.deployments.planned_contract_addresses.root_registry_address",
    )?;
    ensure_evm_address(
        &addresses.escrow_address,
        "web3_chain_configuration.deployments.planned_contract_addresses.escrow_address",
    )?;
    ensure_evm_address(
        &addresses.bond_vault_address,
        "web3_chain_configuration.deployments.planned_contract_addresses.bond_vault_address",
    )?;
    ensure_evm_address(
        &addresses.identity_registry_address,
        "web3_chain_configuration.deployments.planned_contract_addresses.identity_registry_address",
    )?;
    ensure_evm_address(
        &addresses.price_resolver_address,
        "web3_chain_configuration.deployments.planned_contract_addresses.price_resolver_address",
    )
}

fn ensure_deployed_contract_addresses(
    addresses: &Web3ChainContractAddresses,
) -> Result<(), Web3ContractError> {
    ensure_evm_address(
        &addresses.root_registry_address,
        "web3_chain_configuration.deployments.deployed_contract_addresses.root_registry_address",
    )?;
    ensure_evm_address(
        &addresses.escrow_address,
        "web3_chain_configuration.deployments.deployed_contract_addresses.escrow_address",
    )?;
    ensure_evm_address(
        &addresses.bond_vault_address,
        "web3_chain_configuration.deployments.deployed_contract_addresses.bond_vault_address",
    )?;
    ensure_evm_address(
        &addresses.identity_registry_address,
        "web3_chain_configuration.deployments.deployed_contract_addresses.identity_registry_address",
    )?;
    ensure_evm_address(
        &addresses.price_resolver_address,
        "web3_chain_configuration.deployments.deployed_contract_addresses.price_resolver_address",
    )
}

fn ensure_deployment_plan(plan: &Web3ChainDeploymentPlan) -> Result<(), Web3ContractError> {
    ensure_non_empty(
        &plan.source_manifest_path,
        "web3_chain_configuration.deployments.deployment_plan.source_manifest_path",
    )?;
    ensure_sha256_hex(
        &plan.source_manifest_sha256,
        "web3_chain_configuration.deployments.deployment_plan.source_manifest_sha256",
    )?;
    if let Some(factory_address) = plan.create2_factory_address.as_deref() {
        ensure_evm_address(
            factory_address,
            "web3_chain_configuration.deployments.deployment_plan.create2_factory_address",
        )?;
    }
    ensure_non_empty(
        &plan.create2_factory_source,
        "web3_chain_configuration.deployments.deployment_plan.create2_factory_source",
    )?;
    ensure_non_empty(
        &plan.salt_namespace,
        "web3_chain_configuration.deployments.deployment_plan.salt_namespace",
    )
}

fn ensure_sha256_hex(value: &str, field: &'static str) -> Result<(), Web3ContractError> {
    ensure_non_empty(value, field)?;
    if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(Web3ContractError::InvalidBinding(format!(
            "{field} must be exactly 32 bytes of hex"
        )));
    }
    Ok(())
}

fn ensure_evm_address(address: &str, field: &'static str) -> Result<(), Web3ContractError> {
    ensure_non_empty(address, field)?;
    let Some(hex) = address.strip_prefix("0x") else {
        return Err(Web3ContractError::InvalidBinding(format!(
            "{field} must be a 0x-prefixed EVM address"
        )));
    };
    if hex.len() != 40 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(Web3ContractError::InvalidBinding(format!(
            "{field} must be exactly 20 bytes of hex"
        )));
    }
    let normalized = hex.to_ascii_lowercase();
    if normalized.bytes().all(|byte| byte == b'0') {
        return Err(Web3ContractError::InvalidBinding(format!(
            "{field} must not be the zero address"
        )));
    }
    if normalized
        .as_bytes()
        .windows(2)
        .all(|window| window[0] == window[1])
    {
        return Err(Web3ContractError::InvalidBinding(format!(
            "{field} must not use a repeated-byte sentinel address"
        )));
    }
    let bytes = normalized.as_bytes();
    if bytes[1..39].iter().all(|byte| *byte == b'0') {
        return Err(Web3ContractError::InvalidBinding(format!(
            "{field} must not use a low-numbered sentinel address"
        )));
    }
    Ok(())
}