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}