1pub use chio_core_types::{canonical, capability, crypto, hashing, merkle, receipt};
9pub use chio_credit as credit;
10
11use std::collections::HashSet;
12
13pub use chio_core_types::oracle::OracleConversionEvidence;
14use serde::{Deserialize, Serialize};
15
16use crate::canonical::canonical_json_bytes;
17use crate::capability::MonetaryAmount;
18use crate::credit::{
19 CapitalExecutionInstructionAction, CapitalExecutionRailKind, CapitalExecutionReconciledState,
20 CreditBondLifecycleState, SignedCapitalExecutionInstruction, SignedCreditBond,
21 CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA,
22};
23use crate::crypto::{PublicKey, Signature};
24use crate::hashing::Hash;
25use crate::merkle::{leaf_hash, MerkleProof};
26use crate::receipt::{ChioReceipt, SignedExportEnvelope};
27
28pub const CHIO_KEY_BINDING_CERTIFICATE_SCHEMA: &str = "chio.key-binding-certificate.v1";
29pub const CHIO_WEB3_TRUST_PROFILE_SCHEMA: &str = "chio.web3-trust-profile.v1";
30pub const CHIO_WEB3_CONTRACT_PACKAGE_SCHEMA: &str = "chio.web3-contract-package.v1";
31pub const CHIO_WEB3_CHAIN_CONFIGURATION_SCHEMA: &str = "chio.web3-chain-configuration.v1";
32pub const CHIO_CHECKPOINT_STATEMENT_SCHEMA: &str = "chio.checkpoint_statement.v1";
33pub const CHIO_ANCHOR_INCLUSION_PROOF_SCHEMA: &str = "chio.anchor-inclusion-proof.v1";
34pub const CHIO_ORACLE_CONVERSION_EVIDENCE_SCHEMA: &str = "chio.oracle-conversion-evidence.v1";
35pub const CHIO_LINK_ORACLE_AUTHORITY: &str = "chio_link_runtime_v1";
36pub const CHIO_WEB3_SETTLEMENT_DISPATCH_SCHEMA: &str = "chio.web3-settlement-dispatch.v1";
37pub const CHIO_WEB3_SETTLEMENT_RECEIPT_SCHEMA: &str = "chio.web3-settlement-execution-receipt.v1";
38pub const CHIO_WEB3_QUALIFICATION_MATRIX_SCHEMA: &str = "chio.web3-qualification-matrix.v1";
39pub const CHIO_LINK_CONTROL_STATE_SCHEMA: &str = "chio.link.control-state.v1";
40pub const CHIO_LINK_CONTROL_TRACE_SCHEMA: &str = "chio.link.control-trace.v1";
41pub const CHIO_ANCHOR_CONTROL_STATE_SCHEMA: &str = "chio.anchor.control-state.v1";
42pub const CHIO_ANCHOR_CONTROL_TRACE_SCHEMA: &str = "chio.anchor.control-trace.v1";
43pub const CHIO_SETTLE_CONTROL_STATE_SCHEMA: &str = "chio.settle.control-state.v1";
44pub const CHIO_SETTLE_CONTROL_TRACE_SCHEMA: &str = "chio.settle.control-trace.v1";
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum Web3KeyBindingPurpose {
49 Anchor,
50 Settle,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum Web3SettlementPath {
56 DualSignature,
57 MerkleProof,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum Web3DisputePolicy {
63 OffChainArbitration,
64 TimeoutRefund,
65 BondSlash,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum Web3FinalityMode {
71 OptimisticL2,
72 L1Finalized,
73 SolanaConfirmed,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum Web3RegulatedRole {
79 Operator,
80 Custodian,
81 PaymentInstitution,
82 OracleOperator,
83 Arbitrator,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum Web3ContractKind {
89 RootRegistry,
90 Escrow,
91 BondVault,
92 IdentityRegistry,
93 PriceResolver,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum Web3BindingLanguage {
99 Rust,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104pub enum Web3ChainRole {
105 Primary,
106 Secondary,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum Web3SettlementLifecycleState {
112 PendingDispatch,
113 EscrowLocked,
114 PartiallySettled,
115 Settled,
116 Reversed,
117 ChargedBack,
118 TimedOut,
119 Failed,
120 Reorged,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum Web3QualificationOutcome {
126 Pass,
127 FailClosed,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct Web3IdentityBindingCertificate {
133 pub schema: String,
134 pub chio_identity: String,
135 pub chio_public_key: PublicKey,
136 pub chain_scope: Vec<String>,
137 pub purpose: Vec<Web3KeyBindingPurpose>,
138 pub settlement_address: String,
139 pub issued_at: u64,
140 pub expires_at: u64,
141 pub nonce: String,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(deny_unknown_fields)]
146pub struct SignedWeb3IdentityBinding {
147 pub certificate: Web3IdentityBindingCertificate,
148 pub signature: Signature,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(deny_unknown_fields)]
153pub struct Web3DisputeWindow {
154 pub settlement_path: Web3SettlementPath,
155 pub challenge_window_secs: u64,
156 pub recovery_window_secs: u64,
157 pub dispute_policy: Web3DisputePolicy,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(deny_unknown_fields)]
162pub struct Web3ChainFinalityRule {
163 pub chain_id: String,
164 pub mode: Web3FinalityMode,
165 pub min_confirmations: u32,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(deny_unknown_fields)]
170pub struct Web3RegulatedRoleAssumption {
171 pub role: Web3RegulatedRole,
172 pub actor_id: String,
173 pub responsibility: String,
174 pub custody_boundary_explicit: bool,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct Web3TrustProfile {
180 pub schema: String,
181 pub profile_id: String,
182 pub chio_contract_version: String,
183 pub primary_chain_id: String,
184 pub secondary_chain_ids: Vec<String>,
185 pub operator_binding: SignedWeb3IdentityBinding,
186 pub proof_bundle_required: bool,
187 pub dispute_windows: Vec<Web3DisputeWindow>,
188 pub finality_rules: Vec<Web3ChainFinalityRule>,
189 pub regulated_roles: Vec<Web3RegulatedRoleAssumption>,
190 pub custody_boundary_note: String,
191 pub local_policy_activation_required: bool,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(deny_unknown_fields)]
196pub struct Web3ContractInterface {
197 pub contract_id: String,
198 pub kind: Web3ContractKind,
199 pub interface_name: String,
200 pub abi_reference: String,
201 pub implementation_reference: String,
202 pub immutable: bool,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct Web3BindingTarget {
208 pub language: Web3BindingLanguage,
209 pub crate_path: String,
210 pub module_name: String,
211 pub contract_ids: Vec<String>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(deny_unknown_fields)]
216pub struct Web3ContractPackage {
217 pub schema: String,
218 pub package_id: String,
219 pub version: String,
220 pub chio_contract_version: String,
221 pub contracts: Vec<Web3ContractInterface>,
222 pub bindings: Vec<Web3BindingTarget>,
223 pub deferred_capabilities: Vec<String>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(deny_unknown_fields)]
228pub struct Web3ChainDeployment {
229 pub chain_id: String,
230 pub network_name: String,
231 pub role: Web3ChainRole,
232 pub settlement_token_symbol: String,
233 pub settlement_token_address: String,
234 pub root_registry_address: String,
235 pub escrow_address: String,
236 pub bond_vault_address: String,
237 pub identity_registry_address: String,
238 pub price_resolver_address: String,
239 pub operator_address: String,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(deny_unknown_fields)]
244pub struct Web3ChainGasProfile {
245 pub chain_id: String,
246 pub publish_root_gas: u64,
247 pub dual_sign_settlement_gas: u64,
248 pub merkle_settlement_gas: u64,
249 pub bond_release_gas: u64,
250 pub price_read_gas: u64,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(deny_unknown_fields)]
255pub struct Web3ChainConfiguration {
256 pub schema: String,
257 pub package_id: String,
258 pub primary_chain_id: String,
259 pub deployments: Vec<Web3ChainDeployment>,
260 pub gas_profiles: Vec<Web3ChainGasProfile>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(deny_unknown_fields)]
265pub struct Web3ReceiptInclusion {
266 pub checkpoint_seq: u64,
267 pub merkle_root: Hash,
268 pub proof: MerkleProof,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(deny_unknown_fields)]
273pub struct Web3CheckpointStatement {
274 pub schema: String,
275 pub checkpoint_seq: u64,
276 pub batch_start_seq: u64,
277 pub batch_end_seq: u64,
278 pub tree_size: u64,
279 pub merkle_root: Hash,
280 pub issued_at: u64,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub previous_checkpoint_sha256: Option<String>,
283 pub kernel_key: PublicKey,
284 pub signature: Signature,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(deny_unknown_fields)]
289pub struct Web3ChainAnchorRecord {
290 pub chain_id: String,
291 pub contract_address: String,
292 pub operator_address: String,
293 pub tx_hash: String,
294 pub block_number: u64,
295 pub block_hash: String,
296 pub anchored_merkle_root: Hash,
297 pub anchored_checkpoint_seq: u64,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301#[serde(deny_unknown_fields)]
302pub struct Web3BitcoinAnchor {
303 pub method: String,
304 pub ots_proof_b64: String,
305 pub bitcoin_block_height: u64,
306 pub bitcoin_block_hash: String,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(deny_unknown_fields)]
311pub struct Web3SuperRootInclusion {
312 pub super_root: Hash,
313 pub proof: MerkleProof,
314 pub aggregated_checkpoint_start: u64,
315 pub aggregated_checkpoint_end: u64,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
319#[serde(deny_unknown_fields)]
320pub struct AnchorInclusionProof {
321 pub schema: String,
322 pub receipt: ChioReceipt,
323 pub receipt_inclusion: Web3ReceiptInclusion,
324 pub checkpoint_statement: Web3CheckpointStatement,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub chain_anchor: Option<Web3ChainAnchorRecord>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub bitcoin_anchor: Option<Web3BitcoinAnchor>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub super_root_inclusion: Option<Web3SuperRootInclusion>,
331 pub key_binding_certificate: SignedWeb3IdentityBinding,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(deny_unknown_fields)]
336pub struct Web3SettlementSupportBoundary {
337 pub real_dispatch_supported: bool,
338 pub anchor_proof_required: bool,
339 pub oracle_evidence_required_for_fx: bool,
340 pub custody_boundary_explicit: bool,
341 pub reversal_supported: bool,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(deny_unknown_fields)]
346pub struct Web3SettlementDispatchArtifact {
347 pub schema: String,
348 pub dispatch_id: String,
349 pub issued_at: u64,
350 pub trust_profile_id: String,
351 pub contract_package_id: String,
352 pub chain_id: String,
353 pub capital_instruction: SignedCapitalExecutionInstruction,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub bond: Option<SignedCreditBond>,
356 pub settlement_path: Web3SettlementPath,
357 pub settlement_amount: MonetaryAmount,
358 pub escrow_id: String,
359 pub escrow_contract: String,
360 pub bond_vault_contract: String,
361 pub beneficiary_address: String,
362 pub support_boundary: Web3SettlementSupportBoundary,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub note: Option<String>,
365}
366
367pub type SignedWeb3SettlementDispatch = SignedExportEnvelope<Web3SettlementDispatchArtifact>;
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
370#[serde(deny_unknown_fields)]
371pub struct Web3SettlementExecutionReceiptArtifact {
372 pub schema: String,
373 pub execution_receipt_id: String,
374 pub issued_at: u64,
375 pub dispatch: Web3SettlementDispatchArtifact,
376 pub observed_execution: crate::credit::CapitalExecutionObservation,
377 pub lifecycle_state: Web3SettlementLifecycleState,
378 pub settlement_reference: String,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub reconciled_anchor_proof: Option<AnchorInclusionProof>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub oracle_evidence: Option<OracleConversionEvidence>,
383 pub settled_amount: MonetaryAmount,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
385 pub reversal_of: Option<String>,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
387 pub failure_reason: Option<String>,
388 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub note: Option<String>,
390}
391
392pub type SignedWeb3SettlementExecutionReceipt =
393 SignedExportEnvelope<Web3SettlementExecutionReceiptArtifact>;
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(deny_unknown_fields)]
397pub struct Web3QualificationCase {
398 pub id: String,
399 pub name: String,
400 pub requirement_ids: Vec<String>,
401 pub lifecycle_state: Web3SettlementLifecycleState,
402 pub expected_outcome: Web3QualificationOutcome,
403 pub observed_outcome: Web3QualificationOutcome,
404 pub notes: String,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
408#[serde(deny_unknown_fields)]
409pub struct Web3QualificationMatrix {
410 pub schema: String,
411 pub trust_profile_id: String,
412 pub contract_package_id: String,
413 pub cases: Vec<Web3QualificationCase>,
414}
415
416#[derive(Debug, thiserror::Error, PartialEq, Eq)]
417pub enum Web3ContractError {
418 #[error("unsupported schema: {0}")]
419 UnsupportedSchema(String),
420
421 #[error("missing field: {0}")]
422 MissingField(&'static str),
423
424 #[error("duplicate id or value: {0}")]
425 DuplicateValue(String),
426
427 #[error("unknown reference: {0}")]
428 UnknownReference(String),
429
430 #[error("invalid binding: {0}")]
431 InvalidBinding(String),
432
433 #[error("invalid proof: {0}")]
434 InvalidProof(String),
435
436 #[error("invalid settlement: {0}")]
437 InvalidSettlement(String),
438
439 #[error("invalid qualification case: {0}")]
440 InvalidQualificationCase(String),
441}
442
443pub fn validate_web3_identity_binding(
444 binding: &SignedWeb3IdentityBinding,
445) -> Result<(), Web3ContractError> {
446 if binding.certificate.schema != CHIO_KEY_BINDING_CERTIFICATE_SCHEMA {
447 return Err(Web3ContractError::UnsupportedSchema(
448 binding.certificate.schema.clone(),
449 ));
450 }
451 ensure_non_empty(&binding.certificate.chio_identity, "binding.chio_identity")?;
452 ensure_non_empty(
453 &binding.certificate.settlement_address,
454 "binding.settlement_address",
455 )?;
456 ensure_non_empty(&binding.certificate.nonce, "binding.nonce")?;
457 if binding.certificate.chain_scope.is_empty() {
458 return Err(Web3ContractError::MissingField("binding.chain_scope"));
459 }
460 if binding.certificate.purpose.is_empty() {
461 return Err(Web3ContractError::MissingField("binding.purpose"));
462 }
463 ensure_unique_strings(&binding.certificate.chain_scope, "binding.chain_scope")?;
464 ensure_unique_copy_values(&binding.certificate.purpose, "binding.purpose")?;
465 if binding.certificate.issued_at >= binding.certificate.expires_at {
466 return Err(Web3ContractError::InvalidBinding(
467 "identity binding issued_at must be earlier than expires_at".to_string(),
468 ));
469 }
470 Ok(())
471}
472
473pub fn verify_web3_identity_binding(
474 binding: &SignedWeb3IdentityBinding,
475) -> Result<(), Web3ContractError> {
476 validate_web3_identity_binding(binding)?;
477 let verified = binding
478 .certificate
479 .chio_public_key
480 .verify_canonical(&binding.certificate, &binding.signature)
481 .map_err(|error| Web3ContractError::InvalidBinding(error.to_string()))?;
482 if !verified {
483 return Err(Web3ContractError::InvalidBinding(
484 "identity binding signature verification failed".to_string(),
485 ));
486 }
487 Ok(())
488}
489
490pub fn validate_web3_trust_profile(profile: &Web3TrustProfile) -> Result<(), Web3ContractError> {
491 if profile.schema != CHIO_WEB3_TRUST_PROFILE_SCHEMA {
492 return Err(Web3ContractError::UnsupportedSchema(profile.schema.clone()));
493 }
494 ensure_non_empty(&profile.profile_id, "web3_trust_profile.profile_id")?;
495 ensure_non_empty(
496 &profile.chio_contract_version,
497 "web3_trust_profile.chio_contract_version",
498 )?;
499 ensure_non_empty(
500 &profile.primary_chain_id,
501 "web3_trust_profile.primary_chain_id",
502 )?;
503 ensure_non_empty(
504 &profile.custody_boundary_note,
505 "web3_trust_profile.custody_boundary_note",
506 )?;
507 validate_web3_identity_binding(&profile.operator_binding)?;
508 ensure_unique_strings(
509 &profile.secondary_chain_ids,
510 "web3_trust_profile.secondary_chain_ids",
511 )?;
512 if profile
513 .secondary_chain_ids
514 .iter()
515 .any(|chain_id| chain_id == &profile.primary_chain_id)
516 {
517 return Err(Web3ContractError::DuplicateValue(
518 profile.primary_chain_id.clone(),
519 ));
520 }
521
522 let mut known_chains = HashSet::new();
523 known_chains.insert(profile.primary_chain_id.as_str());
524 for chain_id in &profile.secondary_chain_ids {
525 known_chains.insert(chain_id.as_str());
526 }
527 for chain_id in &profile.operator_binding.certificate.chain_scope {
528 known_chains.insert(chain_id.as_str());
529 }
530 if !profile
531 .operator_binding
532 .certificate
533 .chain_scope
534 .iter()
535 .any(|chain_id| chain_id == &profile.primary_chain_id)
536 {
537 return Err(Web3ContractError::InvalidBinding(format!(
538 "binding does not cover primary chain {}",
539 profile.primary_chain_id
540 )));
541 }
542 for chain_id in &profile.secondary_chain_ids {
543 if !profile
544 .operator_binding
545 .certificate
546 .chain_scope
547 .iter()
548 .any(|candidate| candidate == chain_id)
549 {
550 return Err(Web3ContractError::InvalidBinding(format!(
551 "binding does not cover secondary chain {}",
552 chain_id
553 )));
554 }
555 }
556
557 if !profile.local_policy_activation_required {
558 return Err(Web3ContractError::InvalidBinding(
559 "web3 trust profile must require explicit local policy activation".to_string(),
560 ));
561 }
562
563 if profile.dispute_windows.is_empty() {
564 return Err(Web3ContractError::MissingField(
565 "web3_trust_profile.dispute_windows",
566 ));
567 }
568 let mut seen_paths = HashSet::new();
569 for window in &profile.dispute_windows {
570 if !seen_paths.insert(window.settlement_path) {
571 return Err(Web3ContractError::DuplicateValue(format!(
572 "web3_trust_profile.dispute_windows:{:?}",
573 window.settlement_path
574 )));
575 }
576 if window.challenge_window_secs == 0 || window.recovery_window_secs == 0 {
577 return Err(Web3ContractError::InvalidBinding(format!(
578 "dispute window {:?} must have non-zero durations",
579 window.settlement_path
580 )));
581 }
582 }
583 if profile.finality_rules.is_empty() {
584 return Err(Web3ContractError::MissingField(
585 "web3_trust_profile.finality_rules",
586 ));
587 }
588 let mut seen_finality = HashSet::new();
589 for rule in &profile.finality_rules {
590 ensure_non_empty(&rule.chain_id, "web3_trust_profile.finality_rules.chain_id")?;
591 if rule.min_confirmations == 0 {
592 return Err(Web3ContractError::InvalidBinding(format!(
593 "finality rule {} must require at least one confirmation",
594 rule.chain_id
595 )));
596 }
597 if !seen_finality.insert(rule.chain_id.as_str()) {
598 return Err(Web3ContractError::DuplicateValue(rule.chain_id.clone()));
599 }
600 }
601 for chain_id in [&profile.primary_chain_id]
602 .into_iter()
603 .chain(profile.secondary_chain_ids.iter())
604 {
605 if !seen_finality.contains(chain_id.as_str()) {
606 return Err(Web3ContractError::UnknownReference(chain_id.clone()));
607 }
608 }
609
610 if profile.regulated_roles.is_empty() {
611 return Err(Web3ContractError::MissingField(
612 "web3_trust_profile.regulated_roles",
613 ));
614 }
615 let mut saw_custodian = false;
616 for role in &profile.regulated_roles {
617 ensure_non_empty(
618 &role.actor_id,
619 "web3_trust_profile.regulated_roles.actor_id",
620 )?;
621 ensure_non_empty(
622 &role.responsibility,
623 "web3_trust_profile.regulated_roles.responsibility",
624 )?;
625 if !role.custody_boundary_explicit {
626 return Err(Web3ContractError::InvalidBinding(format!(
627 "regulated role {:?} must keep custody boundary explicit",
628 role.role
629 )));
630 }
631 if role.role == Web3RegulatedRole::Custodian {
632 saw_custodian = true;
633 }
634 }
635 if !saw_custodian {
636 return Err(Web3ContractError::InvalidBinding(
637 "web3 trust profile must record at least one custodian role".to_string(),
638 ));
639 }
640
641 Ok(())
642}
643
644pub fn validate_web3_contract_package(
645 package: &Web3ContractPackage,
646) -> Result<(), Web3ContractError> {
647 if package.schema != CHIO_WEB3_CONTRACT_PACKAGE_SCHEMA {
648 return Err(Web3ContractError::UnsupportedSchema(package.schema.clone()));
649 }
650 ensure_non_empty(&package.package_id, "web3_contract_package.package_id")?;
651 ensure_non_empty(&package.version, "web3_contract_package.version")?;
652 ensure_non_empty(
653 &package.chio_contract_version,
654 "web3_contract_package.chio_contract_version",
655 )?;
656 if package.contracts.is_empty() {
657 return Err(Web3ContractError::MissingField(
658 "web3_contract_package.contracts",
659 ));
660 }
661 if package.bindings.is_empty() {
662 return Err(Web3ContractError::MissingField(
663 "web3_contract_package.bindings",
664 ));
665 }
666
667 let mut contract_ids = HashSet::new();
668 let mut contract_kinds = HashSet::new();
669 for contract in &package.contracts {
670 ensure_non_empty(
671 &contract.contract_id,
672 "web3_contract_package.contracts.contract_id",
673 )?;
674 ensure_non_empty(
675 &contract.interface_name,
676 "web3_contract_package.contracts.interface_name",
677 )?;
678 ensure_non_empty(
679 &contract.abi_reference,
680 "web3_contract_package.contracts.abi_reference",
681 )?;
682 ensure_non_empty(
683 &contract.implementation_reference,
684 "web3_contract_package.contracts.implementation_reference",
685 )?;
686 if !contract_ids.insert(contract.contract_id.as_str()) {
687 return Err(Web3ContractError::DuplicateValue(
688 contract.contract_id.clone(),
689 ));
690 }
691 if !contract_kinds.insert(contract.kind) {
692 return Err(Web3ContractError::DuplicateValue(format!(
693 "web3_contract_package.contract_kind:{:?}",
694 contract.kind
695 )));
696 }
697 }
698 for required in [
699 Web3ContractKind::RootRegistry,
700 Web3ContractKind::Escrow,
701 Web3ContractKind::BondVault,
702 Web3ContractKind::IdentityRegistry,
703 Web3ContractKind::PriceResolver,
704 ] {
705 if !contract_kinds.contains(&required) {
706 return Err(Web3ContractError::UnknownReference(format!(
707 "missing required contract kind {:?}",
708 required
709 )));
710 }
711 }
712
713 for binding in &package.bindings {
714 ensure_non_empty(
715 &binding.crate_path,
716 "web3_contract_package.bindings.crate_path",
717 )?;
718 ensure_non_empty(
719 &binding.module_name,
720 "web3_contract_package.bindings.module_name",
721 )?;
722 if binding.contract_ids.is_empty() {
723 return Err(Web3ContractError::MissingField(
724 "web3_contract_package.bindings.contract_ids",
725 ));
726 }
727 ensure_unique_strings(
728 &binding.contract_ids,
729 "web3_contract_package.bindings.contract_ids",
730 )?;
731 for contract_id in &binding.contract_ids {
732 if !contract_ids.contains(contract_id.as_str()) {
733 return Err(Web3ContractError::UnknownReference(contract_id.clone()));
734 }
735 }
736 }
737
738 ensure_unique_strings(
739 &package.deferred_capabilities,
740 "web3_contract_package.deferred_capabilities",
741 )?;
742
743 Ok(())
744}
745
746pub fn validate_web3_chain_configuration(
747 configuration: &Web3ChainConfiguration,
748) -> Result<(), Web3ContractError> {
749 if configuration.schema != CHIO_WEB3_CHAIN_CONFIGURATION_SCHEMA {
750 return Err(Web3ContractError::UnsupportedSchema(
751 configuration.schema.clone(),
752 ));
753 }
754 ensure_non_empty(
755 &configuration.package_id,
756 "web3_chain_configuration.package_id",
757 )?;
758 ensure_non_empty(
759 &configuration.primary_chain_id,
760 "web3_chain_configuration.primary_chain_id",
761 )?;
762 if configuration.deployments.is_empty() {
763 return Err(Web3ContractError::MissingField(
764 "web3_chain_configuration.deployments",
765 ));
766 }
767 if configuration.gas_profiles.is_empty() {
768 return Err(Web3ContractError::MissingField(
769 "web3_chain_configuration.gas_profiles",
770 ));
771 }
772
773 let mut deployment_ids = HashSet::new();
774 let mut primary_count = 0usize;
775 for deployment in &configuration.deployments {
776 ensure_non_empty(
777 &deployment.chain_id,
778 "web3_chain_configuration.deployments.chain_id",
779 )?;
780 ensure_non_empty(
781 &deployment.network_name,
782 "web3_chain_configuration.deployments.network_name",
783 )?;
784 ensure_non_empty(
785 &deployment.settlement_token_symbol,
786 "web3_chain_configuration.deployments.settlement_token_symbol",
787 )?;
788 for field in [
789 &deployment.settlement_token_address,
790 &deployment.root_registry_address,
791 &deployment.escrow_address,
792 &deployment.bond_vault_address,
793 &deployment.identity_registry_address,
794 &deployment.price_resolver_address,
795 &deployment.operator_address,
796 ] {
797 ensure_non_empty(field, "web3_chain_configuration.deployments.addresses")?;
798 }
799 if !deployment_ids.insert(deployment.chain_id.as_str()) {
800 return Err(Web3ContractError::DuplicateValue(
801 deployment.chain_id.clone(),
802 ));
803 }
804 if deployment.role == Web3ChainRole::Primary {
805 primary_count += 1;
806 if deployment.chain_id != configuration.primary_chain_id {
807 return Err(Web3ContractError::InvalidBinding(format!(
808 "primary deployment {} does not match primary_chain_id {}",
809 deployment.chain_id, configuration.primary_chain_id
810 )));
811 }
812 }
813 }
814 if primary_count != 1 {
815 return Err(Web3ContractError::InvalidBinding(
816 "web3 chain configuration must declare exactly one primary deployment".to_string(),
817 ));
818 }
819
820 let mut gas_profiles = HashSet::new();
821 for gas in &configuration.gas_profiles {
822 ensure_non_empty(
823 &gas.chain_id,
824 "web3_chain_configuration.gas_profiles.chain_id",
825 )?;
826 if !deployment_ids.contains(gas.chain_id.as_str()) {
827 return Err(Web3ContractError::UnknownReference(gas.chain_id.clone()));
828 }
829 if !gas_profiles.insert(gas.chain_id.as_str()) {
830 return Err(Web3ContractError::DuplicateValue(gas.chain_id.clone()));
831 }
832 for metric in [
833 gas.publish_root_gas,
834 gas.dual_sign_settlement_gas,
835 gas.merkle_settlement_gas,
836 gas.bond_release_gas,
837 gas.price_read_gas,
838 ] {
839 if metric == 0 {
840 return Err(Web3ContractError::InvalidBinding(format!(
841 "gas profile {} must not contain zero-valued gas assumptions",
842 gas.chain_id
843 )));
844 }
845 }
846 }
847
848 Ok(())
849}
850
851pub fn validate_oracle_conversion_evidence(
852 evidence: &OracleConversionEvidence,
853) -> Result<(), Web3ContractError> {
854 if evidence.schema != CHIO_ORACLE_CONVERSION_EVIDENCE_SCHEMA {
855 return Err(Web3ContractError::UnsupportedSchema(
856 evidence.schema.clone(),
857 ));
858 }
859 for field in [
860 &evidence.base,
861 &evidence.quote,
862 &evidence.authority,
863 &evidence.source,
864 &evidence.feed_address,
865 &evidence.original_currency,
866 &evidence.grant_currency,
867 ] {
868 ensure_non_empty(field, "oracle_conversion_evidence.field")?;
869 }
870 if evidence.authority != CHIO_LINK_ORACLE_AUTHORITY {
871 return Err(Web3ContractError::InvalidProof(format!(
872 "oracle conversion evidence authority {} is unsupported",
873 evidence.authority
874 )));
875 }
876 if evidence.rate_denominator == 0 {
877 return Err(Web3ContractError::InvalidProof(
878 "oracle conversion evidence rate_denominator must be non-zero".to_string(),
879 ));
880 }
881 if evidence.max_age_seconds == 0 {
882 return Err(Web3ContractError::InvalidProof(
883 "oracle conversion evidence max_age_seconds must be non-zero".to_string(),
884 ));
885 }
886 if evidence.original_cost_units == 0 || evidence.converted_cost_units == 0 {
887 return Err(Web3ContractError::InvalidProof(
888 "oracle conversion evidence cost units must be non-zero".to_string(),
889 ));
890 }
891 Ok(())
892}
893
894pub fn validate_anchor_inclusion_proof(
895 proof: &AnchorInclusionProof,
896) -> Result<(), Web3ContractError> {
897 if proof.schema != CHIO_ANCHOR_INCLUSION_PROOF_SCHEMA {
898 return Err(Web3ContractError::UnsupportedSchema(proof.schema.clone()));
899 }
900 validate_web3_identity_binding(&proof.key_binding_certificate)?;
901 if proof.receipt.id.trim().is_empty() {
902 return Err(Web3ContractError::MissingField(
903 "anchor_inclusion.receipt.id",
904 ));
905 }
906 if proof.receipt_inclusion.proof.tree_size == 0 {
907 return Err(Web3ContractError::InvalidProof(
908 "anchor inclusion proof tree_size must be non-zero".to_string(),
909 ));
910 }
911 if proof.receipt_inclusion.proof.leaf_index >= proof.receipt_inclusion.proof.tree_size {
912 return Err(Web3ContractError::InvalidProof(
913 "anchor inclusion proof leaf_index exceeds tree_size".to_string(),
914 ));
915 }
916 if proof.receipt_inclusion.checkpoint_seq != proof.checkpoint_statement.checkpoint_seq {
917 return Err(Web3ContractError::InvalidProof(
918 "receipt inclusion checkpoint_seq must match checkpoint statement".to_string(),
919 ));
920 }
921 if proof.receipt_inclusion.merkle_root != proof.checkpoint_statement.merkle_root {
922 return Err(Web3ContractError::InvalidProof(
923 "receipt inclusion merkle_root must match checkpoint statement".to_string(),
924 ));
925 }
926 if proof.checkpoint_statement.schema != CHIO_CHECKPOINT_STATEMENT_SCHEMA {
927 return Err(Web3ContractError::UnsupportedSchema(
928 proof.checkpoint_statement.schema.clone(),
929 ));
930 }
931 if proof.checkpoint_statement.batch_start_seq > proof.checkpoint_statement.batch_end_seq {
932 return Err(Web3ContractError::InvalidProof(
933 "checkpoint statement batch_start_seq must be <= batch_end_seq".to_string(),
934 ));
935 }
936 if proof.checkpoint_statement.tree_size == 0 {
937 return Err(Web3ContractError::InvalidProof(
938 "checkpoint statement tree_size must be non-zero".to_string(),
939 ));
940 }
941 if proof.checkpoint_statement.tree_size as usize != proof.receipt_inclusion.proof.tree_size {
942 return Err(Web3ContractError::InvalidProof(
943 "checkpoint statement tree_size must match receipt inclusion proof".to_string(),
944 ));
945 }
946 if let Some(chain_anchor) = proof.chain_anchor.as_ref() {
947 ensure_non_empty(
948 &chain_anchor.chain_id,
949 "anchor_inclusion.chain_anchor.chain_id",
950 )?;
951 ensure_non_empty(
952 &chain_anchor.contract_address,
953 "anchor_inclusion.chain_anchor.contract_address",
954 )?;
955 ensure_non_empty(
956 &chain_anchor.operator_address,
957 "anchor_inclusion.chain_anchor.operator_address",
958 )?;
959 ensure_non_empty(
960 &chain_anchor.tx_hash,
961 "anchor_inclusion.chain_anchor.tx_hash",
962 )?;
963 ensure_non_empty(
964 &chain_anchor.block_hash,
965 "anchor_inclusion.chain_anchor.block_hash",
966 )?;
967 if chain_anchor.anchored_checkpoint_seq != proof.checkpoint_statement.checkpoint_seq {
968 return Err(Web3ContractError::InvalidProof(
969 "chain anchor checkpoint seq must match checkpoint statement".to_string(),
970 ));
971 }
972 if chain_anchor.anchored_merkle_root != proof.checkpoint_statement.merkle_root {
973 return Err(Web3ContractError::InvalidProof(
974 "chain anchor root must match checkpoint statement".to_string(),
975 ));
976 }
977 if chain_anchor.operator_address
978 != proof.key_binding_certificate.certificate.settlement_address
979 {
980 return Err(Web3ContractError::InvalidBinding(
981 "chain anchor operator address must match settlement binding".to_string(),
982 ));
983 }
984 if !proof
985 .key_binding_certificate
986 .certificate
987 .purpose
988 .contains(&Web3KeyBindingPurpose::Anchor)
989 {
990 return Err(Web3ContractError::InvalidBinding(
991 "anchor proof requires a binding certificate scoped to anchor".to_string(),
992 ));
993 }
994 if !proof
995 .key_binding_certificate
996 .certificate
997 .chain_scope
998 .iter()
999 .any(|chain_id| chain_id == &chain_anchor.chain_id)
1000 {
1001 return Err(Web3ContractError::InvalidBinding(format!(
1002 "binding certificate does not cover anchor chain {}",
1003 chain_anchor.chain_id
1004 )));
1005 }
1006 }
1007 if let Some(bitcoin_anchor) = proof.bitcoin_anchor.as_ref() {
1008 ensure_non_empty(
1009 &bitcoin_anchor.method,
1010 "anchor_inclusion.bitcoin_anchor.method",
1011 )?;
1012 ensure_non_empty(
1013 &bitcoin_anchor.ots_proof_b64,
1014 "anchor_inclusion.bitcoin_anchor.ots_proof_b64",
1015 )?;
1016 ensure_non_empty(
1017 &bitcoin_anchor.bitcoin_block_hash,
1018 "anchor_inclusion.bitcoin_anchor.bitcoin_block_hash",
1019 )?;
1020 if proof.super_root_inclusion.is_none() {
1021 return Err(Web3ContractError::InvalidProof(
1022 "Bitcoin anchor requires super-root inclusion metadata".to_string(),
1023 ));
1024 }
1025 }
1026 if let Some(super_root) = proof.super_root_inclusion.as_ref() {
1027 if super_root.aggregated_checkpoint_start > super_root.aggregated_checkpoint_end {
1028 return Err(Web3ContractError::InvalidProof(
1029 "super-root inclusion checkpoint range must be ordered".to_string(),
1030 ));
1031 }
1032 if proof.checkpoint_statement.checkpoint_seq < super_root.aggregated_checkpoint_start
1033 || proof.checkpoint_statement.checkpoint_seq > super_root.aggregated_checkpoint_end
1034 {
1035 return Err(Web3ContractError::InvalidProof(
1036 "checkpoint statement must fall within the super-root aggregation range"
1037 .to_string(),
1038 ));
1039 }
1040 if super_root.proof.tree_size == 0 {
1041 return Err(Web3ContractError::InvalidProof(
1042 "super-root inclusion tree_size must be non-zero".to_string(),
1043 ));
1044 }
1045 }
1046 Ok(())
1047}
1048
1049pub fn verify_checkpoint_statement(
1050 statement: &Web3CheckpointStatement,
1051) -> Result<(), Web3ContractError> {
1052 let body = checkpoint_statement_body(statement);
1053 let verified = statement
1054 .kernel_key
1055 .verify_canonical(&body, &statement.signature)
1056 .map_err(|error| Web3ContractError::InvalidProof(error.to_string()))?;
1057 if !verified {
1058 return Err(Web3ContractError::InvalidProof(
1059 "checkpoint statement signature verification failed".to_string(),
1060 ));
1061 }
1062 Ok(())
1063}
1064
1065pub fn verify_anchor_inclusion_proof(
1066 proof: &AnchorInclusionProof,
1067) -> Result<(), Web3ContractError> {
1068 validate_anchor_inclusion_proof(proof)?;
1069 let receipt_verified = proof
1070 .receipt
1071 .verify_signature()
1072 .map_err(|error| Web3ContractError::InvalidProof(error.to_string()))?;
1073 if !receipt_verified {
1074 return Err(Web3ContractError::InvalidProof(
1075 "receipt signature verification failed".to_string(),
1076 ));
1077 }
1078 verify_web3_identity_binding(&proof.key_binding_certificate)?;
1079 verify_checkpoint_statement(&proof.checkpoint_statement)?;
1080 if proof.key_binding_certificate.certificate.chio_public_key != proof.receipt.kernel_key {
1081 return Err(Web3ContractError::InvalidBinding(
1082 "binding certificate public key must match receipt kernel_key".to_string(),
1083 ));
1084 }
1085 if proof.checkpoint_statement.kernel_key != proof.receipt.kernel_key {
1086 return Err(Web3ContractError::InvalidProof(
1087 "checkpoint statement kernel_key must match receipt kernel_key".to_string(),
1088 ));
1089 }
1090
1091 let receipt_body = proof.receipt.body();
1092 let receipt_bytes = canonical_json_bytes(&receipt_body)
1093 .map_err(|error| Web3ContractError::InvalidProof(error.to_string()))?;
1094 let receipt_leaf = leaf_hash(&receipt_bytes);
1095 if !proof
1096 .receipt_inclusion
1097 .proof
1098 .verify_hash(receipt_leaf, &proof.receipt_inclusion.merkle_root)
1099 {
1100 return Err(Web3ContractError::InvalidProof(
1101 "receipt inclusion Merkle proof verification failed".to_string(),
1102 ));
1103 }
1104 if let Some(super_root) = proof.super_root_inclusion.as_ref() {
1105 if !super_root
1106 .proof
1107 .verify_hash(proof.receipt_inclusion.merkle_root, &super_root.super_root)
1108 {
1109 return Err(Web3ContractError::InvalidProof(
1110 "super-root inclusion verification failed".to_string(),
1111 ));
1112 }
1113 }
1114 Ok(())
1115}
1116
1117pub fn validate_web3_settlement_dispatch(
1118 dispatch: &Web3SettlementDispatchArtifact,
1119) -> Result<(), Web3ContractError> {
1120 if dispatch.schema != CHIO_WEB3_SETTLEMENT_DISPATCH_SCHEMA {
1121 return Err(Web3ContractError::UnsupportedSchema(
1122 dispatch.schema.clone(),
1123 ));
1124 }
1125 for field in [
1126 &dispatch.dispatch_id,
1127 &dispatch.trust_profile_id,
1128 &dispatch.contract_package_id,
1129 &dispatch.chain_id,
1130 &dispatch.escrow_id,
1131 &dispatch.escrow_contract,
1132 &dispatch.bond_vault_contract,
1133 &dispatch.beneficiary_address,
1134 ] {
1135 ensure_non_empty(field, "web3_settlement_dispatch.field")?;
1136 }
1137 ensure_money(
1138 &dispatch.settlement_amount,
1139 "web3_settlement_dispatch.settlement_amount",
1140 )?;
1141 if !dispatch.support_boundary.real_dispatch_supported {
1142 return Err(Web3ContractError::InvalidSettlement(
1143 "web3 settlement dispatch must explicitly mark real dispatch as supported".to_string(),
1144 ));
1145 }
1146 if !dispatch.support_boundary.custody_boundary_explicit {
1147 return Err(Web3ContractError::InvalidSettlement(
1148 "web3 settlement dispatch must keep custody boundaries explicit".to_string(),
1149 ));
1150 }
1151 if dispatch.settlement_path == Web3SettlementPath::MerkleProof
1152 && !dispatch.support_boundary.anchor_proof_required
1153 {
1154 return Err(Web3ContractError::InvalidSettlement(
1155 "Merkle-proof settlement dispatch must require anchor proof reconciliation".to_string(),
1156 ));
1157 }
1158 if dispatch.capital_instruction.body.schema != CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA {
1159 return Err(Web3ContractError::UnsupportedSchema(
1160 dispatch.capital_instruction.body.schema.clone(),
1161 ));
1162 }
1163 if dispatch.capital_instruction.body.action
1164 == CapitalExecutionInstructionAction::CancelInstruction
1165 {
1166 return Err(Web3ContractError::InvalidSettlement(
1167 "web3 settlement dispatch cannot use cancel_instruction as the primary action"
1168 .to_string(),
1169 ));
1170 }
1171 if dispatch.capital_instruction.body.rail.kind != CapitalExecutionRailKind::Web3 {
1172 return Err(Web3ContractError::InvalidSettlement(
1173 "web3 settlement dispatch requires capital_instruction rail.kind = web3".to_string(),
1174 ));
1175 }
1176 let Some(amount) = dispatch.capital_instruction.body.amount.as_ref() else {
1177 return Err(Web3ContractError::MissingField(
1178 "web3_settlement_dispatch.capital_instruction.amount",
1179 ));
1180 };
1181 if amount != &dispatch.settlement_amount {
1182 return Err(Web3ContractError::InvalidSettlement(
1183 "web3 settlement dispatch settlement_amount must match capital_instruction amount"
1184 .to_string(),
1185 ));
1186 }
1187 if dispatch.capital_instruction.body.reconciled_state
1188 != CapitalExecutionReconciledState::NotObserved
1189 {
1190 return Err(Web3ContractError::InvalidSettlement(
1191 "web3 settlement dispatch capital_instruction must remain unreconciled until execution receipt"
1192 .to_string(),
1193 ));
1194 }
1195 validate_transfer_completion_flow_binding(&dispatch.capital_instruction.body)?;
1196 if let Some(bond) = dispatch.bond.as_ref() {
1197 if bond.body.lifecycle_state != CreditBondLifecycleState::Active {
1198 return Err(Web3ContractError::InvalidSettlement(
1199 "web3 settlement dispatch requires an active bond when bond backing is present"
1200 .to_string(),
1201 ));
1202 }
1203 }
1204 Ok(())
1205}
1206
1207pub fn validate_web3_settlement_execution_receipt(
1208 receipt: &Web3SettlementExecutionReceiptArtifact,
1209) -> Result<(), Web3ContractError> {
1210 if receipt.schema != CHIO_WEB3_SETTLEMENT_RECEIPT_SCHEMA {
1211 return Err(Web3ContractError::UnsupportedSchema(receipt.schema.clone()));
1212 }
1213 ensure_non_empty(
1214 &receipt.execution_receipt_id,
1215 "web3_settlement_receipt.execution_receipt_id",
1216 )?;
1217 ensure_non_empty(
1218 &receipt.settlement_reference,
1219 "web3_settlement_receipt.settlement_reference",
1220 )?;
1221 validate_web3_settlement_dispatch(&receipt.dispatch)?;
1222 ensure_money(
1223 &receipt.observed_execution.amount,
1224 "web3_settlement_receipt.observed_amount",
1225 )?;
1226 ensure_money(
1227 &receipt.settled_amount,
1228 "web3_settlement_receipt.settled_amount",
1229 )?;
1230 if receipt.observed_execution.amount.currency != receipt.dispatch.settlement_amount.currency {
1231 return Err(Web3ContractError::InvalidSettlement(
1232 "observed execution currency must match dispatch settlement currency".to_string(),
1233 ));
1234 }
1235 if receipt.settled_amount.currency != receipt.dispatch.settlement_amount.currency {
1236 return Err(Web3ContractError::InvalidSettlement(
1237 "settled amount currency must match dispatch settlement currency".to_string(),
1238 ));
1239 }
1240 if receipt.observed_execution.amount != receipt.settled_amount {
1241 return Err(Web3ContractError::InvalidSettlement(
1242 "observed execution amount must equal settled_amount".to_string(),
1243 ));
1244 }
1245 if let Some(anchor_proof) = receipt.reconciled_anchor_proof.as_ref() {
1246 validate_anchor_inclusion_proof(anchor_proof)?;
1247 if let Some(chain_anchor) = anchor_proof.chain_anchor.as_ref() {
1248 if chain_anchor.chain_id != receipt.dispatch.chain_id {
1249 return Err(Web3ContractError::InvalidSettlement(
1250 "anchor proof chain_id must match settlement dispatch chain_id".to_string(),
1251 ));
1252 }
1253 }
1254 }
1255 if let Some(oracle_evidence) = receipt.oracle_evidence.as_ref() {
1256 validate_oracle_conversion_evidence(oracle_evidence)?;
1257 }
1258 if receipt
1259 .dispatch
1260 .support_boundary
1261 .oracle_evidence_required_for_fx
1262 && !matches!(
1263 receipt.lifecycle_state,
1264 Web3SettlementLifecycleState::TimedOut
1265 | Web3SettlementLifecycleState::Failed
1266 | Web3SettlementLifecycleState::Reorged
1267 )
1268 && receipt.oracle_evidence.is_none()
1269 {
1270 return Err(Web3ContractError::InvalidSettlement(
1271 "receipt requires oracle_evidence for FX-sensitive settlement paths".to_string(),
1272 ));
1273 }
1274
1275 match receipt.lifecycle_state {
1276 Web3SettlementLifecycleState::PendingDispatch
1277 | Web3SettlementLifecycleState::EscrowLocked => {
1278 return Err(Web3ContractError::InvalidSettlement(
1279 "execution receipts must record an observed terminal or reconciled lifecycle state"
1280 .to_string(),
1281 ));
1282 }
1283 Web3SettlementLifecycleState::PartiallySettled => {
1284 if receipt.settled_amount.units == 0
1285 || receipt.settled_amount.units >= receipt.dispatch.settlement_amount.units
1286 {
1287 return Err(Web3ContractError::InvalidSettlement(
1288 "partially_settled receipts must settle a non-zero amount smaller than the dispatch amount"
1289 .to_string(),
1290 ));
1291 }
1292 }
1293 Web3SettlementLifecycleState::Settled => {
1294 if receipt.settled_amount != receipt.dispatch.settlement_amount {
1295 return Err(Web3ContractError::InvalidSettlement(
1296 "settled receipts must match the dispatch settlement amount".to_string(),
1297 ));
1298 }
1299 }
1300 Web3SettlementLifecycleState::Reversed | Web3SettlementLifecycleState::ChargedBack => {
1301 ensure_non_empty(
1302 receipt.reversal_of.as_deref().unwrap_or_default(),
1303 "web3_settlement_receipt.reversal_of",
1304 )?;
1305 if !receipt.dispatch.support_boundary.reversal_supported {
1306 return Err(Web3ContractError::InvalidSettlement(
1307 "receipt records reversal state but dispatch did not declare reversal support"
1308 .to_string(),
1309 ));
1310 }
1311 }
1312 Web3SettlementLifecycleState::TimedOut
1313 | Web3SettlementLifecycleState::Failed
1314 | Web3SettlementLifecycleState::Reorged => {
1315 ensure_non_empty(
1316 receipt.failure_reason.as_deref().unwrap_or_default(),
1317 "web3_settlement_receipt.failure_reason",
1318 )?;
1319 }
1320 }
1321
1322 let must_have_anchor = receipt.dispatch.support_boundary.anchor_proof_required
1323 && !matches!(
1324 receipt.lifecycle_state,
1325 Web3SettlementLifecycleState::TimedOut | Web3SettlementLifecycleState::Failed
1326 );
1327 if must_have_anchor && receipt.reconciled_anchor_proof.is_none() {
1328 return Err(Web3ContractError::InvalidSettlement(
1329 "receipt requires reconciled anchor proof for the selected settlement path".to_string(),
1330 ));
1331 }
1332
1333 Ok(())
1334}
1335
1336pub fn validate_web3_qualification_matrix(
1337 matrix: &Web3QualificationMatrix,
1338) -> Result<(), Web3ContractError> {
1339 if matrix.schema != CHIO_WEB3_QUALIFICATION_MATRIX_SCHEMA {
1340 return Err(Web3ContractError::UnsupportedSchema(matrix.schema.clone()));
1341 }
1342 ensure_non_empty(
1343 &matrix.trust_profile_id,
1344 "web3_qualification_matrix.trust_profile_id",
1345 )?;
1346 ensure_non_empty(
1347 &matrix.contract_package_id,
1348 "web3_qualification_matrix.contract_package_id",
1349 )?;
1350 if matrix.cases.is_empty() {
1351 return Err(Web3ContractError::MissingField(
1352 "web3_qualification_matrix.cases",
1353 ));
1354 }
1355 let mut case_ids = HashSet::new();
1356 for case in &matrix.cases {
1357 ensure_non_empty(&case.id, "web3_qualification_matrix.case.id")?;
1358 ensure_non_empty(&case.name, "web3_qualification_matrix.case.name")?;
1359 ensure_non_empty(&case.notes, "web3_qualification_matrix.case.notes")?;
1360 if !case_ids.insert(case.id.as_str()) {
1361 return Err(Web3ContractError::DuplicateValue(case.id.clone()));
1362 }
1363 if case.requirement_ids.is_empty() {
1364 return Err(Web3ContractError::InvalidQualificationCase(format!(
1365 "case {} must cite at least one requirement id",
1366 case.id
1367 )));
1368 }
1369 ensure_unique_strings(
1370 &case.requirement_ids,
1371 "web3_qualification_matrix.case.requirement_ids",
1372 )?;
1373 }
1374 Ok(())
1375}
1376
1377#[derive(Debug, Clone, Serialize)]
1378struct Web3CheckpointStatementBody {
1379 schema: String,
1380 checkpoint_seq: u64,
1381 batch_start_seq: u64,
1382 batch_end_seq: u64,
1383 tree_size: u64,
1384 merkle_root: Hash,
1385 issued_at: u64,
1386 #[serde(default, skip_serializing_if = "Option::is_none")]
1387 previous_checkpoint_sha256: Option<String>,
1388 kernel_key: PublicKey,
1389}
1390
1391fn checkpoint_statement_body(statement: &Web3CheckpointStatement) -> Web3CheckpointStatementBody {
1392 Web3CheckpointStatementBody {
1393 schema: statement.schema.clone(),
1394 checkpoint_seq: statement.checkpoint_seq,
1395 batch_start_seq: statement.batch_start_seq,
1396 batch_end_seq: statement.batch_end_seq,
1397 tree_size: statement.tree_size,
1398 merkle_root: statement.merkle_root,
1399 issued_at: statement.issued_at,
1400 previous_checkpoint_sha256: statement.previous_checkpoint_sha256.clone(),
1401 kernel_key: statement.kernel_key.clone(),
1402 }
1403}
1404
1405fn ensure_non_empty(value: &str, field: &'static str) -> Result<(), Web3ContractError> {
1406 if value.trim().is_empty() {
1407 Err(Web3ContractError::MissingField(field))
1408 } else {
1409 Ok(())
1410 }
1411}
1412
1413fn validate_transfer_completion_flow_binding(
1414 instruction: &crate::credit::CapitalExecutionInstructionArtifact,
1415) -> Result<(), Web3ContractError> {
1416 if instruction.action != CapitalExecutionInstructionAction::TransferFunds {
1417 return Ok(());
1418 }
1419 let governed_receipt_id =
1420 instruction
1421 .governed_receipt_id
1422 .as_deref()
1423 .ok_or(Web3ContractError::MissingField(
1424 "web3_settlement_dispatch.capital_instruction.governed_receipt_id",
1425 ))?;
1426 ensure_non_empty(
1427 governed_receipt_id,
1428 "web3_settlement_dispatch.capital_instruction.governed_receipt_id",
1429 )?;
1430 let completion_flow_row_id =
1431 instruction
1432 .completion_flow_row_id
1433 .as_deref()
1434 .ok_or(Web3ContractError::MissingField(
1435 "web3_settlement_dispatch.capital_instruction.completion_flow_row_id",
1436 ))?;
1437 ensure_non_empty(
1438 completion_flow_row_id,
1439 "web3_settlement_dispatch.capital_instruction.completion_flow_row_id",
1440 )?;
1441 let expected_row_id = format!("economic-completion-flow:{governed_receipt_id}");
1442 if completion_flow_row_id != expected_row_id {
1443 return Err(Web3ContractError::InvalidSettlement(
1444 "web3 settlement dispatch completion_flow_row_id must match governed_receipt_id"
1445 .to_string(),
1446 ));
1447 }
1448 Ok(())
1449}
1450
1451fn ensure_unique_strings(values: &[String], field: &'static str) -> Result<(), Web3ContractError> {
1452 let mut seen = HashSet::new();
1453 for value in values {
1454 ensure_non_empty(value, field)?;
1455 if !seen.insert(value.as_str()) {
1456 return Err(Web3ContractError::DuplicateValue(value.clone()));
1457 }
1458 }
1459 Ok(())
1460}
1461
1462fn ensure_unique_copy_values<T>(values: &[T], field: &'static str) -> Result<(), Web3ContractError>
1463where
1464 T: Eq + std::hash::Hash + Copy + std::fmt::Debug,
1465{
1466 let mut seen = HashSet::new();
1467 for value in values {
1468 if !seen.insert(*value) {
1469 return Err(Web3ContractError::DuplicateValue(format!(
1470 "{field}:{value:?}"
1471 )));
1472 }
1473 }
1474 Ok(())
1475}
1476
1477fn ensure_money(amount: &MonetaryAmount, field: &'static str) -> Result<(), Web3ContractError> {
1478 if amount.units == 0 {
1479 return Err(Web3ContractError::InvalidSettlement(format!(
1480 "{field} must be non-zero"
1481 )));
1482 }
1483 ensure_non_empty(&amount.currency, field)
1484 .map_err(|_| Web3ContractError::InvalidSettlement(format!("{field} currency is required")))
1485}
1486
1487#[cfg(test)]
1488#[allow(clippy::unwrap_used, clippy::expect_used)]
1489mod tests {
1490 use serde_json::json;
1491
1492 use super::*;
1493 use crate::capability::MonetaryAmount;
1494 use crate::credit::{
1495 CapitalBookEvidenceKind, CapitalBookEvidenceReference, CapitalBookQuery,
1496 CapitalBookSourceKind, CapitalExecutionAuthorityStep, CapitalExecutionInstructionAction,
1497 CapitalExecutionInstructionSupportBoundary, CapitalExecutionIntendedState,
1498 CapitalExecutionObservation, CapitalExecutionRail, CapitalExecutionRole,
1499 CapitalExecutionWindow, SignedCapitalExecutionInstruction,
1500 CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA,
1501 };
1502 use crate::crypto::{sha256_hex, Keypair};
1503 use crate::merkle::MerkleTree;
1504 use crate::receipt::{ChioReceipt, ChioReceiptBody, Decision, ToolCallAction};
1505
1506 fn operator_keypair() -> Keypair {
1507 Keypair::from_seed(&[7u8; 32])
1508 }
1509
1510 fn treasury_keypair() -> Keypair {
1511 Keypair::from_seed(&[9u8; 32])
1512 }
1513
1514 fn sample_binding() -> SignedWeb3IdentityBinding {
1515 let operator = operator_keypair();
1516 let certificate = Web3IdentityBindingCertificate {
1517 schema: CHIO_KEY_BINDING_CERTIFICATE_SCHEMA.to_string(),
1518 chio_identity: format!("did:chio:{}", operator.public_key().to_hex()),
1519 chio_public_key: operator.public_key(),
1520 chain_scope: vec!["eip155:8453".to_string(), "eip155:42161".to_string()],
1521 purpose: vec![Web3KeyBindingPurpose::Anchor, Web3KeyBindingPurpose::Settle],
1522 settlement_address: "0x1111111111111111111111111111111111111111".to_string(),
1523 issued_at: 1_743_292_800,
1524 expires_at: 1_774_828_800,
1525 nonce: "0123456789abcdef0123456789abcdef".to_string(),
1526 };
1527 let (signature, _) = operator.sign_canonical(&certificate).unwrap();
1528 SignedWeb3IdentityBinding {
1529 certificate,
1530 signature,
1531 }
1532 }
1533
1534 fn sample_trust_profile() -> Web3TrustProfile {
1535 Web3TrustProfile {
1536 schema: CHIO_WEB3_TRUST_PROFILE_SCHEMA.to_string(),
1537 profile_id: "chio.official-web3-stack".to_string(),
1538 chio_contract_version: "2.0".to_string(),
1539 primary_chain_id: "eip155:8453".to_string(),
1540 secondary_chain_ids: vec!["eip155:42161".to_string()],
1541 operator_binding: sample_binding(),
1542 proof_bundle_required: true,
1543 dispute_windows: vec![
1544 Web3DisputeWindow {
1545 settlement_path: Web3SettlementPath::DualSignature,
1546 challenge_window_secs: 600,
1547 recovery_window_secs: 3_600,
1548 dispute_policy: Web3DisputePolicy::OffChainArbitration,
1549 },
1550 Web3DisputeWindow {
1551 settlement_path: Web3SettlementPath::MerkleProof,
1552 challenge_window_secs: 900,
1553 recovery_window_secs: 86_400,
1554 dispute_policy: Web3DisputePolicy::TimeoutRefund,
1555 },
1556 ],
1557 finality_rules: vec![
1558 Web3ChainFinalityRule {
1559 chain_id: "eip155:8453".to_string(),
1560 mode: Web3FinalityMode::OptimisticL2,
1561 min_confirmations: 20,
1562 },
1563 Web3ChainFinalityRule {
1564 chain_id: "eip155:42161".to_string(),
1565 mode: Web3FinalityMode::L1Finalized,
1566 min_confirmations: 12,
1567 },
1568 ],
1569 regulated_roles: vec![
1570 Web3RegulatedRoleAssumption {
1571 role: Web3RegulatedRole::Operator,
1572 actor_id: "chio-operator-main".to_string(),
1573 responsibility: "Originates governed dispatch and maintains local policy activation."
1574 .to_string(),
1575 custody_boundary_explicit: true,
1576 },
1577 Web3RegulatedRoleAssumption {
1578 role: Web3RegulatedRole::Custodian,
1579 actor_id: "custodian-base-main".to_string(),
1580 responsibility: "Holds settlement-side keys and custody accounts for the official stack."
1581 .to_string(),
1582 custody_boundary_explicit: true,
1583 },
1584 Web3RegulatedRoleAssumption {
1585 role: Web3RegulatedRole::Arbitrator,
1586 actor_id: "settlement-dispute-panel".to_string(),
1587 responsibility: "Handles off-chain challenge and reversal review during dispute windows."
1588 .to_string(),
1589 custody_boundary_explicit: true,
1590 },
1591 ],
1592 custody_boundary_note:
1593 "Chio governs intent, proofs, and policy admission; custodians and payment institutions remain explicit operators of record."
1594 .to_string(),
1595 local_policy_activation_required: true,
1596 }
1597 }
1598
1599 fn sample_oracle_evidence() -> OracleConversionEvidence {
1600 OracleConversionEvidence {
1601 schema: CHIO_ORACLE_CONVERSION_EVIDENCE_SCHEMA.to_string(),
1602 base: "ETH".to_string(),
1603 quote: "USD".to_string(),
1604 authority: CHIO_LINK_ORACLE_AUTHORITY.to_string(),
1605 rate_numerator: 300_000,
1606 rate_denominator: 100,
1607 source: "chainlink".to_string(),
1608 feed_address: "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612".to_string(),
1609 updated_at: 1_743_292_740,
1610 max_age_seconds: 3_600,
1611 cache_age_seconds: 45,
1612 converted_cost_units: 300,
1613 original_cost_units: 100_000_000_000_000,
1614 original_currency: "ETH".to_string(),
1615 grant_currency: "USD".to_string(),
1616 }
1617 }
1618
1619 fn sample_receipt() -> ChioReceipt {
1620 let operator = operator_keypair();
1621 let parameters = json!({
1622 "to": "0x2222222222222222222222222222222222222222",
1623 "amount": 150,
1624 "currency": "USDC"
1625 });
1626 let action = ToolCallAction::from_parameters(parameters).unwrap();
1627 let body = ChioReceiptBody {
1628 id: "rcpt-web3-1".to_string(),
1629 timestamp: 1_743_292_800,
1630 capability_id: "cap-web3-1".to_string(),
1631 tool_server: "chio-settle".to_string(),
1632 tool_name: "release_escrow".to_string(),
1633 action,
1634 decision: Decision::Allow,
1635 content_hash: sha256_hex(b"web3-settlement"),
1636 policy_hash: sha256_hex(b"policy-web3"),
1637 evidence: vec![],
1638 metadata: Some(json!({
1639 "financial": {
1640 "grant_index": 0,
1641 "cost_charged": 150,
1642 "currency": "USD",
1643 "budget_remaining": 850,
1644 "budget_total": 1000,
1645 "delegation_depth": 1,
1646 "root_budget_holder": "subject-1",
1647 "payment_reference": "escrow-1",
1648 "settlement_status": "pending",
1649 "oracle_evidence": sample_oracle_evidence()
1650 }
1651 })),
1652 trust_level: chio_core_types::receipt::TrustLevel::default(),
1653 tenant_id: None,
1654 kernel_key: operator.public_key(),
1655 };
1656 ChioReceipt::sign(body, &operator).unwrap()
1657 }
1658
1659 fn sample_anchor_inclusion_proof() -> AnchorInclusionProof {
1660 let operator = operator_keypair();
1661 let receipt = sample_receipt();
1662 let receipt_body = receipt.body();
1663 let receipt_bytes = canonical_json_bytes(&receipt_body).unwrap();
1664 let tree = MerkleTree::from_leaves(&[receipt_bytes]).unwrap();
1665 let merkle_root = tree.root();
1666 let inclusion = Web3ReceiptInclusion {
1667 checkpoint_seq: 1_042,
1668 merkle_root,
1669 proof: tree.inclusion_proof(0).unwrap(),
1670 };
1671 let mut statement = Web3CheckpointStatement {
1672 schema: CHIO_CHECKPOINT_STATEMENT_SCHEMA.to_string(),
1673 checkpoint_seq: 1_042,
1674 batch_start_seq: 104_101,
1675 batch_end_seq: 104_200,
1676 tree_size: 1,
1677 merkle_root,
1678 issued_at: 1_743_292_800,
1679 previous_checkpoint_sha256: None,
1680 kernel_key: operator.public_key(),
1681 signature: Signature::from_hex(
1682 "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1683 )
1684 .unwrap(),
1685 };
1686 let body = checkpoint_statement_body(&statement);
1687 let (signature, _) = operator.sign_canonical(&body).unwrap();
1688 statement.signature = signature;
1689
1690 AnchorInclusionProof {
1691 schema: CHIO_ANCHOR_INCLUSION_PROOF_SCHEMA.to_string(),
1692 receipt,
1693 receipt_inclusion: inclusion,
1694 checkpoint_statement: statement,
1695 chain_anchor: Some(Web3ChainAnchorRecord {
1696 chain_id: "eip155:8453".to_string(),
1697 contract_address: "0x1000000000000000000000000000000000000001".to_string(),
1698 operator_address: "0x1111111111111111111111111111111111111111".to_string(),
1699 tx_hash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1700 .to_string(),
1701 block_number: 12_345_678,
1702 block_hash: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1703 .to_string(),
1704 anchored_merkle_root: merkle_root,
1705 anchored_checkpoint_seq: 1_042,
1706 }),
1707 bitcoin_anchor: None,
1708 super_root_inclusion: None,
1709 key_binding_certificate: sample_binding(),
1710 }
1711 }
1712
1713 fn sample_capital_instruction() -> SignedCapitalExecutionInstruction {
1714 let signer = treasury_keypair();
1715 SignedCapitalExecutionInstruction::sign(
1716 crate::credit::CapitalExecutionInstructionArtifact {
1717 schema: CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA.to_string(),
1718 instruction_id: "cei-web3-1".to_string(),
1719 issued_at: 1_743_292_800,
1720 query: CapitalBookQuery {
1721 agent_subject: Some("subject-1".to_string()),
1722 ..CapitalBookQuery::default()
1723 },
1724 subject_key: "subject-1".to_string(),
1725 source_id: "capital-source:facility:facility-1".to_string(),
1726 source_kind: CapitalBookSourceKind::FacilityCommitment,
1727 governed_receipt_id: Some("rcpt-web3-1".to_string()),
1728 completion_flow_row_id: Some("economic-completion-flow:rcpt-web3-1".to_string()),
1729 action: CapitalExecutionInstructionAction::TransferFunds,
1730 owner_role: CapitalExecutionRole::OperatorTreasury,
1731 counterparty_role: CapitalExecutionRole::AgentCounterparty,
1732 counterparty_id: "subject-1".to_string(),
1733 amount: Some(MonetaryAmount {
1734 units: 150,
1735 currency: "USD".to_string(),
1736 }),
1737 authority_chain: vec![
1738 CapitalExecutionAuthorityStep {
1739 role: CapitalExecutionRole::OperatorTreasury,
1740 principal_id: "treasury-1".to_string(),
1741 approved_at: 1_743_292_790,
1742 expires_at: 1_743_293_800,
1743 note: Some("governed release".to_string()),
1744 },
1745 CapitalExecutionAuthorityStep {
1746 role: CapitalExecutionRole::Custodian,
1747 principal_id: "custodian-base-main".to_string(),
1748 approved_at: 1_743_292_795,
1749 expires_at: 1_743_293_800,
1750 note: Some("official web3 stack".to_string()),
1751 },
1752 ],
1753 execution_window: CapitalExecutionWindow {
1754 not_before: 1_743_292_800,
1755 not_after: 1_743_293_800,
1756 },
1757 rail: CapitalExecutionRail {
1758 kind: CapitalExecutionRailKind::Web3,
1759 rail_id: "base-mainnet-usdc".to_string(),
1760 custody_provider_id: "custodian-base-main".to_string(),
1761 source_account_ref: Some("vault:facility-main".to_string()),
1762 destination_account_ref: Some(
1763 "0x2222222222222222222222222222222222222222".to_string(),
1764 ),
1765 jurisdiction: Some("US".to_string()),
1766 },
1767 intended_state: CapitalExecutionIntendedState::PendingExecution,
1768 reconciled_state: CapitalExecutionReconciledState::NotObserved,
1769 related_instruction_id: None,
1770 observed_execution: None,
1771 support_boundary: CapitalExecutionInstructionSupportBoundary {
1772 capital_book_authoritative: true,
1773 external_execution_authoritative: false,
1774 automatic_dispatch_supported: true,
1775 custody_neutral_instruction_supported: false,
1776 },
1777 evidence_refs: vec![CapitalBookEvidenceReference {
1778 kind: CapitalBookEvidenceKind::Receipt,
1779 reference_id: "rcpt-web3-1".to_string(),
1780 observed_at: Some(1_743_292_800),
1781 locator: Some("receipt:rcpt-web3-1".to_string()),
1782 }],
1783 description: "release escrow over the official web3 rail".to_string(),
1784 },
1785 &signer,
1786 )
1787 .unwrap()
1788 }
1789
1790 fn sample_dispatch() -> Web3SettlementDispatchArtifact {
1791 Web3SettlementDispatchArtifact {
1792 schema: CHIO_WEB3_SETTLEMENT_DISPATCH_SCHEMA.to_string(),
1793 dispatch_id: "dispatch-web3-1".to_string(),
1794 issued_at: 1_743_292_800,
1795 trust_profile_id: "chio.official-web3-stack".to_string(),
1796 contract_package_id: "chio.official-web3-contracts".to_string(),
1797 chain_id: "eip155:8453".to_string(),
1798 capital_instruction: sample_capital_instruction(),
1799 bond: None,
1800 settlement_path: Web3SettlementPath::MerkleProof,
1801 settlement_amount: MonetaryAmount {
1802 units: 150,
1803 currency: "USD".to_string(),
1804 },
1805 escrow_id: "escrow-web3-1".to_string(),
1806 escrow_contract: "0x1000000000000000000000000000000000000002".to_string(),
1807 bond_vault_contract: "0x1000000000000000000000000000000000000003".to_string(),
1808 beneficiary_address: "0x2222222222222222222222222222222222222222".to_string(),
1809 support_boundary: Web3SettlementSupportBoundary {
1810 real_dispatch_supported: true,
1811 anchor_proof_required: true,
1812 oracle_evidence_required_for_fx: true,
1813 custody_boundary_explicit: true,
1814 reversal_supported: true,
1815 },
1816 note: Some(
1817 "Dispatches one governed escrow release over the official Base-first contract stack."
1818 .to_string(),
1819 ),
1820 }
1821 }
1822
1823 fn sample_execution_receipt() -> Web3SettlementExecutionReceiptArtifact {
1824 Web3SettlementExecutionReceiptArtifact {
1825 schema: CHIO_WEB3_SETTLEMENT_RECEIPT_SCHEMA.to_string(),
1826 execution_receipt_id: "receipt-web3-1".to_string(),
1827 issued_at: 1_743_292_860,
1828 dispatch: sample_dispatch(),
1829 observed_execution: CapitalExecutionObservation {
1830 observed_at: 1_743_292_860,
1831 external_reference_id:
1832 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1833 .to_string(),
1834 amount: MonetaryAmount {
1835 units: 150,
1836 currency: "USD".to_string(),
1837 },
1838 },
1839 lifecycle_state: Web3SettlementLifecycleState::Settled,
1840 settlement_reference: "settlement-web3-1".to_string(),
1841 reconciled_anchor_proof: Some(sample_anchor_inclusion_proof()),
1842 oracle_evidence: Some(sample_oracle_evidence()),
1843 settled_amount: MonetaryAmount {
1844 units: 150,
1845 currency: "USD".to_string(),
1846 },
1847 reversal_of: None,
1848 failure_reason: None,
1849 note: Some(
1850 "Settled against an anchored receipt root and retained oracle provenance for the FX conversion."
1851 .to_string(),
1852 ),
1853 }
1854 }
1855
1856 #[test]
1857 fn trust_profile_requires_local_policy_activation() {
1858 let mut profile = sample_trust_profile();
1859 profile.local_policy_activation_required = false;
1860 assert!(matches!(
1861 validate_web3_trust_profile(&profile),
1862 Err(Web3ContractError::InvalidBinding(_))
1863 ));
1864 }
1865
1866 #[test]
1867 fn identity_binding_signature_verifies() {
1868 verify_web3_identity_binding(&sample_binding()).unwrap();
1869 }
1870
1871 #[test]
1872 fn anchor_inclusion_proof_verifies_receipt_and_merkle_root() {
1873 verify_anchor_inclusion_proof(&sample_anchor_inclusion_proof()).unwrap();
1874 }
1875
1876 #[test]
1877 fn oracle_evidence_requires_non_zero_denominator() {
1878 let mut evidence = sample_oracle_evidence();
1879 evidence.rate_denominator = 0;
1880 assert!(matches!(
1881 validate_oracle_conversion_evidence(&evidence),
1882 Err(Web3ContractError::InvalidProof(_))
1883 ));
1884 }
1885
1886 #[test]
1887 fn oracle_evidence_rejects_unknown_authority() {
1888 let mut evidence = sample_oracle_evidence();
1889 evidence.authority = "unknown_authority".to_string();
1890 assert!(matches!(
1891 validate_oracle_conversion_evidence(&evidence),
1892 Err(Web3ContractError::InvalidProof(_))
1893 ));
1894 }
1895
1896 #[test]
1897 fn web3_dispatch_requires_web3_rail_kind() {
1898 let mut dispatch = sample_dispatch();
1899 dispatch.capital_instruction.body.rail.kind = CapitalExecutionRailKind::Api;
1900 assert!(matches!(
1901 validate_web3_settlement_dispatch(&dispatch),
1902 Err(Web3ContractError::InvalidSettlement(_))
1903 ));
1904 }
1905
1906 #[test]
1907 fn web3_dispatch_requires_completion_flow_binding_for_transfers() {
1908 let mut dispatch = sample_dispatch();
1909 dispatch.capital_instruction.body.completion_flow_row_id = None;
1910 assert!(matches!(
1911 validate_web3_settlement_dispatch(&dispatch),
1912 Err(Web3ContractError::MissingField(
1913 "web3_settlement_dispatch.capital_instruction.completion_flow_row_id"
1914 ))
1915 ));
1916 }
1917
1918 #[test]
1919 fn web3_dispatch_rejects_mismatched_completion_flow_binding() {
1920 let mut dispatch = sample_dispatch();
1921 dispatch.capital_instruction.body.completion_flow_row_id =
1922 Some("economic-completion-flow:other-receipt".to_string());
1923 assert!(matches!(
1924 validate_web3_settlement_dispatch(&dispatch),
1925 Err(Web3ContractError::InvalidSettlement(_))
1926 ));
1927 }
1928
1929 #[test]
1930 fn merkle_settlement_receipt_requires_anchor_proof() {
1931 let mut receipt = sample_execution_receipt();
1932 receipt.reconciled_anchor_proof = None;
1933 assert!(matches!(
1934 validate_web3_settlement_execution_receipt(&receipt),
1935 Err(Web3ContractError::InvalidSettlement(_))
1936 ));
1937 }
1938
1939 #[test]
1940 fn fx_sensitive_settlement_receipt_requires_oracle_evidence() {
1941 let mut receipt = sample_execution_receipt();
1942 receipt.oracle_evidence = None;
1943 assert!(matches!(
1944 validate_web3_settlement_execution_receipt(&receipt),
1945 Err(Web3ContractError::InvalidSettlement(_))
1946 ));
1947 }
1948
1949 #[test]
1950 fn reference_artifacts_parse_and_validate() {
1951 let trust_profile: Web3TrustProfile = serde_json::from_str(include_str!(
1952 "../../../docs/standards/CHIO_WEB3_TRUST_PROFILE.json"
1953 ))
1954 .unwrap();
1955 let contract_package: Web3ContractPackage = serde_json::from_str(include_str!(
1956 "../../../docs/standards/CHIO_WEB3_CONTRACT_PACKAGE.json"
1957 ))
1958 .unwrap();
1959 let chain_configuration: Web3ChainConfiguration = serde_json::from_str(include_str!(
1960 "../../../docs/standards/CHIO_WEB3_CHAIN_CONFIGURATION.json"
1961 ))
1962 .unwrap();
1963 let anchor_proof: AnchorInclusionProof = serde_json::from_str(include_str!(
1964 "../../../docs/standards/CHIO_ANCHOR_INCLUSION_PROOF_EXAMPLE.json"
1965 ))
1966 .unwrap();
1967 let dispatch: Web3SettlementDispatchArtifact = serde_json::from_str(include_str!(
1968 "../../../docs/standards/CHIO_WEB3_SETTLEMENT_DISPATCH_EXAMPLE.json"
1969 ))
1970 .unwrap();
1971 let receipt: Web3SettlementExecutionReceiptArtifact = serde_json::from_str(include_str!(
1972 "../../../docs/standards/CHIO_WEB3_SETTLEMENT_RECEIPT_EXAMPLE.json"
1973 ))
1974 .unwrap();
1975 let matrix: Web3QualificationMatrix = serde_json::from_str(include_str!(
1976 "../../../docs/standards/CHIO_WEB3_QUALIFICATION_MATRIX.json"
1977 ))
1978 .unwrap();
1979
1980 validate_web3_trust_profile(&trust_profile).unwrap();
1981 verify_web3_identity_binding(&trust_profile.operator_binding).unwrap();
1982 validate_web3_contract_package(&contract_package).unwrap();
1983 validate_web3_chain_configuration(&chain_configuration).unwrap();
1984 validate_anchor_inclusion_proof(&anchor_proof).unwrap();
1985 verify_anchor_inclusion_proof(&anchor_proof).unwrap();
1986 validate_web3_settlement_dispatch(&dispatch).unwrap();
1987 validate_web3_settlement_execution_receipt(&receipt).unwrap();
1988 validate_web3_qualification_matrix(&matrix).unwrap();
1989 }
1990}