Skip to main content

chio_settle/channel/
open.rs

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}