1use chio_core::capability::scope::MonetaryAmount;
2use chio_core::crypto::PublicKey;
3use chio_core::economic_continuity::VerifiedEconomicStateView;
4use chio_core::web3::trust_profile::Web3FinalityMode;
5use serde::{Deserialize, Serialize};
6
7use super::validation::{
8 digest, validate_digest, validate_positive, validate_text, I_JSON_MAX_SAFE_INTEGER,
9};
10use super::{
11 verify_channel_funding_evidence, ChannelAssetBindingV1, ChannelError, ChannelEscrowReferenceV1,
12 ChannelEscrowReservationStatusV1, ChannelFundingAuthorityV1, ChannelLifecycleStatusV1,
13 ChannelSignatureV1, ChannelStateBodyV1, SignedChannelFundingEvidenceV1,
14 VerifiedChannelLifecycleSnapshotV1, VerifiedChannelStateV1,
15};
16
17pub const CHANNEL_DISPUTE_POLICY_SCHEMA: &str = "chio.channel.dispute-policy.v1";
18pub const CHANNEL_OPEN_INTENT_SCHEMA: &str = "chio.channel.open-intent.v1";
19pub const CHANNEL_FUNDING_ACKNOWLEDGEMENT_SCHEMA: &str = "chio.channel.funding-acknowledgement.v1";
20pub const CHANNEL_OPEN_SCHEMA: &str = "chio.channel.open.v1";
21
22const DISPUTE_POLICY_DIGEST_DOMAIN: &[u8] = b"chio.channel.dispute-policy.digest.v1\0";
23const OPEN_INTENT_DIGEST_DOMAIN: &[u8] = b"chio.channel.open-intent.digest.v1\0";
24const FUNDING_ACKNOWLEDGEMENT_DIGEST_DOMAIN: &[u8] =
25 b"chio.channel.funding-acknowledgement.digest.v1\0";
26const CHANNEL_OPEN_DIGEST_DOMAIN: &[u8] = b"chio.channel.open.digest.v1\0";
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30pub struct ChannelDisputeTierV1 {
31 pub upper_bound_units: u64,
32 pub dispute_window_secs: u64,
33 pub required_confirmations: u64,
34 pub finality_mode: Web3FinalityMode,
35}
36
37impl ChannelDisputeTierV1 {
38 fn validate(&self) -> Result<(), ChannelError> {
39 if self.upper_bound_units == 0 {
40 return Err(ChannelError::InvalidField("dispute_tier_upper_bound"));
41 }
42 validate_positive("dispute_tier_window", self.dispute_window_secs)?;
43 validate_positive(
44 "dispute_tier_required_confirmations",
45 self.required_confirmations,
46 )
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase", deny_unknown_fields)]
52pub struct ChannelDisputePolicyV1 {
53 pub schema: String,
54 pub policy_id: String,
55 pub fixed_finality_broadcast_margin_secs: u64,
56 pub tiers: Vec<ChannelDisputeTierV1>,
57}
58
59impl ChannelDisputePolicyV1 {
60 pub fn validate(&self) -> Result<(), ChannelError> {
61 if self.schema != CHANNEL_DISPUTE_POLICY_SCHEMA {
62 return Err(ChannelError::InvalidField("channel_dispute_policy_schema"));
63 }
64 validate_text("channel_dispute_policy_id", &self.policy_id)?;
65 validate_positive(
66 "fixed_finality_broadcast_margin",
67 self.fixed_finality_broadcast_margin_secs,
68 )?;
69 if self.tiers.is_empty()
70 || !self
71 .tiers
72 .windows(2)
73 .all(|pair| pair[0].upper_bound_units < pair[1].upper_bound_units)
74 || self.tiers.last().map(|tier| tier.upper_bound_units) != Some(I_JSON_MAX_SAFE_INTEGER)
75 {
76 return Err(ChannelError::InvalidField("channel_dispute_tiers"));
77 }
78 for tier in &self.tiers {
79 tier.validate()?;
80 }
81 if self
82 .tiers
83 .iter()
84 .any(|tier| tier.finality_mode != Web3FinalityMode::L1Finalized)
85 || self.tiers.windows(2).any(|pair| {
86 pair[1].dispute_window_secs < pair[0].dispute_window_secs
87 || pair[1].required_confirmations < pair[0].required_confirmations
88 })
89 {
90 return Err(ChannelError::InvalidField("channel_dispute_tiers"));
91 }
92 Ok(())
93 }
94
95 pub fn digest(&self) -> Result<String, ChannelError> {
96 self.validate()?;
97 digest(DISPUTE_POLICY_DIGEST_DOMAIN, self)
98 }
99
100 fn tier_for(&self, units: u64) -> Result<&ChannelDisputeTierV1, ChannelError> {
101 self.validate()?;
102 self.tiers
103 .iter()
104 .find(|tier| units <= tier.upper_bound_units)
105 .ok_or(ChannelError::InvalidField("channel_dispute_tiers"))
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase", deny_unknown_fields)]
111pub struct ChannelOpenIntentBodyV1 {
112 pub schema: String,
113 pub open_intent_id: String,
114 pub payer_id: String,
115 #[serde(deserialize_with = "super::signed::deserialize_canonical_public_key")]
116 pub payer_key: PublicKey,
117 pub payer_key_epoch: u64,
118 pub payer_refund_address: String,
119 pub payee_id: String,
120 #[serde(deserialize_with = "super::signed::deserialize_canonical_public_key")]
121 pub payee_key: PublicKey,
122 pub payee_key_epoch: u64,
123 pub payee_beneficiary_address: String,
124 pub settlement_authority_scope_id: String,
125 pub currency: String,
126 pub bound: MonetaryAmount,
127 pub asset_binding: ChannelAssetBindingV1,
128 pub bound_token_base_units: String,
129 pub channel_expiry_unix_secs: u64,
130 pub dispute_tier_upper_bound_units: u64,
131 pub dispute_window_secs: u64,
132 pub required_confirmations: u64,
133 pub finality_mode: Web3FinalityMode,
134 pub fixed_finality_broadcast_margin_secs: u64,
135 pub close_submission_cutoff_unix_secs: u64,
136 pub original_web3_dispatch_digest: String,
137 pub escrow_reference: ChannelEscrowReferenceV1,
138 pub funding_evidence_digest: String,
139 pub original_operator: String,
140 pub original_operator_key_hash: String,
141 pub participant_snapshot_digest: String,
142}
143
144impl ChannelOpenIntentBodyV1 {
145 pub fn validate(&self) -> Result<(), ChannelError> {
146 if self.schema != CHANNEL_OPEN_INTENT_SCHEMA {
147 return Err(ChannelError::InvalidField("channel_open_intent_schema"));
148 }
149 validate_digest("open_intent_id", &self.open_intent_id)?;
150 validate_text("channel_payer_id", &self.payer_id)?;
151 validate_positive("channel_payer_key_epoch", self.payer_key_epoch)?;
152 validate_text("channel_payee_id", &self.payee_id)?;
153 validate_positive("channel_payee_key_epoch", self.payee_key_epoch)?;
154 validate_text(
155 "settlement_authority_scope_id",
156 &self.settlement_authority_scope_id,
157 )?;
158 super::validation::validate_currency(&self.currency)?;
159 if self.bound.units == 0
160 || self.bound.units > I_JSON_MAX_SAFE_INTEGER
161 || self.bound.currency != self.currency
162 {
163 return Err(ChannelError::InvalidField("channel_bound"));
164 }
165 self.asset_binding.validate()?;
166 self.escrow_reference.validate()?;
167 for (field, value) in [
168 (
169 "original_web3_dispatch_digest",
170 &self.original_web3_dispatch_digest,
171 ),
172 ("funding_evidence_digest", &self.funding_evidence_digest),
173 (
174 "original_operator_key_hash",
175 &self.original_operator_key_hash,
176 ),
177 (
178 "participant_snapshot_digest",
179 &self.participant_snapshot_digest,
180 ),
181 ] {
182 if field == "original_operator_key_hash" {
183 super::validation::validate_evm_hash(field, value)?;
184 } else {
185 validate_digest(field, value)?;
186 }
187 }
188 super::validation::validate_evm_address(
189 "payer_refund_address",
190 &self.payer_refund_address,
191 )?;
192 super::validation::validate_evm_address(
193 "payee_beneficiary_address",
194 &self.payee_beneficiary_address,
195 )?;
196 super::validation::validate_evm_address("original_operator", &self.original_operator)?;
197 super::validation::parse_base_units(&self.bound_token_base_units)?;
198 for (field, value) in [
199 ("channel_expiry", self.channel_expiry_unix_secs),
200 (
201 "dispute_tier_upper_bound",
202 self.dispute_tier_upper_bound_units,
203 ),
204 ("dispute_window", self.dispute_window_secs),
205 ("required_confirmations", self.required_confirmations),
206 (
207 "fixed_finality_broadcast_margin",
208 self.fixed_finality_broadcast_margin_secs,
209 ),
210 (
211 "close_submission_cutoff",
212 self.close_submission_cutoff_unix_secs,
213 ),
214 ] {
215 validate_positive(field, value)?;
216 }
217 Ok(())
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase", deny_unknown_fields)]
223pub struct SignedChannelOpenIntentV1 {
224 pub body: ChannelOpenIntentBodyV1,
225 pub payer_signature: ChannelSignatureV1,
226 pub payee_signature: ChannelSignatureV1,
227}
228
229impl SignedChannelOpenIntentV1 {
230 pub fn digest(&self) -> Result<String, ChannelError> {
231 self.body.validate()?;
232 digest(OPEN_INTENT_DIGEST_DOMAIN, self)
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase", deny_unknown_fields)]
238pub struct ChannelOpenTrustV1 {
239 pub payer_id: String,
240 #[serde(deserialize_with = "super::signed::deserialize_canonical_public_key")]
241 pub payer_key: PublicKey,
242 pub payer_key_epoch: u64,
243 pub payee_id: String,
244 #[serde(deserialize_with = "super::signed::deserialize_canonical_public_key")]
245 pub payee_key: PublicKey,
246 pub payee_key_epoch: u64,
247 pub settlement_authority_scope_id: String,
248 pub original_web3_dispatch_digest: String,
249 pub participant_snapshot_digest: String,
250 pub trusted_time_unix_ms: u64,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct VerifiedChannelOpenIntentV1 {
255 intent: SignedChannelOpenIntentV1,
256 funding_authority: ChannelFundingAuthorityV1,
257 funding_evidence_expires_at_unix_ms: u64,
258}
259
260impl VerifiedChannelOpenIntentV1 {
261 #[must_use]
262 pub const fn artifact(&self) -> &SignedChannelOpenIntentV1 {
263 &self.intent
264 }
265}
266
267impl ChannelOpenTrustV1 {
268 pub(super) fn validate(&self) -> Result<(), ChannelError> {
269 validate_text("trusted_payer_id", &self.payer_id)?;
270 validate_positive("trusted_payer_key_epoch", self.payer_key_epoch)?;
271 validate_text("trusted_payee_id", &self.payee_id)?;
272 validate_positive("trusted_payee_key_epoch", self.payee_key_epoch)?;
273 validate_text(
274 "trusted_settlement_authority_scope_id",
275 &self.settlement_authority_scope_id,
276 )?;
277 validate_digest(
278 "trusted_original_web3_dispatch_digest",
279 &self.original_web3_dispatch_digest,
280 )?;
281 validate_digest(
282 "trusted_participant_snapshot_digest",
283 &self.participant_snapshot_digest,
284 )?;
285 validate_positive("trusted_time_unix_ms", self.trusted_time_unix_ms)
286 }
287
288 pub(super) fn matches_intent(&self, intent: &ChannelOpenIntentBodyV1) -> bool {
289 intent.payer_id == self.payer_id
290 && intent.payer_key == self.payer_key
291 && intent.payer_key_epoch == self.payer_key_epoch
292 && intent.payee_id == self.payee_id
293 && intent.payee_key == self.payee_key
294 && intent.payee_key_epoch == self.payee_key_epoch
295 && intent.settlement_authority_scope_id == self.settlement_authority_scope_id
296 && intent.original_web3_dispatch_digest == self.original_web3_dispatch_digest
297 && intent.participant_snapshot_digest == self.participant_snapshot_digest
298 }
299}
300
301pub fn verify_channel_open_intent(
302 intent: &SignedChannelOpenIntentV1,
303 funding: &SignedChannelFundingEvidenceV1,
304 funding_authority: &ChannelFundingAuthorityV1,
305 policy: &ChannelDisputePolicyV1,
306 trust: &ChannelOpenTrustV1,
307) -> Result<VerifiedChannelOpenIntentV1, ChannelError> {
308 intent.body.validate()?;
309 trust.validate()?;
310 policy.validate()?;
311 verify_channel_funding_evidence(funding, funding_authority)?;
312 let body = &intent.body;
313 let tier = policy.tier_for(body.bound.units)?;
314 body.payer_signature_binding(&intent.payer_signature, trust)?;
315 body.payee_signature_binding(&intent.payee_signature, trust)?;
316 body.asset_binding
317 .verify_round_trip(&body.bound, &body.bound_token_base_units)?;
318 let minimum_cutoff = body
319 .channel_expiry_unix_secs
320 .checked_add(body.dispute_window_secs)
321 .ok_or(ChannelError::ArithmeticOverflow)?;
322 let expected_cutoff = funding
323 .body
324 .escrow_terms
325 .deadline_unix_secs
326 .checked_sub(policy.fixed_finality_broadcast_margin_secs)
327 .ok_or(ChannelError::ArithmeticOverflow)?;
328 if body.payer_id != trust.payer_id
329 || body.payer_key != trust.payer_key
330 || body.payer_key_epoch != trust.payer_key_epoch
331 || body.payee_id != trust.payee_id
332 || body.payee_key != trust.payee_key
333 || body.payee_key_epoch != trust.payee_key_epoch
334 || body.payer_id == body.payee_id
335 || body.payer_key == body.payee_key
336 || body.settlement_authority_scope_id != trust.settlement_authority_scope_id
337 || body.asset_binding.settlement_policy_digest != policy.digest()?
338 || body.asset_binding != funding.body.asset_binding
339 || body.escrow_reference != funding.body.escrow_reference
340 || body.funding_evidence_digest != funding.digest()?
341 || body.bound_token_base_units != funding.body.escrow_terms.max_token_base_units
342 || body.bound_token_base_units != funding.body.escrow_state.deposited_token_base_units
343 || funding.body.escrow_state.released_token_base_units != "0"
344 || funding.body.escrow_state.refunded_token_base_units != "0"
345 || funding.body.escrow_state.refunded
346 || body.payer_refund_address != funding.body.escrow_terms.depositor
347 || body.payee_beneficiary_address != funding.body.escrow_terms.beneficiary
348 || body.original_operator != funding.body.escrow_terms.operator
349 || body.original_operator_key_hash != funding.body.escrow_terms.operator_key_hash
350 || body.original_web3_dispatch_digest != trust.original_web3_dispatch_digest
351 || body.participant_snapshot_digest != trust.participant_snapshot_digest
352 || body.currency != body.asset_binding.currency
353 || body.dispute_tier_upper_bound_units != tier.upper_bound_units
354 || body.dispute_window_secs != tier.dispute_window_secs
355 || body.required_confirmations != tier.required_confirmations
356 || body.finality_mode != tier.finality_mode
357 || body.fixed_finality_broadcast_margin_secs != policy.fixed_finality_broadcast_margin_secs
358 || funding.body.block_pin.required_confirmations != tier.required_confirmations
359 || funding.body.block_pin.finality_mode != tier.finality_mode
360 || body.close_submission_cutoff_unix_secs != expected_cutoff
361 || expected_cutoff >= funding.body.escrow_terms.deadline_unix_secs
362 || expected_cutoff < minimum_cutoff
363 || funding.body.evidence_expires_at_unix_ms <= trust.trusted_time_unix_ms
364 || body.channel_expiry_unix_secs <= trust.trusted_time_unix_ms / 1_000
365 || funding_authority.trusted_time_unix_ms != trust.trusted_time_unix_ms
366 {
367 return Err(ChannelError::AuthorityVerification);
368 }
369 Ok(VerifiedChannelOpenIntentV1 {
370 intent: intent.clone(),
371 funding_authority: funding_authority.clone(),
372 funding_evidence_expires_at_unix_ms: funding.body.evidence_expires_at_unix_ms,
373 })
374}
375
376impl ChannelOpenIntentBodyV1 {
377 fn payer_signature_binding(
378 &self,
379 signature: &ChannelSignatureV1,
380 trust: &ChannelOpenTrustV1,
381 ) -> Result<(), ChannelError> {
382 signature.verify(
383 self,
384 &trust.payer_id,
385 trust.payer_key_epoch,
386 &trust.payer_key,
387 )
388 }
389
390 fn payee_signature_binding(
391 &self,
392 signature: &ChannelSignatureV1,
393 trust: &ChannelOpenTrustV1,
394 ) -> Result<(), ChannelError> {
395 signature.verify(
396 self,
397 &trust.payee_id,
398 trust.payee_key_epoch,
399 &trust.payee_key,
400 )
401 }
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
405#[serde(rename_all = "snake_case")]
406pub enum ChannelEscrowReservationStateV1 {
407 Unreserved,
408 Opening,
409}
410
411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase", deny_unknown_fields)]
413pub struct ChannelFundingAcknowledgementBodyV1 {
414 pub schema: String,
415 pub open_intent_digest: String,
416 pub escrow_reference: ChannelEscrowReferenceV1,
417 pub prior_state: ChannelEscrowReservationStateV1,
418 pub prior_version: u64,
419 pub prior_head_digest: String,
420 pub new_state: ChannelEscrowReservationStateV1,
421 pub new_version: u64,
422 pub anchored_head_digest: String,
423 pub reserved_at_unix_ms: u64,
424 pub expires_at_unix_ms: u64,
425}
426
427impl ChannelFundingAcknowledgementBodyV1 {
428 pub fn validate(&self) -> Result<(), ChannelError> {
429 if self.schema != CHANNEL_FUNDING_ACKNOWLEDGEMENT_SCHEMA {
430 return Err(ChannelError::InvalidField("funding_acknowledgement_schema"));
431 }
432 validate_digest("ack_open_intent_digest", &self.open_intent_digest)?;
433 self.escrow_reference.validate()?;
434 validate_positive("ack_prior_version", self.prior_version)?;
435 validate_digest("ack_prior_head_digest", &self.prior_head_digest)?;
436 validate_positive("ack_new_version", self.new_version)?;
437 validate_digest("ack_anchored_head_digest", &self.anchored_head_digest)?;
438 validate_positive("ack_reserved_at", self.reserved_at_unix_ms)?;
439 validate_positive("ack_expires_at", self.expires_at_unix_ms)?;
440 let next = self
441 .prior_version
442 .checked_add(1)
443 .filter(|version| *version <= I_JSON_MAX_SAFE_INTEGER)
444 .ok_or(ChannelError::ArithmeticOverflow)?;
445 if self.prior_state != ChannelEscrowReservationStateV1::Unreserved
446 || self.new_state != ChannelEscrowReservationStateV1::Opening
447 || self.new_version != next
448 || self.expires_at_unix_ms <= self.reserved_at_unix_ms
449 {
450 return Err(ChannelError::IllegalTransition);
451 }
452 Ok(())
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase", deny_unknown_fields)]
458pub struct SignedChannelFundingAcknowledgementV1 {
459 pub body: ChannelFundingAcknowledgementBodyV1,
460 pub authority_signature: ChannelSignatureV1,
461}
462
463impl SignedChannelFundingAcknowledgementV1 {
464 pub fn digest(&self) -> Result<String, ChannelError> {
465 self.body.validate()?;
466 digest(FUNDING_ACKNOWLEDGEMENT_DIGEST_DOMAIN, self)
467 }
468}
469
470pub fn verify_channel_funding_acknowledgement(
471 acknowledgement: &SignedChannelFundingAcknowledgementV1,
472 intent: &SignedChannelOpenIntentV1,
473 authority: &ChannelFundingAuthorityV1,
474) -> Result<(), ChannelError> {
475 acknowledgement.body.validate()?;
476 authority.validate()?;
477 acknowledgement.authority_signature.verify(
478 &acknowledgement.body,
479 &authority.authority_id,
480 authority.authority_key_epoch,
481 &authority.authority_key,
482 )?;
483 if acknowledgement.body.open_intent_digest != intent.digest()?
484 || acknowledgement.body.escrow_reference != intent.body.escrow_reference
485 || acknowledgement.body.reserved_at_unix_ms > authority.trusted_time_unix_ms
486 || acknowledgement.body.expires_at_unix_ms <= authority.trusted_time_unix_ms
487 {
488 return Err(ChannelError::AuthorityVerification);
489 }
490 Ok(())
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
494#[serde(rename_all = "camelCase", deny_unknown_fields)]
495pub struct ChannelOpenBodyV1 {
496 pub schema: String,
497 pub channel_id: String,
498 pub open_intent_digest: String,
499 pub funding_acknowledgement_digest: String,
500 pub initial_state_digest: String,
501 pub opened_at_unix_ms: u64,
502}
503
504impl ChannelOpenBodyV1 {
505 pub fn validate(&self) -> Result<(), ChannelError> {
506 if self.schema != CHANNEL_OPEN_SCHEMA {
507 return Err(ChannelError::InvalidField("channel_open_schema"));
508 }
509 for (field, value) in [
510 ("channel_id", &self.channel_id),
511 ("open_intent_digest", &self.open_intent_digest),
512 (
513 "funding_acknowledgement_digest",
514 &self.funding_acknowledgement_digest,
515 ),
516 ("initial_state_digest", &self.initial_state_digest),
517 ] {
518 validate_digest(field, value)?;
519 }
520 validate_positive("channel_opened_at", self.opened_at_unix_ms)
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(rename_all = "camelCase", deny_unknown_fields)]
526pub struct SignedChannelOpenV1 {
527 pub body: ChannelOpenBodyV1,
528 pub payer_signature: ChannelSignatureV1,
529 pub payee_signature: ChannelSignatureV1,
530}
531
532#[derive(Debug, Clone, PartialEq, Eq)]
533pub struct VerifiedChannelOpenConsentV1 {
534 open: SignedChannelOpenV1,
535 intent: SignedChannelOpenIntentV1,
536 initial_state: VerifiedChannelStateV1,
537}
538
539impl VerifiedChannelOpenConsentV1 {
540 #[must_use]
541 pub const fn artifact(&self) -> &SignedChannelOpenV1 {
542 &self.open
543 }
544
545 #[must_use]
546 pub const fn intent(&self) -> &SignedChannelOpenIntentV1 {
547 &self.intent
548 }
549
550 #[must_use]
551 pub const fn initial_state(&self) -> &VerifiedChannelStateV1 {
552 &self.initial_state
553 }
554}
555
556impl SignedChannelOpenV1 {
557 pub fn digest(&self) -> Result<String, ChannelError> {
558 self.body.validate()?;
559 digest(CHANNEL_OPEN_DIGEST_DOMAIN, self)
560 }
561}
562
563pub fn derive_channel_id(
564 open_intent_digest: &str,
565 funding_acknowledgement_digest: &str,
566) -> Result<String, ChannelError> {
567 validate_digest("open_intent_digest", open_intent_digest)?;
568 validate_digest(
569 "funding_acknowledgement_digest",
570 funding_acknowledgement_digest,
571 )?;
572 digest(
573 b"",
574 &(
575 "chio.channel.id.v1",
576 open_intent_digest,
577 funding_acknowledgement_digest,
578 ),
579 )
580}
581
582pub fn verify_channel_open_consent(
583 open: &SignedChannelOpenV1,
584 verified_intent: &VerifiedChannelOpenIntentV1,
585 acknowledgement: &SignedChannelFundingAcknowledgementV1,
586 authority: &ChannelFundingAuthorityV1,
587 trust: &ChannelOpenTrustV1,
588) -> Result<VerifiedChannelOpenConsentV1, ChannelError> {
589 let intent = verified_intent.artifact();
590 open.body.validate()?;
591 trust.validate()?;
592 verify_channel_funding_acknowledgement(acknowledgement, intent, authority)?;
593 open.payer_signature.verify(
594 &open.body,
595 &trust.payer_id,
596 trust.payer_key_epoch,
597 &trust.payer_key,
598 )?;
599 open.payee_signature.verify(
600 &open.body,
601 &trust.payee_id,
602 trust.payee_key_epoch,
603 &trust.payee_key,
604 )?;
605 let intent_digest = intent.digest()?;
606 let acknowledgement_digest = acknowledgement.digest()?;
607 let channel_id = derive_channel_id(&intent_digest, &acknowledgement_digest)?;
608 let initial_state = ChannelStateBodyV1::initial(
609 channel_id.clone(),
610 intent.body.currency.clone(),
611 intent.body.asset_binding.digest()?,
612 )?;
613 let channel_expiry_unix_ms = intent
614 .body
615 .channel_expiry_unix_secs
616 .checked_mul(1_000)
617 .ok_or(ChannelError::ArithmeticOverflow)?;
618 if !trust.matches_intent(&intent.body)
619 || !verified_intent
620 .funding_authority
621 .same_configuration(authority)
622 || authority.trusted_time_unix_ms < verified_intent.funding_authority.trusted_time_unix_ms
623 || open.body.channel_id != channel_id
624 || open.body.open_intent_digest != intent_digest
625 || open.body.funding_acknowledgement_digest != acknowledgement_digest
626 || open.body.initial_state_digest != initial_state.digest()?
627 || open.body.opened_at_unix_ms > trust.trusted_time_unix_ms
628 || open.body.opened_at_unix_ms < acknowledgement.body.reserved_at_unix_ms
629 || open.body.opened_at_unix_ms >= acknowledgement.body.expires_at_unix_ms
630 || trust.trusted_time_unix_ms >= acknowledgement.body.expires_at_unix_ms
631 || open.body.opened_at_unix_ms >= channel_expiry_unix_ms
632 || trust.trusted_time_unix_ms >= channel_expiry_unix_ms
633 || trust.trusted_time_unix_ms >= verified_intent.funding_evidence_expires_at_unix_ms
634 || authority.trusted_time_unix_ms != trust.trusted_time_unix_ms
635 {
636 return Err(ChannelError::AuthorityVerification);
637 }
638 Ok(VerifiedChannelOpenConsentV1 {
639 open: open.clone(),
640 intent: intent.clone(),
641 initial_state: VerifiedChannelStateV1::initial(initial_state),
642 })
643}
644
645#[derive(Debug, Clone)]
646pub struct VerifiedAdmittedChannelOpenV1 {
647 consent: VerifiedChannelOpenConsentV1,
648 snapshot: VerifiedChannelLifecycleSnapshotV1,
649}
650
651impl VerifiedAdmittedChannelOpenV1 {
652 #[must_use]
653 pub const fn consent(&self) -> &VerifiedChannelOpenConsentV1 {
654 &self.consent
655 }
656
657 #[must_use]
658 pub const fn snapshot(&self) -> &VerifiedChannelLifecycleSnapshotV1 {
659 &self.snapshot
660 }
661}
662
663pub fn verify_admitted_channel_open(
664 consent: &VerifiedChannelOpenConsentV1,
665 current: &VerifiedEconomicStateView,
666) -> Result<VerifiedAdmittedChannelOpenV1, ChannelError> {
667 let open_digest = consent.artifact().digest()?;
668 let snapshot = super::verify_channel_lifecycle_snapshot(
669 current,
670 &consent.intent().body.settlement_authority_scope_id,
671 &consent.artifact().body.channel_id,
672 )?;
673 let lifecycle = snapshot.lifecycle();
674 let escrow = snapshot.escrow();
675 if lifecycle.status != ChannelLifecycleStatusV1::Open
676 || escrow.status != ChannelEscrowReservationStatusV1::Open
677 || escrow.open_digest != open_digest
678 || escrow.escrow_reference != consent.intent().body.escrow_reference
679 || lifecycle.latest_sequence == 0
680 && lifecycle.latest_state_digest != consent.artifact().body.initial_state_digest
681 {
682 return Err(ChannelError::AuthorityVerification);
683 }
684 Ok(VerifiedAdmittedChannelOpenV1 {
685 consent: consent.clone(),
686 snapshot,
687 })
688}