Skip to main content

chio_web3/
trust_profile.rs

1use std::collections::HashSet;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Web3ContractError;
6use crate::identity::{validate_web3_identity_binding, SignedWeb3IdentityBinding};
7use crate::validation::{ensure_non_empty, ensure_unique_strings};
8
9pub const CHIO_WEB3_TRUST_PROFILE_SCHEMA: &str = "chio.web3-trust-profile.v1";
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum Web3SettlementPath {
14    DualSignature,
15    MerkleProof,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Web3DisputePolicy {
21    OffChainArbitration,
22    TimeoutRefund,
23    BondSlash,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum Web3FinalityMode {
29    OptimisticL2,
30    L1Finalized,
31    SolanaConfirmed,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum Web3RegulatedRole {
37    Operator,
38    Custodian,
39    PaymentInstitution,
40    OracleOperator,
41    Arbitrator,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct Web3DisputeWindow {
47    pub settlement_path: Web3SettlementPath,
48    pub challenge_window_secs: u64,
49    pub recovery_window_secs: u64,
50    pub dispute_policy: Web3DisputePolicy,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct Web3ChainFinalityRule {
56    pub chain_id: String,
57    pub mode: Web3FinalityMode,
58    pub min_confirmations: u32,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct Web3RegulatedRoleAssumption {
64    pub role: Web3RegulatedRole,
65    pub actor_id: String,
66    pub responsibility: String,
67    pub custody_boundary_explicit: bool,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(deny_unknown_fields)]
72pub struct Web3TrustProfile {
73    pub schema: String,
74    pub profile_id: String,
75    pub chio_contract_version: String,
76    pub primary_chain_id: String,
77    pub secondary_chain_ids: Vec<String>,
78    pub operator_binding: SignedWeb3IdentityBinding,
79    pub proof_bundle_required: bool,
80    pub dispute_windows: Vec<Web3DisputeWindow>,
81    pub finality_rules: Vec<Web3ChainFinalityRule>,
82    pub regulated_roles: Vec<Web3RegulatedRoleAssumption>,
83    pub custody_boundary_note: String,
84    pub local_policy_activation_required: bool,
85}
86
87pub fn validate_web3_trust_profile(profile: &Web3TrustProfile) -> Result<(), Web3ContractError> {
88    if profile.schema != CHIO_WEB3_TRUST_PROFILE_SCHEMA {
89        return Err(Web3ContractError::UnsupportedSchema(profile.schema.clone()));
90    }
91    ensure_non_empty(&profile.profile_id, "web3_trust_profile.profile_id")?;
92    ensure_non_empty(
93        &profile.chio_contract_version,
94        "web3_trust_profile.chio_contract_version",
95    )?;
96    ensure_non_empty(
97        &profile.primary_chain_id,
98        "web3_trust_profile.primary_chain_id",
99    )?;
100    ensure_non_empty(
101        &profile.custody_boundary_note,
102        "web3_trust_profile.custody_boundary_note",
103    )?;
104    validate_web3_identity_binding(&profile.operator_binding)?;
105    ensure_unique_strings(
106        &profile.secondary_chain_ids,
107        "web3_trust_profile.secondary_chain_ids",
108    )?;
109    if profile
110        .secondary_chain_ids
111        .iter()
112        .any(|chain_id| chain_id == &profile.primary_chain_id)
113    {
114        return Err(Web3ContractError::DuplicateValue(
115            profile.primary_chain_id.clone(),
116        ));
117    }
118
119    let mut known_chains = HashSet::new();
120    known_chains.insert(profile.primary_chain_id.as_str());
121    for chain_id in &profile.secondary_chain_ids {
122        known_chains.insert(chain_id.as_str());
123    }
124    for chain_id in &profile.operator_binding.certificate.chain_scope {
125        known_chains.insert(chain_id.as_str());
126    }
127    if !profile
128        .operator_binding
129        .certificate
130        .chain_scope
131        .iter()
132        .any(|chain_id| chain_id == &profile.primary_chain_id)
133    {
134        return Err(Web3ContractError::InvalidBinding(format!(
135            "binding does not cover primary chain {}",
136            profile.primary_chain_id
137        )));
138    }
139    for chain_id in &profile.secondary_chain_ids {
140        if !profile
141            .operator_binding
142            .certificate
143            .chain_scope
144            .iter()
145            .any(|candidate| candidate == chain_id)
146        {
147            return Err(Web3ContractError::InvalidBinding(format!(
148                "binding does not cover secondary chain {}",
149                chain_id
150            )));
151        }
152    }
153
154    if !profile.local_policy_activation_required {
155        return Err(Web3ContractError::InvalidBinding(
156            "web3 trust profile must require explicit local policy activation".to_string(),
157        ));
158    }
159
160    if profile.dispute_windows.is_empty() {
161        return Err(Web3ContractError::MissingField(
162            "web3_trust_profile.dispute_windows",
163        ));
164    }
165    let mut seen_paths = HashSet::new();
166    for window in &profile.dispute_windows {
167        if !seen_paths.insert(window.settlement_path) {
168            return Err(Web3ContractError::DuplicateValue(format!(
169                "web3_trust_profile.dispute_windows:{:?}",
170                window.settlement_path
171            )));
172        }
173        if window.challenge_window_secs == 0 || window.recovery_window_secs == 0 {
174            return Err(Web3ContractError::InvalidBinding(format!(
175                "dispute window {:?} must have non-zero durations",
176                window.settlement_path
177            )));
178        }
179    }
180    if profile.finality_rules.is_empty() {
181        return Err(Web3ContractError::MissingField(
182            "web3_trust_profile.finality_rules",
183        ));
184    }
185    let mut seen_finality = HashSet::new();
186    for rule in &profile.finality_rules {
187        ensure_non_empty(&rule.chain_id, "web3_trust_profile.finality_rules.chain_id")?;
188        if rule.min_confirmations == 0 {
189            return Err(Web3ContractError::InvalidBinding(format!(
190                "finality rule {} must require at least one confirmation",
191                rule.chain_id
192            )));
193        }
194        if !seen_finality.insert(rule.chain_id.as_str()) {
195            return Err(Web3ContractError::DuplicateValue(rule.chain_id.clone()));
196        }
197    }
198    for chain_id in [&profile.primary_chain_id]
199        .into_iter()
200        .chain(profile.secondary_chain_ids.iter())
201    {
202        if !seen_finality.contains(chain_id.as_str()) {
203            return Err(Web3ContractError::UnknownReference(chain_id.clone()));
204        }
205    }
206
207    if profile.regulated_roles.is_empty() {
208        return Err(Web3ContractError::MissingField(
209            "web3_trust_profile.regulated_roles",
210        ));
211    }
212    let mut saw_custodian = false;
213    for role in &profile.regulated_roles {
214        ensure_non_empty(
215            &role.actor_id,
216            "web3_trust_profile.regulated_roles.actor_id",
217        )?;
218        ensure_non_empty(
219            &role.responsibility,
220            "web3_trust_profile.regulated_roles.responsibility",
221        )?;
222        if !role.custody_boundary_explicit {
223            return Err(Web3ContractError::InvalidBinding(format!(
224                "regulated role {:?} must keep custody boundary explicit",
225                role.role
226            )));
227        }
228        if role.role == Web3RegulatedRole::Custodian {
229            saw_custodian = true;
230        }
231    }
232    if !saw_custodian {
233        return Err(Web3ContractError::InvalidBinding(
234            "web3 trust profile must record at least one custodian role".to_string(),
235        ));
236    }
237
238    Ok(())
239}