Skip to main content

chio_settle/channel/projection/
cancellation.rs

1use chio_core::economic_continuity::{
2    economic_effect_slot_from_head, EconomicAdmissionHandoffV1, EconomicContentV1,
3    EconomicEffectSlotV1, EconomicEffectStateV1, EconomicEffectTargetV1, EconomicEffectTerminalV1,
4    EconomicNoEffectKindV1, EconomicRequestBindingV1, EconomicRequestReplayV1,
5    EconomicResourceHeadV1, EconomicResourceKeyV1, EconomicStateAnchorError, EconomicStateBatchV1,
6    EconomicStateTransitionV1, EconomicTransitionAuthorizationV1, EconomicTransitionProofVerifier,
7    VerifiedEconomicStateBatchAdvance, VerifiedEconomicStateView,
8};
9use serde::de::DeserializeOwned;
10use serde::{Deserialize, Serialize};
11
12use super::super::validation::{digest, validate_positive};
13use super::super::{
14    derive_channel_service_dispatch_idempotency_key, verify_channel_lifecycle_snapshot,
15    ChannelError, ChannelEscrowReservationStatusV1, ChannelEscrowReservationViewV1,
16    ChannelLifecycleStatusV1, ChannelLifecycleViewV1, VerifiedAdmittedChannelReservationV1,
17    CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY, CHANNEL_LIFECYCLE_RESOURCE_FAMILY,
18    CHANNEL_SERVICE_DISPATCH_EFFECT_KIND,
19};
20use super::{
21    head_digest, successor_head, transition, ChannelLifecycleBatchVerifier,
22    ChannelLifecycleProjectionV1, SuccessorHeadBinding,
23};
24
25pub const CHANNEL_PREDISPATCH_CANCELLATION_EVIDENCE_SCHEMA: &str =
26    "chio.channel.predispatch-cancellation-evidence.v1";
27
28const CHANNEL_PREDISPATCH_CANCELLATION_EVIDENCE_DOMAIN: &[u8] =
29    b"chio.channel.predispatch-cancellation-evidence.digest.v1\0";
30const CHANNEL_CANCELLATION_TRANSITION_PROOF_SCHEMA: &str =
31    "chio.channel.cancellation-transition-proof.v1";
32const CHANNEL_CANCELLATION_TRANSITION_PROOF_DOMAIN: &[u8] =
33    b"chio.channel.cancellation-transition-proof.digest.v1\0";
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37pub struct ChannelPredispatchCancellationEvidenceV1 {
38    pub schema: String,
39    pub operation_id: String,
40    pub reservation_id: String,
41    pub reservation_proposal_digest: String,
42    pub reservation_digest: String,
43    pub channel_id: String,
44    pub service_binding_digest: String,
45    pub request: EconomicRequestBindingV1,
46    pub admission_handoff: EconomicAdmissionHandoffV1,
47    pub provider: EconomicEffectTargetV1,
48    pub action_digest: String,
49    pub parameters_digest: String,
50    pub idempotency_key: String,
51    pub source_checkpoint_sequence: u64,
52    pub source_checkpoint_digest: String,
53    pub source_channel_head_digest: String,
54    pub source_escrow_head_digest: String,
55    pub source_effect_head_digest: String,
56    pub authenticated_clock_unix_ms: u64,
57    pub issued_at: u64,
58}
59
60impl ChannelPredispatchCancellationEvidenceV1 {
61    fn digest(&self) -> Result<String, ChannelError> {
62        digest(CHANNEL_PREDISPATCH_CANCELLATION_EVIDENCE_DOMAIN, self)
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67#[serde(rename_all = "camelCase")]
68struct ChannelCancellationTransitionProofV1 {
69    schema: String,
70    evidence: ChannelPredispatchCancellationEvidenceV1,
71    request_replay: EconomicRequestReplayV1,
72    released_channel_head_digest: String,
73    released_escrow_head_digest: String,
74    terminal_effect_head_digest: String,
75}
76
77impl ChannelCancellationTransitionProofV1 {
78    fn digest(&self) -> Result<String, ChannelError> {
79        digest(CHANNEL_CANCELLATION_TRANSITION_PROOF_DOMAIN, self)
80    }
81}
82
83pub fn compose_channel_cancellation_transition(
84    reservation: &VerifiedAdmittedChannelReservationV1,
85    current: &VerifiedEconomicStateView,
86    issued_at: u64,
87) -> Result<ChannelLifecycleProjectionV1, ChannelError> {
88    validate_positive("cancellation_issued_at", issued_at)?;
89    let body = &reservation.artifact().body;
90    let admitted = reservation.snapshot();
91    let ready_effect = reservation.ready_effect();
92    let snapshot = verify_channel_lifecycle_snapshot(
93        current,
94        admitted.settlement_authority_scope_id(),
95        &body.channel_id,
96    )?;
97    let channel_key = EconomicResourceKeyV1 {
98        resource_family: CHANNEL_LIFECYCLE_RESOURCE_FAMILY.to_owned(),
99        scope_id: admitted.settlement_authority_scope_id().to_owned(),
100        resource_id: body.channel_id.clone(),
101    };
102    let escrow_key = EconomicResourceKeyV1 {
103        resource_family: CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY.to_owned(),
104        scope_id: admitted.settlement_authority_scope_id().to_owned(),
105        resource_id: body.channel_id.clone(),
106    };
107    let effect_key = ready_effect.resource_head_key();
108    let channel_head = snapshot.channel_head();
109    let escrow_head = snapshot.escrow_head();
110    let effect_head = current
111        .view()
112        .head(&effect_key)
113        .ok_or(ChannelError::AuthorityVerification)?;
114    let retained_effect = economic_effect_slot_from_head(effect_head)
115        .map_err(|_| ChannelError::AuthorityVerification)?;
116    let reservation_digest = reservation.artifact().digest()?;
117    let expected_idempotency_key = derive_channel_service_dispatch_idempotency_key(
118        &body.operation_id,
119        &body.reservation_id,
120        body.next_sequence,
121    )?;
122    let request_replay = EconomicRequestReplayV1 {
123        request: ready_effect.request.clone(),
124        operation_id: body.operation_id.clone(),
125        effect_slot_ids: vec![ready_effect.slot_id.clone()],
126    };
127    request_replay
128        .validate()
129        .map_err(|_| ChannelError::AuthorityVerification)?;
130    let retained_replay = current
131        .view()
132        .request_replay(&ready_effect.request.key())
133        .ok_or(ChannelError::AuthorityVerification)?;
134    let source_channel_head_digest = head_digest(channel_head)?;
135    let source_escrow_head_digest = head_digest(escrow_head)?;
136    let source_effect_head_digest = head_digest(effect_head)?;
137    let source_clock = channel_head.trusted_clock_high_water;
138    let exact_checkpoint = current.view().checkpoint_sequence == admitted.checkpoint_sequence()
139        && current.view().checkpoint_digest == admitted.checkpoint_digest();
140    let later_checkpoint = current.view().checkpoint_sequence > admitted.checkpoint_sequence()
141        && current.view().checkpoint_digest != admitted.checkpoint_digest();
142    let lifecycle = snapshot.lifecycle();
143    let escrow = snapshot.escrow();
144    if !(exact_checkpoint || later_checkpoint)
145        || current.view().observed_at < admitted.observed_at_unix_ms()
146        || snapshot.lifecycle() != admitted.lifecycle()
147        || snapshot.escrow() != admitted.escrow()
148        || channel_head != admitted.channel_head()
149        || escrow_head != admitted.escrow_head()
150        || source_channel_head_digest != admitted.channel_head_digest()
151        || source_escrow_head_digest != admitted.escrow_head_digest()
152        || source_effect_head_digest != reservation.ready_effect_head_digest()
153        || retained_effect != *ready_effect
154        || retained_replay != &request_replay
155        || lifecycle.status != ChannelLifecycleStatusV1::Open
156        || lifecycle.channel_id != body.channel_id
157        || lifecycle.live_reservation_id.as_deref() != Some(body.reservation_id.as_str())
158        || lifecycle.operation_id.as_deref() != Some(body.operation_id.as_str())
159        || escrow.status != ChannelEscrowReservationStatusV1::Open
160        || escrow.channel_id != body.channel_id
161        || escrow.open_digest != body.open_digest
162        || escrow.lifecycle_fence != lifecycle.lifecycle_fence
163        || channel_head.resource_version != lifecycle.state_version
164        || channel_head.lifecycle_fence != lifecycle.lifecycle_fence
165        || channel_head.lifecycle_state != "open"
166        || channel_head.operation_id.as_deref() != Some(body.operation_id.as_str())
167        || channel_head.frost.is_some()
168        || channel_head.terminal_result.is_some()
169        || channel_head.predecessor_digest.is_none()
170        || escrow_head.resource_version != escrow.version
171        || escrow_head.lifecycle_fence != escrow.lifecycle_fence
172        || escrow_head.lifecycle_state != "open"
173        || escrow_head.operation_id.as_deref() != Some(body.operation_id.as_str())
174        || escrow_head.frost.is_some()
175        || escrow_head.terminal_result.is_some()
176        || escrow_head.predecessor_digest.is_none()
177        || escrow_head.trusted_clock_high_water != source_clock
178        || effect_head.trusted_clock_high_water != source_clock
179        || source_clock < reservation.accepted_at_unix_ms()
180        || source_clock > admitted.observed_at_unix_ms()
181        || retained_effect.operation_id != body.operation_id
182        || retained_effect.request.request_id != body.request_id
183        || retained_effect.effect_kind != CHANNEL_SERVICE_DISPATCH_EFFECT_KIND
184        || retained_effect.resource_key != channel_key
185        || retained_effect.resource_head_digest != source_channel_head_digest
186        || retained_effect.parameters_digest != reservation_digest
187        || retained_effect.idempotency_key != expected_idempotency_key
188        || retained_effect.state != EconomicEffectStateV1::Ready
189        || retained_effect.terminal.is_some()
190        || retained_effect.frost.is_some()
191        || effect_head.head_version != 1
192        || effect_head.resource_version != 1
193        || effect_head.lifecycle_fence != 1
194        || effect_head.lifecycle_state != "ready"
195        || effect_head.operation_id.as_deref() != Some(body.operation_id.as_str())
196        || effect_head.effect_idempotency_key.as_deref() != Some(expected_idempotency_key.as_str())
197        || effect_head.frost.is_some()
198        || effect_head.terminal_result.is_some()
199        || effect_head.predecessor_digest.is_some()
200        || issued_at < reservation.accepted_at_unix_ms()
201        || issued_at < current.view().observed_at
202    {
203        return Err(ChannelError::AuthorityVerification);
204    }
205
206    let state_version = lifecycle
207        .state_version
208        .checked_add(1)
209        .ok_or(ChannelError::ArithmeticOverflow)?;
210    let escrow_version = escrow
211        .version
212        .checked_add(1)
213        .ok_or(ChannelError::ArithmeticOverflow)?;
214    let lifecycle_fence = lifecycle
215        .lifecycle_fence
216        .checked_add(1)
217        .ok_or(ChannelError::ArithmeticOverflow)?;
218    let released_lifecycle = ChannelLifecycleViewV1 {
219        state_version,
220        lifecycle_fence,
221        live_reservation_id: None,
222        operation_id: None,
223        ..lifecycle.clone()
224    };
225    let released_escrow = ChannelEscrowReservationViewV1 {
226        version: escrow_version,
227        lifecycle_fence,
228        ..escrow.clone()
229    };
230    released_lifecycle.validate()?;
231    released_escrow.validate()?;
232    let released_channel_head = successor_head(
233        channel_head,
234        &released_lifecycle,
235        SuccessorHeadBinding {
236            resource_version: state_version,
237            lifecycle_fence,
238            lifecycle_state: "open",
239            operation_id: None,
240            effect_idempotency_key: None,
241            terminal_result: None,
242        },
243        issued_at,
244    )?;
245    let released_escrow_head = successor_head(
246        escrow_head,
247        &released_escrow,
248        SuccessorHeadBinding {
249            resource_version: escrow_version,
250            lifecycle_fence,
251            lifecycle_state: "open",
252            operation_id: None,
253            effect_idempotency_key: None,
254            terminal_result: None,
255        },
256        issued_at,
257    )?;
258    let evidence = ChannelPredispatchCancellationEvidenceV1 {
259        schema: CHANNEL_PREDISPATCH_CANCELLATION_EVIDENCE_SCHEMA.to_owned(),
260        operation_id: body.operation_id.clone(),
261        reservation_id: body.reservation_id.clone(),
262        reservation_proposal_digest: body.proposal_digest()?,
263        reservation_digest,
264        channel_id: body.channel_id.clone(),
265        service_binding_digest: body.service_binding_digest.clone(),
266        request: ready_effect.request.clone(),
267        admission_handoff: ready_effect.admission_handoff.clone(),
268        provider: ready_effect.target.clone(),
269        action_digest: ready_effect.action_digest.clone(),
270        parameters_digest: ready_effect.parameters_digest.clone(),
271        idempotency_key: ready_effect.idempotency_key.clone(),
272        source_checkpoint_sequence: current.view().checkpoint_sequence,
273        source_checkpoint_digest: current.view().checkpoint_digest.clone(),
274        source_channel_head_digest,
275        source_escrow_head_digest,
276        source_effect_head_digest,
277        authenticated_clock_unix_ms: source_clock,
278        issued_at,
279    };
280    let proof_id = evidence.digest()?;
281    let proof = EconomicContentV1::Inline {
282        value: serde_json::to_value(&evidence)
283            .map_err(|error| ChannelError::Canonicalization(error.to_string()))?,
284    };
285    let proof_digest = proof
286        .digest()
287        .map_err(|_| ChannelError::AuthorityVerification)?;
288    let mut terminal_effect = ready_effect.clone();
289    terminal_effect.state = EconomicEffectStateV1::NoEffect;
290    terminal_effect.terminal = Some(EconomicEffectTerminalV1::NoEffect {
291        kind: EconomicNoEffectKindV1::PreDispatch,
292        proof_id,
293        proof_digest,
294        proof,
295    });
296    ready_effect
297        .validate_successor(&terminal_effect)
298        .map_err(|_| ChannelError::AuthorityVerification)?;
299    let terminal_effect_head = successor_head(
300        effect_head,
301        &terminal_effect,
302        SuccessorHeadBinding {
303            resource_version: 2,
304            lifecycle_fence: 2,
305            lifecycle_state: "no_effect",
306            operation_id: Some(body.operation_id.clone()),
307            effect_idempotency_key: Some(expected_idempotency_key),
308            terminal_result: None,
309        },
310        issued_at,
311    )?;
312    let proof = ChannelCancellationTransitionProofV1 {
313        schema: CHANNEL_CANCELLATION_TRANSITION_PROOF_SCHEMA.to_owned(),
314        evidence,
315        request_replay,
316        released_channel_head_digest: head_digest(&released_channel_head)?,
317        released_escrow_head_digest: head_digest(&released_escrow_head)?,
318        terminal_effect_head_digest: head_digest(&terminal_effect_head)?,
319    };
320    let transition_proof_digest = proof.digest()?;
321    let mut transitions = vec![
322        transition(
323            channel_key,
324            proof.evidence.source_channel_head_digest.clone(),
325            released_channel_head,
326            &transition_proof_digest,
327        ),
328        transition(
329            escrow_key,
330            proof.evidence.source_escrow_head_digest.clone(),
331            released_escrow_head,
332            &transition_proof_digest,
333        ),
334        transition(
335            effect_key,
336            proof.evidence.source_effect_head_digest.clone(),
337            terminal_effect_head,
338            &transition_proof_digest,
339        ),
340    ];
341    transitions.sort_by(|left, right| left.resource_key.cmp(&right.resource_key));
342    Ok(ChannelLifecycleProjectionV1 {
343        current: current.clone(),
344        proof_digest: transition_proof_digest,
345        transitions,
346        effect_slots: Vec::new(),
347        request_replays: Vec::new(),
348        operation_id: body.operation_id.clone(),
349        issued_at,
350        not_after_unix_ms: None,
351    })
352}
353
354#[derive(Debug, Clone)]
355pub struct ChannelCancellationTransitionVerifierV1 {
356    reservation: VerifiedAdmittedChannelReservationV1,
357}
358
359impl ChannelCancellationTransitionVerifierV1 {
360    #[must_use]
361    pub const fn new(reservation: VerifiedAdmittedChannelReservationV1) -> Self {
362        Self { reservation }
363    }
364}
365
366impl EconomicTransitionProofVerifier for ChannelCancellationTransitionVerifierV1 {
367    fn verify_transition(
368        &self,
369        _current: Option<&EconomicResourceHeadV1>,
370        transition: &EconomicStateTransitionV1,
371    ) -> Result<EconomicTransitionAuthorizationV1, EconomicStateAnchorError> {
372        Err(EconomicStateAnchorError::TransitionProofRejected(
373            transition.resource_key.clone(),
374        ))
375    }
376
377    fn verify_batch(
378        &self,
379        current: &VerifiedEconomicStateView,
380        batch: &EconomicStateBatchV1,
381    ) -> Result<Vec<EconomicTransitionAuthorizationV1>, EconomicStateAnchorError> {
382        let projection =
383            compose_channel_cancellation_transition(&self.reservation, current, batch.issued_at)
384                .map_err(|_| rejected_batch(batch))?;
385        ChannelLifecycleBatchVerifier::new(projection).verify_batch(current, batch)
386    }
387}
388
389#[derive(Debug, Clone)]
390pub struct VerifiedChannelCancellationAdvanceV1 {
391    current_view: VerifiedEconomicStateView,
392    batch: EconomicStateBatchV1,
393    lifecycle: ChannelLifecycleViewV1,
394    escrow: ChannelEscrowReservationViewV1,
395    effect_slot: EconomicEffectSlotV1,
396    request_replay: EconomicRequestReplayV1,
397    evidence: ChannelPredispatchCancellationEvidenceV1,
398    reservation_digest: String,
399}
400
401impl VerifiedChannelCancellationAdvanceV1 {
402    #[must_use]
403    pub const fn current_view(&self) -> &VerifiedEconomicStateView {
404        &self.current_view
405    }
406
407    #[must_use]
408    pub const fn batch(&self) -> &EconomicStateBatchV1 {
409        &self.batch
410    }
411
412    #[must_use]
413    pub const fn lifecycle(&self) -> &ChannelLifecycleViewV1 {
414        &self.lifecycle
415    }
416
417    #[must_use]
418    pub const fn escrow(&self) -> &ChannelEscrowReservationViewV1 {
419        &self.escrow
420    }
421
422    #[must_use]
423    pub const fn effect_slot(&self) -> &EconomicEffectSlotV1 {
424        &self.effect_slot
425    }
426
427    #[must_use]
428    pub const fn request_replay(&self) -> &EconomicRequestReplayV1 {
429        &self.request_replay
430    }
431
432    #[must_use]
433    pub const fn evidence(&self) -> &ChannelPredispatchCancellationEvidenceV1 {
434        &self.evidence
435    }
436
437    #[must_use]
438    pub fn reservation_digest(&self) -> &str {
439        &self.reservation_digest
440    }
441}
442
443pub fn verify_channel_cancellation_advance(
444    reservation: &VerifiedAdmittedChannelReservationV1,
445    advance: &VerifiedEconomicStateBatchAdvance,
446) -> Result<VerifiedChannelCancellationAdvanceV1, ChannelError> {
447    let verifier = ChannelCancellationTransitionVerifierV1::new(reservation.clone());
448    verifier
449        .verify_batch(advance.current(), advance.batch())
450        .map_err(|_| ChannelError::AuthorityVerification)?;
451    let body = &reservation.artifact().body;
452    let scope_id = reservation.snapshot().settlement_authority_scope_id();
453    let channel_key = EconomicResourceKeyV1 {
454        resource_family: CHANNEL_LIFECYCLE_RESOURCE_FAMILY.to_owned(),
455        scope_id: scope_id.to_owned(),
456        resource_id: body.channel_id.clone(),
457    };
458    let escrow_key = EconomicResourceKeyV1 {
459        resource_family: CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY.to_owned(),
460        scope_id: scope_id.to_owned(),
461        resource_id: body.channel_id.clone(),
462    };
463    let channel_transition = exact_transition(advance.batch(), &channel_key)?;
464    let escrow_transition = exact_transition(advance.batch(), &escrow_key)?;
465    let effect_transition = exact_transition(
466        advance.batch(),
467        &reservation.ready_effect().resource_head_key(),
468    )?;
469    let lifecycle = decode_head(&channel_transition.next_head)?;
470    let escrow = decode_head(&escrow_transition.next_head)?;
471    let effect_slot = economic_effect_slot_from_head(&effect_transition.next_head)
472        .map_err(|_| ChannelError::AuthorityVerification)?;
473    let Some(EconomicEffectTerminalV1::NoEffect {
474        kind: EconomicNoEffectKindV1::PreDispatch,
475        proof_id,
476        proof_digest,
477        proof,
478    }) = &effect_slot.terminal
479    else {
480        return Err(ChannelError::AuthorityVerification);
481    };
482    let evidence: ChannelPredispatchCancellationEvidenceV1 = decode_content(proof)?;
483    if evidence.digest()? != *proof_id
484        || proof
485            .digest()
486            .map_err(|_| ChannelError::AuthorityVerification)?
487            != *proof_digest
488    {
489        return Err(ChannelError::AuthorityVerification);
490    }
491    let request_replay = advance
492        .current()
493        .view()
494        .request_replay(&effect_slot.request.key())
495        .ok_or(ChannelError::AuthorityVerification)?
496        .clone();
497    Ok(VerifiedChannelCancellationAdvanceV1 {
498        current_view: advance.current().clone(),
499        batch: advance.batch().clone(),
500        lifecycle,
501        escrow,
502        effect_slot,
503        request_replay,
504        evidence,
505        reservation_digest: reservation.artifact().digest()?,
506    })
507}
508
509fn exact_transition<'a>(
510    batch: &'a EconomicStateBatchV1,
511    resource_key: &EconomicResourceKeyV1,
512) -> Result<&'a EconomicStateTransitionV1, ChannelError> {
513    batch
514        .transitions
515        .iter()
516        .find(|transition| transition.resource_key == *resource_key)
517        .ok_or(ChannelError::AuthorityVerification)
518}
519
520fn decode_head<T: DeserializeOwned>(head: &EconomicResourceHeadV1) -> Result<T, ChannelError> {
521    decode_content(&head.state)
522}
523
524fn decode_content<T: DeserializeOwned>(content: &EconomicContentV1) -> Result<T, ChannelError> {
525    let EconomicContentV1::Inline { value } = content else {
526        return Err(ChannelError::AuthorityVerification);
527    };
528    serde_json::from_value(value.clone()).map_err(|_| ChannelError::AuthorityVerification)
529}
530
531fn rejected_batch(batch: &EconomicStateBatchV1) -> EconomicStateAnchorError {
532    match batch.transitions.first() {
533        Some(transition) => {
534            EconomicStateAnchorError::TransitionProofRejected(transition.resource_key.clone())
535        }
536        None => {
537            EconomicStateAnchorError::InvalidView("channel cancellation batch has no transition")
538        }
539    }
540}