Skip to main content

chio_settle/channel/
lifecycle.rs

1use chio_core::economic_continuity::{
2    EconomicContentV1, EconomicResourceHeadV1, EconomicResourceKeyV1, VerifiedEconomicStateView,
3};
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6
7use super::validation::{validate_digest, validate_positive, I_JSON_MAX_SAFE_INTEGER};
8use super::{ChannelError, ChannelEscrowReferenceV1};
9
10pub const CHANNEL_LIFECYCLE_SCHEMA: &str = "chio.channel.lifecycle.v1";
11pub const CHANNEL_LIFECYCLE_RESOURCE_FAMILY: &str = "channel_lifecycle";
12pub const CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY: &str = "channel_escrow_reservation";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ChannelLifecycleStatusV1 {
17    Open,
18    ClosePending,
19    Closing,
20    Released,
21    Refunded,
22    Incident,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct ChannelLifecycleViewV1 {
28    pub schema: String,
29    pub channel_id: String,
30    pub status: ChannelLifecycleStatusV1,
31    pub latest_state_digest: String,
32    pub latest_sequence: u64,
33    pub state_version: u64,
34    pub lifecycle_fence: u64,
35    #[serde(
36        default,
37        skip_serializing_if = "Option::is_none",
38        deserialize_with = "super::validation::deserialize_present_option"
39    )]
40    pub pending_close_body_digest: Option<String>,
41    #[serde(
42        default,
43        skip_serializing_if = "Option::is_none",
44        deserialize_with = "super::validation::deserialize_present_option"
45    )]
46    pub admitted_dispute_digest: Option<String>,
47    #[serde(
48        default,
49        skip_serializing_if = "Option::is_none",
50        deserialize_with = "super::validation::deserialize_present_option"
51    )]
52    pub live_reservation_id: Option<String>,
53    #[serde(
54        default,
55        skip_serializing_if = "Option::is_none",
56        deserialize_with = "super::validation::deserialize_present_option"
57    )]
58    pub operation_id: Option<String>,
59}
60
61impl ChannelLifecycleViewV1 {
62    pub fn validate(&self) -> Result<(), ChannelError> {
63        if self.schema != CHANNEL_LIFECYCLE_SCHEMA {
64            return Err(ChannelError::InvalidField("channel_lifecycle_schema"));
65        }
66        validate_digest("lifecycle_channel_id", &self.channel_id)?;
67        validate_digest("lifecycle_latest_state_digest", &self.latest_state_digest)?;
68        validate_positive("lifecycle_state_version", self.state_version)?;
69        validate_positive("lifecycle_fence", self.lifecycle_fence)?;
70        if self.latest_sequence > I_JSON_MAX_SAFE_INTEGER {
71            return Err(ChannelError::InvalidField("lifecycle_latest_sequence"));
72        }
73        match (&self.live_reservation_id, &self.operation_id) {
74            (Some(reservation_id), Some(operation_id)) => {
75                validate_digest("lifecycle_live_reservation_id", reservation_id)?;
76                validate_digest("lifecycle_operation_id", operation_id)?;
77                if self.status != ChannelLifecycleStatusV1::Open {
78                    return Err(ChannelError::IllegalTransition);
79                }
80            }
81            (None, None) => {}
82            _ => return Err(ChannelError::InvalidField("lifecycle_live_reservation")),
83        }
84        match (self.status, &self.pending_close_body_digest) {
85            (
86                ChannelLifecycleStatusV1::ClosePending | ChannelLifecycleStatusV1::Closing,
87                Some(digest),
88            ) => {
89                validate_digest("lifecycle_pending_close_body_digest", digest)?;
90            }
91            (ChannelLifecycleStatusV1::ClosePending | ChannelLifecycleStatusV1::Closing, None) => {
92                return Err(ChannelError::InvalidField("lifecycle_pending_close"));
93            }
94            (_, None) => {}
95            (_, Some(_)) => return Err(ChannelError::InvalidField("lifecycle_pending_close")),
96        }
97        if let Some(digest) = &self.admitted_dispute_digest {
98            validate_digest("lifecycle_admitted_dispute_digest", digest)?;
99            if self.status != ChannelLifecycleStatusV1::ClosePending {
100                return Err(ChannelError::InvalidField(
101                    "lifecycle_admitted_dispute_digest",
102                ));
103            }
104        }
105        Ok(())
106    }
107
108    const fn lifecycle_state(&self) -> &'static str {
109        match self.status {
110            ChannelLifecycleStatusV1::Open => "open",
111            ChannelLifecycleStatusV1::ClosePending => "close_pending",
112            ChannelLifecycleStatusV1::Closing => "closing",
113            ChannelLifecycleStatusV1::Released => "released",
114            ChannelLifecycleStatusV1::Refunded => "refunded",
115            ChannelLifecycleStatusV1::Incident => "incident",
116        }
117    }
118}
119
120pub const CHANNEL_ESCROW_RESERVATION_SCHEMA: &str = "chio.channel.escrow-reservation.v1";
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum ChannelEscrowReservationStatusV1 {
125    Open,
126    Closing,
127    Released,
128    Refunded,
129    Incident,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase", deny_unknown_fields)]
134pub struct ChannelEscrowReservationViewV1 {
135    pub schema: String,
136    pub channel_id: String,
137    pub open_digest: String,
138    pub escrow_reference: ChannelEscrowReferenceV1,
139    pub status: ChannelEscrowReservationStatusV1,
140    pub version: u64,
141    pub lifecycle_fence: u64,
142    #[serde(
143        default,
144        skip_serializing_if = "Option::is_none",
145        deserialize_with = "super::validation::deserialize_present_option"
146    )]
147    pub pending_close_body_digest: Option<String>,
148}
149
150impl ChannelEscrowReservationViewV1 {
151    pub fn validate(&self) -> Result<(), ChannelError> {
152        if self.schema != CHANNEL_ESCROW_RESERVATION_SCHEMA {
153            return Err(ChannelError::InvalidField(
154                "channel_escrow_reservation_schema",
155            ));
156        }
157        validate_digest("escrow_reservation_channel_id", &self.channel_id)?;
158        validate_digest("escrow_reservation_open_digest", &self.open_digest)?;
159        self.escrow_reference.validate()?;
160        validate_positive("escrow_reservation_version", self.version)?;
161        validate_positive("escrow_reservation_fence", self.lifecycle_fence)?;
162        match (self.status, &self.pending_close_body_digest) {
163            (
164                ChannelEscrowReservationStatusV1::Open | ChannelEscrowReservationStatusV1::Closing,
165                Some(digest),
166            ) => {
167                validate_digest("escrow_reservation_pending_close", digest)?;
168            }
169            (ChannelEscrowReservationStatusV1::Closing, None) => {
170                return Err(ChannelError::InvalidField(
171                    "escrow_reservation_pending_close",
172                ));
173            }
174            (_, None) => {}
175            (_, Some(_)) => {
176                return Err(ChannelError::InvalidField(
177                    "escrow_reservation_pending_close",
178                ));
179            }
180        }
181        Ok(())
182    }
183
184    const fn lifecycle_state(&self) -> &'static str {
185        match self.status {
186            ChannelEscrowReservationStatusV1::Open => "open",
187            ChannelEscrowReservationStatusV1::Closing => "closing",
188            ChannelEscrowReservationStatusV1::Released => "released",
189            ChannelEscrowReservationStatusV1::Refunded => "refunded",
190            ChannelEscrowReservationStatusV1::Incident => "incident",
191        }
192    }
193}
194
195#[derive(Debug, Clone)]
196pub struct VerifiedChannelLifecycleSnapshotV1 {
197    lifecycle: ChannelLifecycleViewV1,
198    escrow: ChannelEscrowReservationViewV1,
199    settlement_authority_scope_id: String,
200    checkpoint_sequence: u64,
201    checkpoint_digest: String,
202    channel_head_digest: String,
203    escrow_head_digest: String,
204    observed_at_unix_ms: u64,
205    channel_head: EconomicResourceHeadV1,
206    escrow_head: EconomicResourceHeadV1,
207}
208
209impl VerifiedChannelLifecycleSnapshotV1 {
210    #[must_use]
211    pub const fn lifecycle(&self) -> &ChannelLifecycleViewV1 {
212        &self.lifecycle
213    }
214
215    #[must_use]
216    pub const fn escrow(&self) -> &ChannelEscrowReservationViewV1 {
217        &self.escrow
218    }
219
220    #[must_use]
221    pub fn settlement_authority_scope_id(&self) -> &str {
222        &self.settlement_authority_scope_id
223    }
224
225    #[must_use]
226    pub fn checkpoint_digest(&self) -> &str {
227        &self.checkpoint_digest
228    }
229
230    #[must_use]
231    pub const fn checkpoint_sequence(&self) -> u64 {
232        self.checkpoint_sequence
233    }
234
235    #[must_use]
236    pub fn channel_head_digest(&self) -> &str {
237        &self.channel_head_digest
238    }
239
240    #[must_use]
241    pub fn escrow_head_digest(&self) -> &str {
242        &self.escrow_head_digest
243    }
244
245    #[must_use]
246    pub fn channel_predecessor_digest(&self) -> Option<&str> {
247        self.channel_head.predecessor_digest.as_deref()
248    }
249
250    #[must_use]
251    pub fn escrow_predecessor_digest(&self) -> Option<&str> {
252        self.escrow_head.predecessor_digest.as_deref()
253    }
254
255    #[must_use]
256    pub const fn observed_at_unix_ms(&self) -> u64 {
257        self.observed_at_unix_ms
258    }
259
260    pub(super) const fn channel_head(&self) -> &EconomicResourceHeadV1 {
261        &self.channel_head
262    }
263
264    pub(super) const fn escrow_head(&self) -> &EconomicResourceHeadV1 {
265        &self.escrow_head
266    }
267}
268
269pub fn verify_channel_lifecycle_snapshot(
270    current: &VerifiedEconomicStateView,
271    settlement_authority_scope_id: &str,
272    channel_id: &str,
273) -> Result<VerifiedChannelLifecycleSnapshotV1, ChannelError> {
274    validate_digest("anchored_channel_id", channel_id)?;
275    super::validation::validate_text(
276        "anchored_settlement_authority_scope_id",
277        settlement_authority_scope_id,
278    )?;
279    let channel_key = EconomicResourceKeyV1 {
280        resource_family: CHANNEL_LIFECYCLE_RESOURCE_FAMILY.to_owned(),
281        scope_id: settlement_authority_scope_id.to_owned(),
282        resource_id: channel_id.to_owned(),
283    };
284    let escrow_key = EconomicResourceKeyV1 {
285        resource_family: CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY.to_owned(),
286        scope_id: settlement_authority_scope_id.to_owned(),
287        resource_id: channel_id.to_owned(),
288    };
289    let channel_head = current
290        .view()
291        .head(&channel_key)
292        .ok_or(ChannelError::AuthorityVerification)?;
293    let escrow_head = current
294        .view()
295        .head(&escrow_key)
296        .ok_or(ChannelError::AuthorityVerification)?;
297    let lifecycle: ChannelLifecycleViewV1 = decode_inline(channel_head)?;
298    let escrow: ChannelEscrowReservationViewV1 = decode_inline(escrow_head)?;
299    lifecycle.validate()?;
300    escrow.validate()?;
301    if lifecycle.channel_id != channel_id
302        || escrow.channel_id != channel_id
303        || channel_head.resource_version != lifecycle.state_version
304        || channel_head.lifecycle_fence != lifecycle.lifecycle_fence
305        || channel_head.lifecycle_state != lifecycle.lifecycle_state()
306        || channel_head.operation_id != lifecycle.operation_id
307        || escrow_head.resource_version != escrow.version
308        || escrow_head.lifecycle_fence != escrow.lifecycle_fence
309        || escrow_head.lifecycle_state != escrow.lifecycle_state()
310        || escrow_head.operation_id != lifecycle.operation_id
311        || lifecycle.lifecycle_fence != escrow.lifecycle_fence
312        || lifecycle.pending_close_body_digest != escrow.pending_close_body_digest
313        || !statuses_match(lifecycle.status, escrow.status)
314        || channel_head.trusted_clock_high_water > current.view().observed_at
315        || escrow_head.trusted_clock_high_water > current.view().observed_at
316    {
317        return Err(ChannelError::AuthorityVerification);
318    }
319    Ok(VerifiedChannelLifecycleSnapshotV1 {
320        lifecycle,
321        escrow,
322        settlement_authority_scope_id: settlement_authority_scope_id.to_owned(),
323        checkpoint_sequence: current.view().checkpoint_sequence,
324        checkpoint_digest: current.view().checkpoint_digest.clone(),
325        channel_head_digest: channel_head
326            .digest()
327            .map_err(|_| ChannelError::AuthorityVerification)?,
328        escrow_head_digest: escrow_head
329            .digest()
330            .map_err(|_| ChannelError::AuthorityVerification)?,
331        observed_at_unix_ms: current.view().observed_at,
332        channel_head: channel_head.clone(),
333        escrow_head: escrow_head.clone(),
334    })
335}
336
337fn decode_inline<T: DeserializeOwned>(head: &EconomicResourceHeadV1) -> Result<T, ChannelError> {
338    let EconomicContentV1::Inline { value } = &head.state else {
339        return Err(ChannelError::AuthorityVerification);
340    };
341    serde_json::from_value(value.clone()).map_err(|_| ChannelError::AuthorityVerification)
342}
343
344const fn statuses_match(
345    lifecycle: ChannelLifecycleStatusV1,
346    escrow: ChannelEscrowReservationStatusV1,
347) -> bool {
348    matches!(
349        (lifecycle, escrow),
350        (
351            ChannelLifecycleStatusV1::Open,
352            ChannelEscrowReservationStatusV1::Open
353        ) | (
354            ChannelLifecycleStatusV1::ClosePending,
355            ChannelEscrowReservationStatusV1::Open
356        ) | (
357            ChannelLifecycleStatusV1::Closing,
358            ChannelEscrowReservationStatusV1::Closing
359        ) | (
360            ChannelLifecycleStatusV1::Released,
361            ChannelEscrowReservationStatusV1::Released
362        ) | (
363            ChannelLifecycleStatusV1::Refunded,
364            ChannelEscrowReservationStatusV1::Refunded
365        ) | (
366            ChannelLifecycleStatusV1::Incident,
367            ChannelEscrowReservationStatusV1::Incident
368        )
369    )
370}