Skip to main content

chio_settle/channel/
funding.rs

1use chio_core::crypto::PublicKey;
2use chio_core::web3::trust_profile::Web3FinalityMode;
3use serde::{Deserialize, Serialize};
4
5use super::validation::{
6    digest, parse_base_units, validate_chain_id, validate_digest, validate_evm_address,
7    validate_evm_hash, validate_positive, validate_text, I_JSON_MAX_SAFE_INTEGER,
8};
9use super::{ChannelAssetBindingV1, ChannelError, ChannelSignatureV1};
10
11pub const CHANNEL_FUNDING_EVIDENCE_SCHEMA: &str = "chio.channel.funding-evidence.v1";
12
13const ESCROW_CREATED_EVENT_SIGNATURE: &str =
14    "EscrowCreated(bytes32,bytes32,address,address,address,uint256,uint256,address)";
15
16const FUNDING_EVIDENCE_BODY_DIGEST_DOMAIN: &[u8] =
17    b"chio.channel.funding-evidence.body.digest.v1\0";
18const FUNDING_EVIDENCE_DIGEST_DOMAIN: &[u8] = b"chio.channel.funding-evidence.digest.v1\0";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ChannelFinalityStatusV1 {
23    Finalized,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase", deny_unknown_fields)]
28pub struct ChannelEscrowReferenceV1 {
29    pub chain_id: String,
30    pub escrow_contract: String,
31    pub escrow_id: String,
32}
33
34impl ChannelEscrowReferenceV1 {
35    pub fn validate(&self) -> Result<(), ChannelError> {
36        validate_chain_id(&self.chain_id)?;
37        validate_evm_address("escrow_contract", &self.escrow_contract)?;
38        validate_evm_hash("escrow_id", &self.escrow_id)
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase", deny_unknown_fields)]
44pub struct ChannelEscrowTermsV1 {
45    pub capability_id: String,
46    pub depositor: String,
47    pub beneficiary: String,
48    pub token_address: String,
49    pub max_token_base_units: String,
50    pub deadline_unix_secs: u64,
51    pub operator: String,
52    pub operator_key_hash: String,
53}
54
55impl ChannelEscrowTermsV1 {
56    fn validate(&self) -> Result<(), ChannelError> {
57        validate_evm_hash("capability_id", &self.capability_id)?;
58        validate_evm_address("depositor", &self.depositor)?;
59        validate_evm_address("beneficiary", &self.beneficiary)?;
60        validate_evm_address("escrow_token", &self.token_address)?;
61        parse_base_units(&self.max_token_base_units)?;
62        validate_positive("escrow_deadline", self.deadline_unix_secs)?;
63        validate_evm_address("escrow_operator", &self.operator)?;
64        validate_evm_hash("operator_key_hash", &self.operator_key_hash)
65    }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70pub struct ChannelEscrowStateV1 {
71    pub deposited_token_base_units: String,
72    pub released_token_base_units: String,
73    pub refunded_token_base_units: String,
74    pub refunded: bool,
75}
76
77impl ChannelEscrowStateV1 {
78    pub fn validate(&self) -> Result<(), ChannelError> {
79        let deposited = parse_base_units(&self.deposited_token_base_units)?;
80        let released = parse_base_units(&self.released_token_base_units)?;
81        let refunded = parse_base_units(&self.refunded_token_base_units)?;
82        let total = released
83            .checked_add(refunded)
84            .ok_or(ChannelError::ArithmeticOverflow)?;
85        if total > deposited
86            || !self.refunded && refunded != 0
87            || self.refunded && total != deposited
88        {
89            return Err(ChannelError::InvalidField("escrow_state"));
90        }
91        Ok(())
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase", deny_unknown_fields)]
97pub struct ChannelEscrowCreatedEventV1 {
98    pub transaction_hash: String,
99    pub transaction_to: String,
100    pub transaction_succeeded: bool,
101    pub receipt_block_number: u64,
102    pub receipt_block_hash: String,
103    pub log_emitter: String,
104    pub log_index: u64,
105    pub event_signature: String,
106    pub escrow_id: String,
107    pub capability_id: String,
108    pub depositor: String,
109    pub beneficiary: String,
110    pub token_address: String,
111    pub max_token_base_units: String,
112    pub deadline_unix_secs: u64,
113    pub operator: String,
114}
115
116impl ChannelEscrowCreatedEventV1 {
117    fn validate(&self) -> Result<(), ChannelError> {
118        validate_evm_hash("creation_transaction_hash", &self.transaction_hash)?;
119        validate_evm_address("creation_transaction_to", &self.transaction_to)?;
120        validate_positive("creation_receipt_block_number", self.receipt_block_number)?;
121        validate_evm_hash("creation_receipt_block_hash", &self.receipt_block_hash)?;
122        validate_evm_address("creation_log_emitter", &self.log_emitter)?;
123        if self.log_index > I_JSON_MAX_SAFE_INTEGER {
124            return Err(ChannelError::InvalidField("creation_log_index"));
125        }
126        validate_evm_hash("creation_event_signature", &self.event_signature)?;
127        validate_evm_hash("creation_escrow_id", &self.escrow_id)?;
128        validate_evm_hash("creation_capability_id", &self.capability_id)?;
129        validate_evm_address("creation_depositor", &self.depositor)?;
130        validate_evm_address("creation_beneficiary", &self.beneficiary)?;
131        validate_evm_address("creation_token", &self.token_address)?;
132        parse_base_units(&self.max_token_base_units)?;
133        validate_positive("creation_deadline", self.deadline_unix_secs)?;
134        validate_evm_address("creation_operator", &self.operator)?;
135        if !self.transaction_succeeded
136            || self.event_signature != channel_escrow_created_event_signature()
137        {
138            return Err(ChannelError::InvalidField("creation_transaction_status"));
139        }
140        Ok(())
141    }
142}
143
144pub fn channel_escrow_created_event_signature() -> String {
145    format!(
146        "{:#x}",
147        alloy_primitives::keccak256(ESCROW_CREATED_EVENT_SIGNATURE.as_bytes())
148    )
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase", deny_unknown_fields)]
153pub struct ChannelPinnedStateReadV1 {
154    pub contract: String,
155    pub block_number: u64,
156    pub block_hash: String,
157    pub call_data_digest: String,
158    pub return_data_digest: String,
159}
160
161impl ChannelPinnedStateReadV1 {
162    fn validate(&self) -> Result<(), ChannelError> {
163        validate_evm_address("state_read_contract", &self.contract)?;
164        validate_positive("state_read_block_number", self.block_number)?;
165        validate_evm_hash("state_read_block_hash", &self.block_hash)?;
166        validate_evm_hash("state_read_call_data_digest", &self.call_data_digest)?;
167        validate_evm_hash("state_read_return_data_digest", &self.return_data_digest)
168    }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase", deny_unknown_fields)]
173pub struct ChannelTokenObservationV1 {
174    pub token_address: String,
175    pub token_symbol: String,
176    pub token_decimals: u8,
177    pub allowed: bool,
178    pub escrow_contract: String,
179    pub block_number: u64,
180    pub block_hash: String,
181}
182
183impl ChannelTokenObservationV1 {
184    fn validate(&self) -> Result<(), ChannelError> {
185        validate_evm_address("observed_token_address", &self.token_address)?;
186        validate_text("observed_token_symbol", &self.token_symbol)?;
187        validate_evm_address("observed_token_escrow_contract", &self.escrow_contract)?;
188        validate_positive("observed_token_block_number", self.block_number)?;
189        validate_evm_hash("observed_token_block_hash", &self.block_hash)?;
190        if !self.allowed {
191            return Err(ChannelError::InvalidField("observed_token_allowed"));
192        }
193        Ok(())
194    }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase", deny_unknown_fields)]
199pub struct ChannelIdentityRegistryObservationV1 {
200    pub registry_contract: String,
201    pub operator: String,
202    pub active: bool,
203    pub operator_key_hash: String,
204    pub block_number: u64,
205    pub block_hash: String,
206}
207
208impl ChannelIdentityRegistryObservationV1 {
209    fn validate(&self) -> Result<(), ChannelError> {
210        validate_evm_address("identity_registry_contract", &self.registry_contract)?;
211        validate_evm_address("identity_operator", &self.operator)?;
212        validate_evm_hash("identity_operator_key_hash", &self.operator_key_hash)?;
213        validate_positive("identity_block_number", self.block_number)?;
214        validate_evm_hash("identity_block_hash", &self.block_hash)?;
215        if !self.active {
216            return Err(ChannelError::InvalidField("identity_operator_active"));
217        }
218        Ok(())
219    }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase", deny_unknown_fields)]
224pub struct ChannelBlockPinV1 {
225    pub block_number: u64,
226    pub block_hash: String,
227    pub block_timestamp_unix_secs: u64,
228    pub observed_at_unix_ms: u64,
229    pub required_confirmations: u64,
230    pub observed_confirmations: u64,
231    pub finalized_head_number: u64,
232    pub finalized_head_hash: String,
233    pub finality_mode: Web3FinalityMode,
234    pub finality_status: ChannelFinalityStatusV1,
235}
236
237impl ChannelBlockPinV1 {
238    fn validate(&self) -> Result<(), ChannelError> {
239        validate_positive("funding_block_number", self.block_number)?;
240        validate_evm_hash("funding_block_hash", &self.block_hash)?;
241        validate_positive("funding_block_timestamp", self.block_timestamp_unix_secs)?;
242        validate_positive("funding_observed_at", self.observed_at_unix_ms)?;
243        validate_positive(
244            "funding_required_confirmations",
245            self.required_confirmations,
246        )?;
247        validate_positive("funding_finalized_head_number", self.finalized_head_number)?;
248        validate_evm_hash("funding_finalized_head_hash", &self.finalized_head_hash)?;
249        let confirmations = self
250            .finalized_head_number
251            .checked_sub(self.block_number)
252            .ok_or(ChannelError::ArithmeticOverflow)?;
253        if self.observed_confirmations < self.required_confirmations
254            || self.observed_confirmations != confirmations
255            || self.finality_mode != Web3FinalityMode::L1Finalized
256            || self
257                .block_timestamp_unix_secs
258                .checked_mul(1_000)
259                .filter(|timestamp| *timestamp <= self.observed_at_unix_ms)
260                .is_none()
261        {
262            return Err(ChannelError::InvalidField("funding_finality"));
263        }
264        Ok(())
265    }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase", deny_unknown_fields)]
270pub struct ChannelFundingEvidenceBodyV1 {
271    pub schema: String,
272    pub escrow_reference: ChannelEscrowReferenceV1,
273    pub escrow_terms: ChannelEscrowTermsV1,
274    pub escrow_state: ChannelEscrowStateV1,
275    pub escrow_state_read: ChannelPinnedStateReadV1,
276    pub creation_event: ChannelEscrowCreatedEventV1,
277    pub identity_observation: ChannelIdentityRegistryObservationV1,
278    pub token_observation: ChannelTokenObservationV1,
279    pub asset_binding: ChannelAssetBindingV1,
280    pub block_pin: ChannelBlockPinV1,
281    pub evidence_expires_at_unix_ms: u64,
282}
283
284impl ChannelFundingEvidenceBodyV1 {
285    pub fn validate(&self) -> Result<(), ChannelError> {
286        if self.schema != CHANNEL_FUNDING_EVIDENCE_SCHEMA {
287            return Err(ChannelError::InvalidField("funding_evidence_schema"));
288        }
289        self.escrow_reference.validate()?;
290        self.escrow_terms.validate()?;
291        self.escrow_state.validate()?;
292        self.escrow_state_read.validate()?;
293        self.creation_event.validate()?;
294        self.identity_observation.validate()?;
295        self.token_observation.validate()?;
296        self.asset_binding.validate()?;
297        self.block_pin.validate()?;
298        validate_positive("funding_evidence_expiry", self.evidence_expires_at_unix_ms)?;
299        if self.escrow_reference.chain_id != self.asset_binding.chain_id
300            || self.escrow_terms.token_address != self.asset_binding.token_address
301            || self.creation_event.escrow_id != self.escrow_reference.escrow_id
302            || self.creation_event.transaction_to != self.escrow_reference.escrow_contract
303            || self.creation_event.log_emitter != self.escrow_reference.escrow_contract
304            || self.creation_event.receipt_block_number != self.block_pin.block_number
305            || self.creation_event.receipt_block_hash != self.block_pin.block_hash
306            || self.creation_event.capability_id != self.escrow_terms.capability_id
307            || self.creation_event.depositor != self.escrow_terms.depositor
308            || self.creation_event.beneficiary != self.escrow_terms.beneficiary
309            || self.creation_event.token_address != self.escrow_terms.token_address
310            || self.creation_event.max_token_base_units != self.escrow_terms.max_token_base_units
311            || self.creation_event.deadline_unix_secs != self.escrow_terms.deadline_unix_secs
312            || self.creation_event.operator != self.escrow_terms.operator
313            || self.escrow_state_read.contract != self.escrow_reference.escrow_contract
314            || self.escrow_state_read.block_number != self.block_pin.block_number
315            || self.escrow_state_read.block_hash != self.block_pin.block_hash
316            || self.identity_observation.operator != self.escrow_terms.operator
317            || self.identity_observation.operator_key_hash != self.escrow_terms.operator_key_hash
318            || self.identity_observation.block_number != self.block_pin.block_number
319            || self.identity_observation.block_hash != self.block_pin.block_hash
320            || self.token_observation.token_address != self.escrow_terms.token_address
321            || self.token_observation.token_symbol != self.asset_binding.token_symbol
322            || self.token_observation.token_decimals != self.asset_binding.token_decimals
323            || self.token_observation.escrow_contract != self.escrow_reference.escrow_contract
324            || self.token_observation.block_number != self.block_pin.block_number
325            || self.token_observation.block_hash != self.block_pin.block_hash
326            || self.evidence_expires_at_unix_ms <= self.block_pin.observed_at_unix_ms
327        {
328            return Err(ChannelError::InvalidField("funding_evidence_binding"));
329        }
330        Ok(())
331    }
332
333    pub fn digest(&self) -> Result<String, ChannelError> {
334        self.validate()?;
335        digest(FUNDING_EVIDENCE_BODY_DIGEST_DOMAIN, self)
336    }
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase", deny_unknown_fields)]
341pub struct SignedChannelFundingEvidenceV1 {
342    pub body: ChannelFundingEvidenceBodyV1,
343    pub authority_signature: ChannelSignatureV1,
344}
345
346impl SignedChannelFundingEvidenceV1 {
347    pub fn digest(&self) -> Result<String, ChannelError> {
348        self.body.validate()?;
349        digest(FUNDING_EVIDENCE_DIGEST_DOMAIN, self)
350    }
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase", deny_unknown_fields)]
355pub struct ChannelFundingAuthorityV1 {
356    pub authority_id: String,
357    pub authority_key_epoch: u64,
358    #[serde(deserialize_with = "super::signed::deserialize_canonical_public_key")]
359    pub authority_key: PublicKey,
360    pub trusted_time_unix_ms: u64,
361    pub chain_id: String,
362    pub escrow_contract: String,
363    pub identity_registry_contract: String,
364    pub token_address: String,
365    pub token_symbol: String,
366    pub currency: String,
367    pub protocol_minor_unit_decimals: u8,
368    pub token_decimals: u8,
369    pub settlement_policy_digest: String,
370    pub minimum_confirmations: u64,
371    pub finality_mode: Web3FinalityMode,
372}
373
374impl ChannelFundingAuthorityV1 {
375    pub(super) fn validate(&self) -> Result<(), ChannelError> {
376        validate_text("funding_authority_id", &self.authority_id)?;
377        validate_positive("funding_authority_key_epoch", self.authority_key_epoch)?;
378        validate_positive("trusted_time_unix_ms", self.trusted_time_unix_ms)?;
379        validate_chain_id(&self.chain_id)?;
380        validate_evm_address("trusted_escrow_contract", &self.escrow_contract)?;
381        validate_evm_address(
382            "trusted_identity_registry_contract",
383            &self.identity_registry_contract,
384        )?;
385        validate_evm_address("trusted_token_address", &self.token_address)?;
386        validate_text("trusted_token_symbol", &self.token_symbol)?;
387        super::validation::validate_currency(&self.currency)?;
388        validate_digest(
389            "trusted_settlement_policy_digest",
390            &self.settlement_policy_digest,
391        )?;
392        validate_positive("trusted_minimum_confirmations", self.minimum_confirmations)?;
393        if self.finality_mode != Web3FinalityMode::L1Finalized {
394            return Err(ChannelError::InvalidField("trusted_finality_mode"));
395        }
396        Ok(())
397    }
398
399    pub(super) fn same_configuration(&self, other: &Self) -> bool {
400        self.authority_id == other.authority_id
401            && self.authority_key_epoch == other.authority_key_epoch
402            && self.authority_key == other.authority_key
403            && self.chain_id == other.chain_id
404            && self.escrow_contract == other.escrow_contract
405            && self.identity_registry_contract == other.identity_registry_contract
406            && self.token_address == other.token_address
407            && self.token_symbol == other.token_symbol
408            && self.currency == other.currency
409            && self.protocol_minor_unit_decimals == other.protocol_minor_unit_decimals
410            && self.token_decimals == other.token_decimals
411            && self.settlement_policy_digest == other.settlement_policy_digest
412            && self.minimum_confirmations == other.minimum_confirmations
413            && self.finality_mode == other.finality_mode
414    }
415}
416
417pub fn verify_channel_funding_evidence(
418    evidence: &SignedChannelFundingEvidenceV1,
419    authority: &ChannelFundingAuthorityV1,
420) -> Result<(), ChannelError> {
421    authority.validate()?;
422    evidence.body.validate()?;
423    evidence.authority_signature.verify(
424        &evidence.body,
425        &authority.authority_id,
426        authority.authority_key_epoch,
427        &authority.authority_key,
428    )?;
429    let body = &evidence.body;
430    if body.block_pin.observed_at_unix_ms > authority.trusted_time_unix_ms
431        || body.evidence_expires_at_unix_ms <= authority.trusted_time_unix_ms
432        || body.escrow_reference.chain_id != authority.chain_id
433        || body.escrow_reference.escrow_contract != authority.escrow_contract
434        || body.identity_observation.registry_contract != authority.identity_registry_contract
435        || body.asset_binding.token_address != authority.token_address
436        || body.asset_binding.token_symbol != authority.token_symbol
437        || body.asset_binding.currency != authority.currency
438        || body.asset_binding.protocol_minor_unit_decimals != authority.protocol_minor_unit_decimals
439        || body.asset_binding.token_decimals != authority.token_decimals
440        || body.asset_binding.settlement_policy_digest != authority.settlement_policy_digest
441        || body.block_pin.required_confirmations != authority.minimum_confirmations
442        || body.block_pin.finality_mode != authority.finality_mode
443    {
444        return Err(ChannelError::AuthorityVerification);
445    }
446    Ok(())
447}