1use std::collections::HashSet;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Web3ContractError;
6use crate::validation::ensure_non_empty;
7
8pub const CHIO_WEB3_CHAIN_CONFIGURATION_SCHEMA: &str = "chio.web3-chain-configuration.v1";
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum Web3ChainRole {
13 Primary,
14 Secondary,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct Web3ChainDeployment {
20 pub chain_id: String,
21 pub network_name: String,
22 pub role: Web3ChainRole,
23 pub deployment_status: String,
24 pub settlement_token_symbol: String,
25 pub live_external_addresses: Option<Web3ChainExternalAddresses>,
26 pub planned_contract_addresses: Option<Web3ChainContractAddresses>,
27 pub deployed_contract_addresses: Option<Web3ChainContractAddresses>,
28 pub planned_operator_address: Option<String>,
29 pub deployed_operator_address: Option<String>,
30 pub deployment_plan: Option<Web3ChainDeploymentPlan>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct Web3ChainExternalAddresses {
36 pub settlement_token_address: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct Web3ChainContractAddresses {
42 pub root_registry_address: String,
43 pub escrow_address: String,
44 pub bond_vault_address: String,
45 pub identity_registry_address: String,
46 pub price_resolver_address: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct Web3ChainDeploymentPlan {
52 pub source_manifest_path: String,
53 pub source_manifest_sha256: String,
54 pub create2_factory_address: Option<String>,
55 pub create2_factory_source: String,
56 pub salt_namespace: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct Web3ChainGasProfile {
62 pub chain_id: String,
63 pub publish_root_gas: u64,
64 pub dual_sign_settlement_gas: u64,
65 pub merkle_settlement_gas: u64,
66 pub bond_release_gas: u64,
67 pub price_read_gas: u64,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(deny_unknown_fields)]
72pub struct Web3ChainConfiguration {
73 pub schema: String,
74 pub package_id: String,
75 pub primary_chain_id: String,
76 pub deployments: Vec<Web3ChainDeployment>,
77 pub gas_profiles: Vec<Web3ChainGasProfile>,
78}
79
80pub fn validate_web3_chain_configuration(
81 configuration: &Web3ChainConfiguration,
82) -> Result<(), Web3ContractError> {
83 if configuration.schema != CHIO_WEB3_CHAIN_CONFIGURATION_SCHEMA {
84 return Err(Web3ContractError::UnsupportedSchema(
85 configuration.schema.clone(),
86 ));
87 }
88 ensure_non_empty(
89 &configuration.package_id,
90 "web3_chain_configuration.package_id",
91 )?;
92 ensure_non_empty(
93 &configuration.primary_chain_id,
94 "web3_chain_configuration.primary_chain_id",
95 )?;
96 if configuration.deployments.is_empty() {
97 return Err(Web3ContractError::MissingField(
98 "web3_chain_configuration.deployments",
99 ));
100 }
101 if configuration.gas_profiles.is_empty() {
102 return Err(Web3ContractError::MissingField(
103 "web3_chain_configuration.gas_profiles",
104 ));
105 }
106
107 let mut deployment_ids = HashSet::new();
108 let mut primary_count = 0usize;
109 for deployment in &configuration.deployments {
110 ensure_non_empty(
111 &deployment.chain_id,
112 "web3_chain_configuration.deployments.chain_id",
113 )?;
114 ensure_non_empty(
115 &deployment.network_name,
116 "web3_chain_configuration.deployments.network_name",
117 )?;
118 ensure_non_empty(
119 &deployment.deployment_status,
120 "web3_chain_configuration.deployments.deployment_status",
121 )?;
122 ensure_non_empty(
123 &deployment.settlement_token_symbol,
124 "web3_chain_configuration.deployments.settlement_token_symbol",
125 )?;
126 let template_blocked =
127 deployment.deployment_status == "template_blocked_pending_external_assurance";
128 ensure_chain_deployment_addresses(deployment, template_blocked)?;
129 if !deployment_ids.insert(deployment.chain_id.as_str()) {
130 return Err(Web3ContractError::DuplicateValue(
131 deployment.chain_id.clone(),
132 ));
133 }
134 if deployment.role == Web3ChainRole::Primary {
135 primary_count += 1;
136 if deployment.chain_id != configuration.primary_chain_id {
137 return Err(Web3ContractError::InvalidBinding(format!(
138 "primary deployment {} does not match primary_chain_id {}",
139 deployment.chain_id, configuration.primary_chain_id
140 )));
141 }
142 }
143 }
144 if primary_count != 1 {
145 return Err(Web3ContractError::InvalidBinding(
146 "web3 chain configuration must declare exactly one primary deployment".to_string(),
147 ));
148 }
149
150 let mut gas_profiles = HashSet::new();
151 for gas in &configuration.gas_profiles {
152 ensure_non_empty(
153 &gas.chain_id,
154 "web3_chain_configuration.gas_profiles.chain_id",
155 )?;
156 if !deployment_ids.contains(gas.chain_id.as_str()) {
157 return Err(Web3ContractError::UnknownReference(gas.chain_id.clone()));
158 }
159 if !gas_profiles.insert(gas.chain_id.as_str()) {
160 return Err(Web3ContractError::DuplicateValue(gas.chain_id.clone()));
161 }
162 for metric in [
163 gas.publish_root_gas,
164 gas.dual_sign_settlement_gas,
165 gas.merkle_settlement_gas,
166 gas.bond_release_gas,
167 gas.price_read_gas,
168 ] {
169 if metric == 0 {
170 return Err(Web3ContractError::InvalidBinding(format!(
171 "gas profile {} must not contain zero-valued gas assumptions",
172 gas.chain_id
173 )));
174 }
175 }
176 }
177
178 Ok(())
179}
180
181fn ensure_chain_deployment_addresses(
182 deployment: &Web3ChainDeployment,
183 template_blocked: bool,
184) -> Result<(), Web3ContractError> {
185 let Some(external_addresses) = deployment.live_external_addresses.as_ref() else {
186 return Err(Web3ContractError::MissingField(
187 "web3_chain_configuration.deployments.live_external_addresses",
188 ));
189 };
190 ensure_evm_address(
191 &external_addresses.settlement_token_address,
192 "web3_chain_configuration.deployments.live_external_addresses.settlement_token_address",
193 )?;
194
195 if template_blocked {
196 if deployment.deployed_contract_addresses.is_some() {
197 return Err(Web3ContractError::InvalidBinding(
198 "template-blocked deployments must keep deployed_contract_addresses null"
199 .to_string(),
200 ));
201 }
202 if deployment.deployed_operator_address.is_some() {
203 return Err(Web3ContractError::InvalidBinding(
204 "template-blocked deployments must keep deployed_operator_address null".to_string(),
205 ));
206 }
207 let Some(planned_addresses) = deployment.planned_contract_addresses.as_ref() else {
208 return Err(Web3ContractError::MissingField(
209 "web3_chain_configuration.deployments.planned_contract_addresses",
210 ));
211 };
212 ensure_planned_contract_addresses(planned_addresses)?;
213 let Some(operator_address) = deployment.planned_operator_address.as_deref() else {
214 return Err(Web3ContractError::MissingField(
215 "web3_chain_configuration.deployments.planned_operator_address",
216 ));
217 };
218 ensure_evm_address(
219 operator_address,
220 "web3_chain_configuration.deployments.planned_operator_address",
221 )?;
222 let Some(plan) = deployment.deployment_plan.as_ref() else {
223 return Err(Web3ContractError::MissingField(
224 "web3_chain_configuration.deployments.deployment_plan",
225 ));
226 };
227 ensure_deployment_plan(plan)?;
228 return Ok(());
229 }
230
231 if deployment.planned_contract_addresses.is_some()
232 || deployment.planned_operator_address.is_some()
233 || deployment.deployment_plan.is_some()
234 {
235 return Err(Web3ContractError::InvalidBinding(
236 "deployed chain configurations must omit planned contract, operator, and deployment-plan fields"
237 .to_string(),
238 ));
239 }
240 let Some(deployed_addresses) = deployment.deployed_contract_addresses.as_ref() else {
241 return Err(Web3ContractError::MissingField(
242 "web3_chain_configuration.deployments.deployed_contract_addresses",
243 ));
244 };
245 ensure_deployed_contract_addresses(deployed_addresses)?;
246 let Some(operator_address) = deployment.deployed_operator_address.as_deref() else {
247 return Err(Web3ContractError::MissingField(
248 "web3_chain_configuration.deployments.deployed_operator_address",
249 ));
250 };
251 ensure_evm_address(
252 operator_address,
253 "web3_chain_configuration.deployments.deployed_operator_address",
254 )
255}
256
257fn ensure_planned_contract_addresses(
258 addresses: &Web3ChainContractAddresses,
259) -> Result<(), Web3ContractError> {
260 ensure_evm_address(
261 &addresses.root_registry_address,
262 "web3_chain_configuration.deployments.planned_contract_addresses.root_registry_address",
263 )?;
264 ensure_evm_address(
265 &addresses.escrow_address,
266 "web3_chain_configuration.deployments.planned_contract_addresses.escrow_address",
267 )?;
268 ensure_evm_address(
269 &addresses.bond_vault_address,
270 "web3_chain_configuration.deployments.planned_contract_addresses.bond_vault_address",
271 )?;
272 ensure_evm_address(
273 &addresses.identity_registry_address,
274 "web3_chain_configuration.deployments.planned_contract_addresses.identity_registry_address",
275 )?;
276 ensure_evm_address(
277 &addresses.price_resolver_address,
278 "web3_chain_configuration.deployments.planned_contract_addresses.price_resolver_address",
279 )
280}
281
282fn ensure_deployed_contract_addresses(
283 addresses: &Web3ChainContractAddresses,
284) -> Result<(), Web3ContractError> {
285 ensure_evm_address(
286 &addresses.root_registry_address,
287 "web3_chain_configuration.deployments.deployed_contract_addresses.root_registry_address",
288 )?;
289 ensure_evm_address(
290 &addresses.escrow_address,
291 "web3_chain_configuration.deployments.deployed_contract_addresses.escrow_address",
292 )?;
293 ensure_evm_address(
294 &addresses.bond_vault_address,
295 "web3_chain_configuration.deployments.deployed_contract_addresses.bond_vault_address",
296 )?;
297 ensure_evm_address(
298 &addresses.identity_registry_address,
299 "web3_chain_configuration.deployments.deployed_contract_addresses.identity_registry_address",
300 )?;
301 ensure_evm_address(
302 &addresses.price_resolver_address,
303 "web3_chain_configuration.deployments.deployed_contract_addresses.price_resolver_address",
304 )
305}
306
307fn ensure_deployment_plan(plan: &Web3ChainDeploymentPlan) -> Result<(), Web3ContractError> {
308 ensure_non_empty(
309 &plan.source_manifest_path,
310 "web3_chain_configuration.deployments.deployment_plan.source_manifest_path",
311 )?;
312 ensure_sha256_hex(
313 &plan.source_manifest_sha256,
314 "web3_chain_configuration.deployments.deployment_plan.source_manifest_sha256",
315 )?;
316 if let Some(factory_address) = plan.create2_factory_address.as_deref() {
317 ensure_evm_address(
318 factory_address,
319 "web3_chain_configuration.deployments.deployment_plan.create2_factory_address",
320 )?;
321 }
322 ensure_non_empty(
323 &plan.create2_factory_source,
324 "web3_chain_configuration.deployments.deployment_plan.create2_factory_source",
325 )?;
326 ensure_non_empty(
327 &plan.salt_namespace,
328 "web3_chain_configuration.deployments.deployment_plan.salt_namespace",
329 )
330}
331
332fn ensure_sha256_hex(value: &str, field: &'static str) -> Result<(), Web3ContractError> {
333 ensure_non_empty(value, field)?;
334 if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
335 return Err(Web3ContractError::InvalidBinding(format!(
336 "{field} must be exactly 32 bytes of hex"
337 )));
338 }
339 Ok(())
340}
341
342fn ensure_evm_address(address: &str, field: &'static str) -> Result<(), Web3ContractError> {
343 ensure_non_empty(address, field)?;
344 let Some(hex) = address.strip_prefix("0x") else {
345 return Err(Web3ContractError::InvalidBinding(format!(
346 "{field} must be a 0x-prefixed EVM address"
347 )));
348 };
349 if hex.len() != 40 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
350 return Err(Web3ContractError::InvalidBinding(format!(
351 "{field} must be exactly 20 bytes of hex"
352 )));
353 }
354 let normalized = hex.to_ascii_lowercase();
355 if normalized.bytes().all(|byte| byte == b'0') {
356 return Err(Web3ContractError::InvalidBinding(format!(
357 "{field} must not be the zero address"
358 )));
359 }
360 if normalized
361 .as_bytes()
362 .windows(2)
363 .all(|window| window[0] == window[1])
364 {
365 return Err(Web3ContractError::InvalidBinding(format!(
366 "{field} must not use a repeated-byte sentinel address"
367 )));
368 }
369 let bytes = normalized.as_bytes();
370 if bytes[1..39].iter().all(|byte| *byte == b'0') {
371 return Err(Web3ContractError::InvalidBinding(format!(
372 "{field} must not use a low-numbered sentinel address"
373 )));
374 }
375 Ok(())
376}