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}