chio-settle 0.1.2

Settlement runtime for Chio web3 escrow and bond execution
Documentation
use bs58::decode as bs58_decode;
use chio_core::canonical::canonical_json_bytes;
use chio_core::capability::scope::MonetaryAmount;
use chio_core::receipt::body::ChioReceipt;
use chio_core::web3::identity::{
    verify_web3_identity_binding, SignedWeb3IdentityBinding, Web3KeyBindingPurpose,
};
use serde::{Deserialize, Serialize};

use crate::config::SettlementEvidenceConfig;
use crate::{SettlementCommitment, SettlementError};

pub const SOLANA_ED25519_PROGRAM_ID: &str = "Ed25519SigVerify111111111111111111111111111";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct SolanaSettlementConfig {
    pub chain_id: String,
    pub cluster: String,
    pub program_id: String,
    pub settlement_mint: String,
    pub chio_minor_unit_decimals: u8,
    pub token_minor_unit_decimals: u8,
    #[serde(default)]
    pub evidence_substrate: SettlementEvidenceConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SolanaSettlementRequest {
    pub dispatch_id: String,
    pub capability_id: String,
    pub payer_address: String,
    pub beneficiary_address: String,
    pub settlement_amount: MonetaryAmount,
    pub recent_blockhash: String,
    pub receipt: ChioReceipt,
    pub binding: SignedWeb3IdentityBinding,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct PreparedSolanaSettlement {
    pub dispatch_id: String,
    pub chain_id: String,
    pub cluster: String,
    pub program_id: String,
    pub payer_address: String,
    pub beneficiary_address: String,
    pub settlement_mint: String,
    pub capability_commitment: String,
    pub receipt_hash: String,
    pub settlement_amount_minor_units: u64,
    pub recent_blockhash: String,
    pub ed25519_program_id: String,
    pub ed25519_signature: String,
    pub chio_public_key: String,
    pub instruction_data_hex: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
}

impl PreparedSolanaSettlement {
    #[must_use]
    pub fn commitment(&self, settlement_amount: MonetaryAmount) -> SettlementCommitment {
        SettlementCommitment {
            chain_id: self.chain_id.clone(),
            lane_kind: "solana_native_ed25519".to_string(),
            capability_commitment: self.capability_commitment.clone(),
            receipt_reference: self.receipt_hash.clone(),
            operator_identity: self.beneficiary_address.clone(),
            settlement_amount,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct CommitmentConsistencyReport {
    pub left_chain_id: String,
    pub right_chain_id: String,
    pub same_capability_commitment: bool,
    pub same_receipt_reference: bool,
    pub same_operator_identity: bool,
    pub same_amount: bool,
    pub overall_consistent: bool,
}

pub fn verify_solana_binding_and_receipt(
    chain_id: &str,
    receipt: &ChioReceipt,
    binding: &SignedWeb3IdentityBinding,
) -> Result<(), SettlementError> {
    verify_web3_identity_binding(binding)
        .map_err(|error| SettlementError::InvalidBinding(error.to_string()))?;
    if !binding
        .certificate
        .purpose
        .contains(&Web3KeyBindingPurpose::Settle)
    {
        return Err(SettlementError::InvalidBinding(
            "binding does not include settle purpose".to_string(),
        ));
    }
    if !binding
        .certificate
        .chain_scope
        .iter()
        .any(|scope| scope == chain_id)
    {
        return Err(SettlementError::InvalidBinding(format!(
            "binding does not cover {chain_id}"
        )));
    }
    bs58_decode(&binding.certificate.settlement_address)
        .into_vec()
        .map_err(|error| SettlementError::InvalidBinding(error.to_string()))?;
    let verified = receipt
        .verify_signature()
        .map_err(|error| SettlementError::Verification(error.to_string()))?;
    if !verified {
        return Err(SettlementError::Verification(
            "receipt Ed25519 signature verification failed".to_string(),
        ));
    }
    if receipt.kernel_key != binding.certificate.chio_public_key {
        return Err(SettlementError::InvalidBinding(
            "receipt kernel key must match the Solana settlement binding public key".to_string(),
        ));
    }
    Ok(())
}

pub fn prepare_solana_settlement(
    config: &SolanaSettlementConfig,
    request: &SolanaSettlementRequest,
) -> Result<PreparedSolanaSettlement, SettlementError> {
    config.validate()?;
    if config.chain_id.trim().is_empty()
        || config.cluster.trim().is_empty()
        || config.program_id.trim().is_empty()
        || config.settlement_mint.trim().is_empty()
    {
        return Err(SettlementError::InvalidInput(
            "solana settlement config is incomplete".to_string(),
        ));
    }
    verify_solana_binding_and_receipt(&config.chain_id, &request.receipt, &request.binding)?;
    for (value, field) in [
        (request.payer_address.as_str(), "payer_address"),
        (request.beneficiary_address.as_str(), "beneficiary_address"),
        (request.recent_blockhash.as_str(), "recent_blockhash"),
    ] {
        bs58_decode(value)
            .into_vec()
            .map_err(|error| SettlementError::InvalidInput(format!("{field}: {error}")))?;
    }
    let scale = 10_u64
        .checked_pow(u32::from(
            config
                .token_minor_unit_decimals
                .checked_sub(config.chio_minor_unit_decimals)
                .ok_or_else(|| {
                    SettlementError::InvalidInput(
                        "token decimals must be >= Chio decimals for Solana settlement".to_string(),
                    )
                })?,
        ))
        .ok_or_else(|| SettlementError::InvalidInput("amount scaling overflowed".to_string()))?;
    let settlement_amount_minor_units = request
        .settlement_amount
        .units
        .checked_mul(scale)
        .ok_or_else(|| SettlementError::InvalidInput("scaled amount overflowed".to_string()))?;
    let receipt_hash = chio_core::hashing::sha256(
        &canonical_json_bytes(&request.receipt.body())
            .map_err(|error| SettlementError::Serialization(error.to_string()))?,
    );
    let instruction_payload = serde_json::json!({
        "schema": "chio.settle.solana-release.v1",
        "dispatchId": request.dispatch_id,
        "capabilityCommitment": chio_core::hashing::sha256(request.capability_id.as_bytes()).to_hex_prefixed(),
        "receiptHash": receipt_hash.to_hex_prefixed(),
        "payer": request.payer_address,
        "beneficiary": request.beneficiary_address,
        "mint": config.settlement_mint,
        "amountMinorUnits": settlement_amount_minor_units,
        "recentBlockhash": request.recent_blockhash,
    });
    let instruction_data_hex = hex::encode(
        canonical_json_bytes(&instruction_payload)
            .map_err(|error| SettlementError::Serialization(error.to_string()))?,
    );
    Ok(PreparedSolanaSettlement {
        dispatch_id: request.dispatch_id.clone(),
        chain_id: config.chain_id.clone(),
        cluster: config.cluster.clone(),
        program_id: config.program_id.clone(),
        payer_address: request.payer_address.clone(),
        beneficiary_address: request.beneficiary_address.clone(),
        settlement_mint: config.settlement_mint.clone(),
        capability_commitment: chio_core::hashing::sha256(request.capability_id.as_bytes())
            .to_hex_prefixed(),
        receipt_hash: receipt_hash.to_hex_prefixed(),
        settlement_amount_minor_units,
        recent_blockhash: request.recent_blockhash.clone(),
        ed25519_program_id: SOLANA_ED25519_PROGRAM_ID.to_string(),
        ed25519_signature: request.receipt.signature.to_hex(),
        chio_public_key: request.receipt.kernel_key.to_hex(),
        instruction_data_hex,
        note: request.note.clone(),
    })
}

impl SolanaSettlementConfig {
    pub fn validate(&self) -> Result<(), SettlementError> {
        self.evidence_substrate.validate()?;
        Ok(())
    }
}

pub fn compare_commitments(
    left: &SettlementCommitment,
    right: &SettlementCommitment,
) -> CommitmentConsistencyReport {
    let same_capability_commitment = left.capability_commitment == right.capability_commitment;
    let same_receipt_reference = left.receipt_reference == right.receipt_reference;
    let same_operator_identity = left.operator_identity == right.operator_identity;
    let same_amount = left.settlement_amount == right.settlement_amount;
    CommitmentConsistencyReport {
        left_chain_id: left.chain_id.clone(),
        right_chain_id: right.chain_id.clone(),
        same_capability_commitment,
        same_receipt_reference,
        same_operator_identity,
        same_amount,
        overall_consistent: same_capability_commitment
            && same_receipt_reference
            && same_operator_identity
            && same_amount,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chio_core::crypto::Keypair;
    use chio_core::receipt::{
        body::ChioReceipt, body::ChioReceiptBody, decision::Decision, decision::ToolCallAction,
    };
    use chio_core::web3::identity::{SignedWeb3IdentityBinding, Web3IdentityBindingCertificate};
    use serde_json::json;

    use chio_test_support::prelude::*;

    fn sample_request() -> (SolanaSettlementConfig, SolanaSettlementRequest) {
        let keypair = Keypair::from_seed_hex(
            "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        )
        .test_unwrap();
        let receipt = ChioReceipt::sign(
            ChioReceiptBody {
                id: "rcpt-sol-1".to_string(),
                timestamp: 1_743_292_800,
                capability_id: "cap-sol-1".to_string(),
                tool_server: "chio-settle".to_string(),
                tool_name: "release_escrow".to_string(),
                action: ToolCallAction::from_parameters(json!({"amount": 125})).test_unwrap(),
                decision: Some(Decision::Allow),
                receipt_kind: Default::default(),
                boundary_class: Default::default(),
                observation_outcome: None,
                tool_origin: Default::default(),
                redaction_mode: Default::default(),
                actor_chain: Vec::new(),
                content_hash: "content".to_string(),
                policy_hash: "policy".to_string(),
                evidence: Vec::new(),
                metadata: None,
                trust_level: chio_core::receipt::kinds::TrustLevel::default(),
                tenant_id: None,
                kernel_key: keypair.public_key(),
                bbs_projection_version: None,
            },
            &keypair,
        )
        .test_unwrap();
        let binding = SignedWeb3IdentityBinding {
            certificate: Web3IdentityBindingCertificate {
                schema: chio_core::web3::identity::CHIO_KEY_BINDING_CERTIFICATE_SCHEMA.to_string(),
                chio_identity: format!("did:chio:{}", keypair.public_key().to_hex()),
                chio_public_key: keypair.public_key(),
                chain_scope: vec!["solana:mainnet".to_string()],
                purpose: vec![Web3KeyBindingPurpose::Settle],
                settlement_address: "9xQeWvG816bUx9EPjHmaT23yvVMm82oVxDgRmi1hM2Xy".to_string(),
                issued_at: 1_743_292_800,
                expires_at: 1_774_828_800,
                nonce: "nonce".to_string(),
            },
            signature: {
                let certificate = Web3IdentityBindingCertificate {
                    schema: chio_core::web3::identity::CHIO_KEY_BINDING_CERTIFICATE_SCHEMA
                        .to_string(),
                    chio_identity: format!("did:chio:{}", keypair.public_key().to_hex()),
                    chio_public_key: keypair.public_key(),
                    chain_scope: vec!["solana:mainnet".to_string()],
                    purpose: vec![Web3KeyBindingPurpose::Settle],
                    settlement_address: "9xQeWvG816bUx9EPjHmaT23yvVMm82oVxDgRmi1hM2Xy".to_string(),
                    issued_at: 1_743_292_800,
                    expires_at: 1_774_828_800,
                    nonce: "nonce".to_string(),
                };
                keypair.sign_canonical(&certificate).test_unwrap().0
            },
        };
        (
            SolanaSettlementConfig {
                chain_id: "solana:mainnet".to_string(),
                cluster: "mainnet-beta".to_string(),
                program_id: "11111111111111111111111111111111".to_string(),
                settlement_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
                chio_minor_unit_decimals: 2,
                token_minor_unit_decimals: 6,
                evidence_substrate: SettlementEvidenceConfig::default(),
            },
            SolanaSettlementRequest {
                dispatch_id: "dispatch-sol-1".to_string(),
                capability_id: "cap-sol-1".to_string(),
                payer_address: "9xQeWvG816bUx9EPjHmaT23yvVMm82oVxDgRmi1hM2Xy".to_string(),
                beneficiary_address: "7Yzjsq4LwTnQL4Z1cdq9VHtV6nRvWmvMRdqsiE9zraFM".to_string(),
                settlement_amount: MonetaryAmount {
                    units: 125,
                    currency: "USD".to_string(),
                },
                recent_blockhash: "8f1dWnLxkn6Ne1ASt1MSQd6hw2yffA9H9n1tFoZT3zh7".to_string(),
                receipt,
                binding,
                note: None,
            },
        )
    }

    #[test]
    fn solana_path_verifies_binding_and_receipt() {
        let (config, request) = sample_request();
        let prepared = prepare_solana_settlement(&config, &request).test_unwrap();
        assert_eq!(prepared.settlement_amount_minor_units, 1_250_000);
        assert_eq!(prepared.ed25519_program_id, SOLANA_ED25519_PROGRAM_ID);
    }

    #[test]
    fn solana_path_rejects_invalid_evidence_substrate() {
        let (mut config, request) = sample_request();
        config.evidence_substrate.checkpoint_statements = false;

        let error = prepare_solana_settlement(&config, &request).test_unwrap_err();
        assert!(error
            .to_string()
            .contains("kernel-signed checkpoint statements"));
    }

    #[test]
    fn commitment_comparison_detects_parity() {
        let left = SettlementCommitment {
            chain_id: "eip155:8453".to_string(),
            lane_kind: "evm_dual_signature".to_string(),
            capability_commitment: "0x01".to_string(),
            receipt_reference: "0x02".to_string(),
            operator_identity: "operator".to_string(),
            settlement_amount: MonetaryAmount {
                units: 10,
                currency: "USD".to_string(),
            },
        };
        let right = SettlementCommitment {
            chain_id: "solana:mainnet".to_string(),
            lane_kind: "solana_native_ed25519".to_string(),
            capability_commitment: "0x01".to_string(),
            receipt_reference: "0x02".to_string(),
            operator_identity: "operator".to_string(),
            settlement_amount: MonetaryAmount {
                units: 10,
                currency: "USD".to_string(),
            },
        };
        let report = compare_commitments(&left, &right);
        assert!(report.overall_consistent);
    }
}