Skip to main content

chio_settle/channel/
close.rs

1use chio_core::capability::scope::MonetaryAmount;
2use chio_core::federation::frost::{
3    FrostActionPreimageV1, FrostChannelCloseActionV1, CHIO_FROST_CHANNEL_CLOSE_ACTION_SCHEMA,
4};
5use serde::{Deserialize, Serialize};
6
7use super::validation::{
8    digest, parse_base_units, validate_currency, validate_digest, validate_positive,
9    I_JSON_MAX_SAFE_INTEGER,
10};
11use super::{
12    ChannelError, ChannelEscrowReservationStatusV1, ChannelLifecycleStatusV1, ChannelOpenTrustV1,
13    ChannelSignatureV1, VerifiedChannelDisputeV1, VerifiedChannelLifecycleSnapshotV1,
14    VerifiedChannelOpenConsentV1, VerifiedChannelStateV1,
15};
16
17pub const CHANNEL_CLOSE_SCHEMA: &str = "chio.channel.close.v1";
18
19const CHANNEL_CLOSE_BODY_DIGEST_DOMAIN: &[u8] = b"chio.channel.close.body.digest.v1\0";
20const CHANNEL_CLOSE_DIGEST_DOMAIN: &[u8] = b"chio.channel.close.digest.v1\0";
21const CHANNEL_EFFECTIVE_CLOSE_DIGEST_DOMAIN: &[u8] = b"chio.channel.effective-close.digest.v1\0";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum ChannelCloseKindV1 {
26    Cooperative,
27    Contested,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase", deny_unknown_fields)]
32pub struct ChannelCloseBodyV1 {
33    pub schema: String,
34    pub channel_id: String,
35    pub open_digest: String,
36    pub close_kind: ChannelCloseKindV1,
37    pub final_state_digest: String,
38    pub final_state_sequence: u64,
39    pub final_cumulative_owed: MonetaryAmount,
40    pub expected_release_token_base_units: String,
41    pub expected_refund_after_release_token_base_units: String,
42    pub dispute_window_secs: u64,
43    pub proposed_at_unix_ms: u64,
44    pub dispute_deadline_unix_ms: u64,
45    pub close_submission_cutoff_unix_secs: u64,
46    pub channel_state_version: u64,
47    pub escrow_reservation_version: u64,
48    pub lifecycle_fence: u64,
49}
50
51impl ChannelCloseBodyV1 {
52    pub fn validate(&self) -> Result<(), ChannelError> {
53        if self.schema != CHANNEL_CLOSE_SCHEMA {
54            return Err(ChannelError::InvalidField("channel_close_schema"));
55        }
56        for (field, value) in [
57            ("close_channel_id", &self.channel_id),
58            ("close_open_digest", &self.open_digest),
59            ("close_final_state_digest", &self.final_state_digest),
60        ] {
61            validate_digest(field, value)?;
62        }
63        validate_currency(&self.final_cumulative_owed.currency)?;
64        if self.final_state_sequence > I_JSON_MAX_SAFE_INTEGER
65            || self.final_cumulative_owed.units > I_JSON_MAX_SAFE_INTEGER
66        {
67            return Err(ChannelError::InvalidField("channel_close_amount"));
68        }
69        parse_base_units(&self.expected_release_token_base_units)?;
70        parse_base_units(&self.expected_refund_after_release_token_base_units)?;
71        for (field, value) in [
72            ("close_dispute_window", self.dispute_window_secs),
73            ("close_proposed_at", self.proposed_at_unix_ms),
74            ("close_dispute_deadline", self.dispute_deadline_unix_ms),
75            (
76                "close_submission_cutoff",
77                self.close_submission_cutoff_unix_secs,
78            ),
79            ("close_channel_state_version", self.channel_state_version),
80            (
81                "close_escrow_reservation_version",
82                self.escrow_reservation_version,
83            ),
84            ("close_lifecycle_fence", self.lifecycle_fence),
85        ] {
86            validate_positive(field, value)?;
87        }
88        if self.dispute_deadline_unix_ms <= self.proposed_at_unix_ms {
89            return Err(ChannelError::InvalidField("close_dispute_deadline"));
90        }
91        Ok(())
92    }
93
94    pub fn digest(&self) -> Result<String, ChannelError> {
95        self.validate()?;
96        digest(CHANNEL_CLOSE_BODY_DIGEST_DOMAIN, self)
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase", deny_unknown_fields)]
102pub struct SignedChannelCloseV1 {
103    pub body: ChannelCloseBodyV1,
104    pub payee_signature: ChannelSignatureV1,
105    #[serde(
106        default,
107        skip_serializing_if = "Option::is_none",
108        deserialize_with = "super::validation::deserialize_present_option"
109    )]
110    pub payer_signature: Option<ChannelSignatureV1>,
111}
112
113impl SignedChannelCloseV1 {
114    pub fn digest(&self) -> Result<String, ChannelError> {
115        self.body.validate()?;
116        digest(CHANNEL_CLOSE_DIGEST_DOMAIN, self)
117    }
118}
119
120#[derive(Debug, Clone)]
121pub struct VerifiedChannelCloseV1 {
122    close: SignedChannelCloseV1,
123    open: VerifiedChannelOpenConsentV1,
124    final_state: VerifiedChannelStateV1,
125    snapshot: VerifiedChannelLifecycleSnapshotV1,
126}
127
128impl VerifiedChannelCloseV1 {
129    #[must_use]
130    pub const fn artifact(&self) -> &SignedChannelCloseV1 {
131        &self.close
132    }
133
134    #[must_use]
135    pub const fn open(&self) -> &VerifiedChannelOpenConsentV1 {
136        &self.open
137    }
138
139    #[must_use]
140    pub const fn final_state(&self) -> &VerifiedChannelStateV1 {
141        &self.final_state
142    }
143
144    #[must_use]
145    pub const fn snapshot(&self) -> &VerifiedChannelLifecycleSnapshotV1 {
146        &self.snapshot
147    }
148}
149
150pub fn build_channel_close_body(
151    close_kind: ChannelCloseKindV1,
152    open: &VerifiedChannelOpenConsentV1,
153    final_state: &VerifiedChannelStateV1,
154    snapshot: &VerifiedChannelLifecycleSnapshotV1,
155    proposed_at_unix_ms: u64,
156) -> Result<ChannelCloseBodyV1, ChannelError> {
157    let lifecycle = snapshot.lifecycle();
158    let escrow = snapshot.escrow();
159    lifecycle.validate()?;
160    escrow.validate()?;
161    let intent = open.intent();
162    let final_state_digest = final_state.digest()?;
163    let final_state_body = final_state.body();
164    let open_digest = open.artifact().digest()?;
165    if snapshot.settlement_authority_scope_id() != intent.body.settlement_authority_scope_id
166        || lifecycle.status != ChannelLifecycleStatusV1::Open
167        || lifecycle.channel_id != open.artifact().body.channel_id
168        || lifecycle.latest_state_digest != final_state_digest
169        || lifecycle.latest_sequence != final_state_body.seq
170        || lifecycle.live_reservation_id.is_some()
171        || lifecycle.operation_id.is_some()
172        || escrow.open_digest != open_digest
173        || escrow.escrow_reference != intent.body.escrow_reference
174        || escrow.status != ChannelEscrowReservationStatusV1::Open
175        || escrow.channel_id != lifecycle.channel_id
176        || escrow.lifecycle_fence != lifecycle.lifecycle_fence
177        || final_state_body.channel_id != lifecycle.channel_id
178    {
179        return Err(ChannelError::IllegalTransition);
180    }
181    let dispute_window_ms = intent
182        .body
183        .dispute_window_secs
184        .checked_mul(1_000)
185        .ok_or(ChannelError::ArithmeticOverflow)?;
186    let dispute_deadline_unix_ms = proposed_at_unix_ms
187        .checked_add(dispute_window_ms)
188        .ok_or(ChannelError::ArithmeticOverflow)?;
189    let cutoff_unix_ms = intent
190        .body
191        .close_submission_cutoff_unix_secs
192        .checked_mul(1_000)
193        .ok_or(ChannelError::ArithmeticOverflow)?;
194    if dispute_deadline_unix_ms >= cutoff_unix_ms {
195        return Err(ChannelError::IllegalTransition);
196    }
197    let expected_release_token_base_units = intent
198        .body
199        .asset_binding
200        .token_base_units(&final_state_body.cumulative_owed)?;
201    let expected_refund_after_release_token_base_units =
202        parse_base_units(&intent.body.bound_token_base_units)?
203            .checked_sub(parse_base_units(&expected_release_token_base_units)?)
204            .ok_or(ChannelError::ArithmeticOverflow)?
205            .to_string();
206    let body = ChannelCloseBodyV1 {
207        schema: CHANNEL_CLOSE_SCHEMA.to_owned(),
208        channel_id: lifecycle.channel_id.clone(),
209        open_digest,
210        close_kind,
211        final_state_digest,
212        final_state_sequence: final_state_body.seq,
213        final_cumulative_owed: final_state_body.cumulative_owed.clone(),
214        expected_release_token_base_units,
215        expected_refund_after_release_token_base_units,
216        dispute_window_secs: intent.body.dispute_window_secs,
217        proposed_at_unix_ms,
218        dispute_deadline_unix_ms,
219        close_submission_cutoff_unix_secs: intent.body.close_submission_cutoff_unix_secs,
220        channel_state_version: lifecycle
221            .state_version
222            .checked_add(1)
223            .ok_or(ChannelError::ArithmeticOverflow)?,
224        escrow_reservation_version: escrow
225            .version
226            .checked_add(1)
227            .ok_or(ChannelError::ArithmeticOverflow)?,
228        lifecycle_fence: lifecycle
229            .lifecycle_fence
230            .checked_add(1)
231            .ok_or(ChannelError::ArithmeticOverflow)?,
232    };
233    body.validate()?;
234    Ok(body)
235}
236
237pub fn verify_channel_close(
238    close: &SignedChannelCloseV1,
239    open: &VerifiedChannelOpenConsentV1,
240    final_state: &VerifiedChannelStateV1,
241    snapshot: &VerifiedChannelLifecycleSnapshotV1,
242    trust: &ChannelOpenTrustV1,
243) -> Result<VerifiedChannelCloseV1, ChannelError> {
244    let lifecycle = snapshot.lifecycle();
245    let escrow = snapshot.escrow();
246    close.body.validate()?;
247    lifecycle.validate()?;
248    escrow.validate()?;
249    trust.validate()?;
250    if !trust.matches_intent(&open.intent().body) {
251        return Err(ChannelError::AuthorityVerification);
252    }
253    let body = &close.body;
254    let close_body_digest = body.digest()?;
255    let final_state_digest = final_state.digest()?;
256    let final_state_body = final_state.body();
257    close.payee_signature.verify(
258        body,
259        &trust.payee_id,
260        trust.payee_key_epoch,
261        &trust.payee_key,
262    )?;
263    if let Some(signature) = &close.payer_signature {
264        signature.verify(
265            body,
266            &trust.payer_id,
267            trust.payer_key_epoch,
268            &trust.payer_key,
269        )?;
270    }
271    let cutoff_unix_ms = body
272        .close_submission_cutoff_unix_secs
273        .checked_mul(1_000)
274        .ok_or(ChannelError::ArithmeticOverflow)?;
275    let dispute_deadline_unix_ms = body
276        .dispute_window_secs
277        .checked_mul(1_000)
278        .and_then(|window| body.proposed_at_unix_ms.checked_add(window))
279        .ok_or(ChannelError::ArithmeticOverflow)?;
280    if snapshot.settlement_authority_scope_id() != open.intent().body.settlement_authority_scope_id
281        || body.channel_id != open.artifact().body.channel_id
282        || body.open_digest != open.artifact().digest()?
283        || body.final_state_digest != final_state_digest
284        || body.final_state_sequence != final_state_body.seq
285        || body.final_cumulative_owed != final_state_body.cumulative_owed
286        || body.dispute_window_secs != open.intent().body.dispute_window_secs
287        || body.dispute_deadline_unix_ms != dispute_deadline_unix_ms
288        || body.close_submission_cutoff_unix_secs
289            != open.intent().body.close_submission_cutoff_unix_secs
290        || lifecycle.status != ChannelLifecycleStatusV1::ClosePending
291        || lifecycle.channel_id != body.channel_id
292        || lifecycle.latest_state_digest != body.final_state_digest
293        || lifecycle.latest_sequence != body.final_state_sequence
294        || lifecycle.state_version != body.channel_state_version
295        || lifecycle.lifecycle_fence != body.lifecycle_fence
296        || lifecycle.pending_close_body_digest.as_deref() != Some(&close_body_digest)
297        || lifecycle.admitted_dispute_digest.is_some()
298        || lifecycle.live_reservation_id.is_some()
299        || escrow.status != ChannelEscrowReservationStatusV1::Open
300        || escrow.channel_id != body.channel_id
301        || escrow.open_digest != body.open_digest
302        || escrow.escrow_reference != open.intent().body.escrow_reference
303        || escrow.version != body.escrow_reservation_version
304        || escrow.lifecycle_fence != body.lifecycle_fence
305        || escrow.pending_close_body_digest.as_deref() != Some(&close_body_digest)
306        || body.proposed_at_unix_ms != trust.trusted_time_unix_ms
307        || snapshot.observed_at_unix_ms() < body.proposed_at_unix_ms
308        || snapshot.observed_at_unix_ms() >= body.dispute_deadline_unix_ms
309        || body.dispute_deadline_unix_ms >= cutoff_unix_ms
310        || trust.trusted_time_unix_ms > body.dispute_deadline_unix_ms
311        || trust.trusted_time_unix_ms >= cutoff_unix_ms
312        || body.close_kind == ChannelCloseKindV1::Cooperative && close.payer_signature.is_none()
313    {
314        return Err(ChannelError::AuthorityVerification);
315    }
316    open.intent().body.asset_binding.verify_round_trip(
317        &body.final_cumulative_owed,
318        &body.expected_release_token_base_units,
319    )?;
320    let expected_refund = parse_base_units(&open.intent().body.bound_token_base_units)?
321        .checked_sub(parse_base_units(&body.expected_release_token_base_units)?)
322        .ok_or(ChannelError::ArithmeticOverflow)?;
323    if expected_refund.to_string() != body.expected_refund_after_release_token_base_units {
324        return Err(ChannelError::AuthorityVerification);
325    }
326    Ok(VerifiedChannelCloseV1 {
327        close: close.clone(),
328        open: open.clone(),
329        final_state: final_state.clone(),
330        snapshot: snapshot.clone(),
331    })
332}
333
334#[derive(Debug, Clone)]
335pub struct VerifiedEffectiveChannelCloseV1 {
336    close: VerifiedChannelCloseV1,
337    effective_state: VerifiedChannelStateV1,
338    snapshot: VerifiedChannelLifecycleSnapshotV1,
339    admitted_dispute_digest: Option<String>,
340    expected_release_token_base_units: String,
341    expected_refund_after_release_token_base_units: String,
342    effective_close_digest: String,
343}
344
345impl VerifiedEffectiveChannelCloseV1 {
346    #[must_use]
347    pub const fn close(&self) -> &VerifiedChannelCloseV1 {
348        &self.close
349    }
350
351    #[must_use]
352    pub const fn effective_state(&self) -> &VerifiedChannelStateV1 {
353        &self.effective_state
354    }
355
356    #[must_use]
357    pub const fn snapshot(&self) -> &VerifiedChannelLifecycleSnapshotV1 {
358        &self.snapshot
359    }
360
361    #[must_use]
362    pub fn admitted_dispute_digest(&self) -> Option<&str> {
363        self.admitted_dispute_digest.as_deref()
364    }
365
366    #[must_use]
367    pub fn expected_release_token_base_units(&self) -> &str {
368        &self.expected_release_token_base_units
369    }
370
371    #[must_use]
372    pub fn expected_refund_after_release_token_base_units(&self) -> &str {
373        &self.expected_refund_after_release_token_base_units
374    }
375
376    #[must_use]
377    pub fn effective_close_digest(&self) -> &str {
378        &self.effective_close_digest
379    }
380}
381
382pub fn verify_effective_channel_close(
383    close: &VerifiedChannelCloseV1,
384) -> Result<VerifiedEffectiveChannelCloseV1, ChannelError> {
385    let body = &close.artifact().body;
386    let lifecycle = close.snapshot().lifecycle();
387    let escrow = close.snapshot().escrow();
388    if lifecycle.state_version != body.channel_state_version
389        || escrow.version != body.escrow_reservation_version
390        || lifecycle.lifecycle_fence != body.lifecycle_fence
391        || close.final_state().digest()? != body.final_state_digest
392    {
393        return Err(ChannelError::AuthorityVerification);
394    }
395    effective_close_from_authority(close, close.final_state(), close.snapshot(), None)
396}
397
398pub fn verify_effective_channel_dispute_advance(
399    current: &VerifiedEffectiveChannelCloseV1,
400    dispute: &VerifiedChannelDisputeV1,
401    next_snapshot: &VerifiedChannelLifecycleSnapshotV1,
402) -> Result<VerifiedEffectiveChannelCloseV1, ChannelError> {
403    let close = current.close();
404    let close_digest = close.artifact().digest()?;
405    let close_body_digest = close.artifact().body.digest()?;
406    let chain = dispute.chain();
407    let terminal_state = chain.terminal_state();
408    let terminal_digest = terminal_state.digest()?;
409    let dispute_digest = dispute.artifact().digest()?;
410    let current_digest = current.effective_state().digest()?;
411    let current_sequence = current.effective_state().body().seq;
412    let current_on_chain = if current_sequence == close.artifact().body.final_state_sequence {
413        chain.proof().base_state_digest == current_digest
414    } else {
415        chain.proof().states.iter().any(|state| {
416            state.body.seq == current_sequence
417                && state.digest().is_ok_and(|digest| digest == current_digest)
418        })
419    };
420    let lifecycle = next_snapshot.lifecycle();
421    let escrow = next_snapshot.escrow();
422    let current_lifecycle = current.snapshot().lifecycle();
423    let current_escrow = current.snapshot().escrow();
424    let expected_state_version = current_lifecycle
425        .state_version
426        .checked_add(1)
427        .ok_or(ChannelError::ArithmeticOverflow)?;
428    let expected_escrow_version = current_escrow
429        .version
430        .checked_add(1)
431        .ok_or(ChannelError::ArithmeticOverflow)?;
432    let expected_fence = current_lifecycle
433        .lifecycle_fence
434        .checked_add(1)
435        .ok_or(ChannelError::ArithmeticOverflow)?;
436    let dispute_body = &dispute.artifact().body;
437    if dispute_body.close_digest != close_digest
438        || chain.proof().base_state_digest != close.artifact().body.final_state_digest
439        || !current_on_chain
440        || terminal_state.body().seq <= current_sequence
441        || dispute_body.competing_state_digest != terminal_digest
442        || dispute_body.competing_state_sequence != terminal_state.body().seq
443        || lifecycle.status != ChannelLifecycleStatusV1::ClosePending
444        || lifecycle.latest_state_digest != terminal_digest
445        || lifecycle.latest_sequence != terminal_state.body().seq
446        || lifecycle.state_version != expected_state_version
447        || lifecycle.lifecycle_fence != expected_fence
448        || lifecycle.pending_close_body_digest.as_deref() != Some(close_body_digest.as_str())
449        || lifecycle.admitted_dispute_digest.as_deref() != Some(dispute_digest.as_str())
450        || lifecycle.live_reservation_id.is_some()
451        || lifecycle.operation_id.is_some()
452        || escrow.status != ChannelEscrowReservationStatusV1::Open
453        || escrow.version != expected_escrow_version
454        || escrow.lifecycle_fence != expected_fence
455        || escrow.pending_close_body_digest.as_deref() != Some(close_body_digest.as_str())
456        || next_snapshot.settlement_authority_scope_id()
457            != current.snapshot().settlement_authority_scope_id()
458        || next_snapshot.observed_at_unix_ms() < current.snapshot().observed_at_unix_ms()
459        || next_snapshot.observed_at_unix_ms() < dispute_body.submitted_at_unix_ms
460        || next_snapshot.observed_at_unix_ms() >= close.artifact().body.dispute_deadline_unix_ms
461        || current
462            .snapshot()
463            .channel_head()
464            .validate_successor(next_snapshot.channel_head())
465            .is_err()
466        || current
467            .snapshot()
468            .escrow_head()
469            .validate_successor(next_snapshot.escrow_head())
470            .is_err()
471    {
472        return Err(ChannelError::AuthorityVerification);
473    }
474    effective_close_from_authority(close, terminal_state, next_snapshot, Some(dispute_digest))
475}
476
477fn effective_close_from_authority(
478    close: &VerifiedChannelCloseV1,
479    effective_state: &VerifiedChannelStateV1,
480    snapshot: &VerifiedChannelLifecycleSnapshotV1,
481    admitted_dispute_digest: Option<String>,
482) -> Result<VerifiedEffectiveChannelCloseV1, ChannelError> {
483    #[derive(Serialize)]
484    #[serde(rename_all = "camelCase")]
485    struct EffectiveCloseDigestBody<'a> {
486        close_digest: &'a str,
487        close_body_digest: &'a str,
488        effective_state_digest: &'a str,
489        effective_state_sequence: u64,
490        final_cumulative_owed: &'a MonetaryAmount,
491        expected_release_token_base_units: &'a str,
492        expected_refund_after_release_token_base_units: &'a str,
493        channel_state_version: u64,
494        escrow_reservation_version: u64,
495        lifecycle_fence: u64,
496        checkpoint_digest: &'a str,
497        channel_head_digest: &'a str,
498        escrow_head_digest: &'a str,
499        observed_at_unix_ms: u64,
500        admitted_dispute_digest: Option<&'a str>,
501    }
502
503    let open = close.open();
504    let close_body = &close.artifact().body;
505    let close_digest = close.artifact().digest()?;
506    let close_body_digest = close_body.digest()?;
507    let effective_state_digest = effective_state.digest()?;
508    let effective_state_body = effective_state.body();
509    let lifecycle = snapshot.lifecycle();
510    let escrow = snapshot.escrow();
511    if snapshot.settlement_authority_scope_id() != open.intent().body.settlement_authority_scope_id
512        || lifecycle.status != ChannelLifecycleStatusV1::ClosePending
513        || lifecycle.channel_id != close_body.channel_id
514        || lifecycle.latest_state_digest != effective_state_digest
515        || lifecycle.latest_sequence != effective_state_body.seq
516        || lifecycle.pending_close_body_digest.as_deref() != Some(close_body_digest.as_str())
517        || lifecycle.admitted_dispute_digest.as_deref() != admitted_dispute_digest.as_deref()
518        || lifecycle.live_reservation_id.is_some()
519        || lifecycle.operation_id.is_some()
520        || escrow.status != ChannelEscrowReservationStatusV1::Open
521        || escrow.channel_id != close_body.channel_id
522        || escrow.open_digest != close_body.open_digest
523        || escrow.escrow_reference != open.intent().body.escrow_reference
524        || escrow.lifecycle_fence != lifecycle.lifecycle_fence
525        || escrow.pending_close_body_digest.as_deref() != Some(close_body_digest.as_str())
526        || effective_state_body.channel_id != close_body.channel_id
527        || effective_state_body.cumulative_owed.currency != open.intent().body.currency
528        || effective_state_body.cumulative_owed.units > open.intent().body.bound.units
529    {
530        return Err(ChannelError::AuthorityVerification);
531    }
532    let expected_release_token_base_units = open
533        .intent()
534        .body
535        .asset_binding
536        .token_base_units(&effective_state_body.cumulative_owed)?;
537    open.intent().body.asset_binding.verify_round_trip(
538        &effective_state_body.cumulative_owed,
539        &expected_release_token_base_units,
540    )?;
541    let expected_refund_after_release_token_base_units =
542        parse_base_units(&open.intent().body.bound_token_base_units)?
543            .checked_sub(parse_base_units(&expected_release_token_base_units)?)
544            .ok_or(ChannelError::ArithmeticOverflow)?
545            .to_string();
546    let effective_close_digest = digest(
547        CHANNEL_EFFECTIVE_CLOSE_DIGEST_DOMAIN,
548        &EffectiveCloseDigestBody {
549            close_digest: &close_digest,
550            close_body_digest: &close_body_digest,
551            effective_state_digest: &effective_state_digest,
552            effective_state_sequence: effective_state_body.seq,
553            final_cumulative_owed: &effective_state_body.cumulative_owed,
554            expected_release_token_base_units: &expected_release_token_base_units,
555            expected_refund_after_release_token_base_units:
556                &expected_refund_after_release_token_base_units,
557            channel_state_version: lifecycle.state_version,
558            escrow_reservation_version: escrow.version,
559            lifecycle_fence: lifecycle.lifecycle_fence,
560            checkpoint_digest: snapshot.checkpoint_digest(),
561            channel_head_digest: snapshot.channel_head_digest(),
562            escrow_head_digest: snapshot.escrow_head_digest(),
563            observed_at_unix_ms: snapshot.observed_at_unix_ms(),
564            admitted_dispute_digest: admitted_dispute_digest.as_deref(),
565        },
566    )?;
567    Ok(VerifiedEffectiveChannelCloseV1 {
568        close: close.clone(),
569        effective_state: effective_state.clone(),
570        snapshot: snapshot.clone(),
571        admitted_dispute_digest,
572        expected_release_token_base_units,
573        expected_refund_after_release_token_base_units,
574        effective_close_digest,
575    })
576}
577
578pub fn channel_close_frost_action(
579    close: &VerifiedEffectiveChannelCloseV1,
580    publisher_fence: u64,
581) -> Result<FrostActionPreimageV1, ChannelError> {
582    validate_positive("close_publisher_fence", publisher_fence)?;
583    let body = &close.close().artifact().body;
584    let state = close.effective_state().body();
585    let lifecycle = close.snapshot().lifecycle();
586    let escrow = close.snapshot().escrow();
587    let action = FrostActionPreimageV1::ChannelClose(FrostChannelCloseActionV1 {
588        schema: CHIO_FROST_CHANNEL_CLOSE_ACTION_SCHEMA.to_owned(),
589        close_body_digest: body.digest()?,
590        effective_close_digest: close.effective_close_digest().to_owned(),
591        channel_id: body.channel_id.clone(),
592        final_state_digest: close.effective_state().digest()?,
593        final_state_sequence: state.seq,
594        final_cumulative_owed: state.cumulative_owed.clone(),
595        channel_state_version: lifecycle.state_version,
596        escrow_reservation_version: escrow.version,
597        token_base_unit_release: close.expected_release_token_base_units().to_owned(),
598        publisher_fence,
599        lifecycle_fence: lifecycle.lifecycle_fence,
600    });
601    action
602        .validate()
603        .map_err(|_| ChannelError::AuthorityVerification)?;
604    Ok(action)
605}