Skip to main content

chio_settle/channel/
terminal.rs

1use chio_core::capability::scope::MonetaryAmount;
2use chio_core::economic_continuity::{
3    EconomicAdmissionHandoffStateV1, EconomicContentV1, EconomicEffectSlotV1,
4    EconomicEffectStateV1, EconomicEffectTerminalV1, EconomicResourceHeadV1, EconomicResourceKeyV1,
5    EconomicStateBatchV1, EconomicStateTransitionV1, EconomicTerminalResultV1,
6    VerifiedEconomicStateBatchAdvance, VerifiedEconomicStateView,
7};
8use chio_credit::obligation::ObligationAtomV1;
9use serde::de::DeserializeOwned;
10
11use super::validation::{digest, validate_digest, validate_positive};
12use super::{
13    ChannelError, ChannelEscrowReservationStatusV1, ChannelEscrowReservationViewV1,
14    ChannelLifecycleStatusV1, ChannelLifecycleViewV1, VerifiedAdmittedChannelReservationV1,
15    VerifiedChannelOpenConsentV1, VerifiedChannelReceiptBindingV1, VerifiedChannelStateV1,
16    VerifiedChannelTerminalOutcomeCommitmentV1, CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY,
17    CHANNEL_LIFECYCLE_RESOURCE_FAMILY,
18};
19
20pub const CHANNEL_SERVICE_DISPATCH_EFFECT_KIND: &str = "channel_service_dispatch";
21
22const CHANNEL_SERVICE_DISPATCH_IDEMPOTENCY_DOMAIN: &[u8] =
23    b"chio.channel.service-dispatch.idempotency.v1\0";
24
25pub fn derive_channel_service_dispatch_idempotency_key(
26    operation_id: &str,
27    reservation_id: &str,
28    sequence: u64,
29) -> Result<String, ChannelError> {
30    validate_digest("channel_service_operation_id", operation_id)?;
31    validate_digest("channel_service_reservation_id", reservation_id)?;
32    validate_positive("channel_service_sequence", sequence)?;
33    digest(
34        CHANNEL_SERVICE_DISPATCH_IDEMPOTENCY_DOMAIN,
35        &(operation_id, reservation_id, sequence),
36    )
37}
38
39#[derive(Debug, Clone)]
40pub struct VerifiedChannelTerminalAdvanceV1 {
41    open: VerifiedChannelOpenConsentV1,
42    reservation: VerifiedAdmittedChannelReservationV1,
43    prior_state: VerifiedChannelStateV1,
44    next_state: VerifiedChannelStateV1,
45    receipt: VerifiedChannelReceiptBindingV1,
46    current_view: VerifiedEconomicStateView,
47    batch: EconomicStateBatchV1,
48    terminal_lifecycle: ChannelLifecycleViewV1,
49    terminal_escrow: ChannelEscrowReservationViewV1,
50    completed_effect: EconomicEffectSlotV1,
51    open_digest: String,
52    reservation_digest: String,
53    prior_state_digest: String,
54    next_state_digest: String,
55    prior_channel_head_digest: String,
56    prior_escrow_head_digest: String,
57    prior_effect_head_digest: String,
58    terminal_channel_head_digest: String,
59    terminal_escrow_head_digest: String,
60    terminal_effect_head_digest: String,
61    effect_result_id: String,
62    effect_result_digest: String,
63    effect_result: EconomicContentV1,
64}
65
66impl VerifiedChannelTerminalAdvanceV1 {
67    #[must_use]
68    pub fn channel_id(&self) -> &str {
69        &self.next_state.body().channel_id
70    }
71
72    #[must_use]
73    pub const fn open(&self) -> &VerifiedChannelOpenConsentV1 {
74        &self.open
75    }
76
77    #[must_use]
78    pub fn open_digest(&self) -> &str {
79        &self.open_digest
80    }
81
82    #[must_use]
83    pub const fn reservation(&self) -> &VerifiedAdmittedChannelReservationV1 {
84        &self.reservation
85    }
86
87    #[must_use]
88    pub const fn reservation_proposal(&self) -> &super::VerifiedChannelReservationProposalV1 {
89        self.reservation.proposal()
90    }
91
92    #[must_use]
93    pub fn reservation_id(&self) -> &str {
94        &self.reservation.artifact().body.reservation_id
95    }
96
97    #[must_use]
98    pub fn reservation_digest(&self) -> &str {
99        &self.reservation_digest
100    }
101
102    #[must_use]
103    pub const fn prior_state(&self) -> &VerifiedChannelStateV1 {
104        &self.prior_state
105    }
106
107    #[must_use]
108    pub fn prior_state_digest(&self) -> &str {
109        &self.prior_state_digest
110    }
111
112    #[must_use]
113    pub const fn next_state(&self) -> &VerifiedChannelStateV1 {
114        &self.next_state
115    }
116
117    #[must_use]
118    pub fn next_state_digest(&self) -> &str {
119        &self.next_state_digest
120    }
121
122    #[must_use]
123    pub const fn receipt(&self) -> &VerifiedChannelReceiptBindingV1 {
124        &self.receipt
125    }
126
127    #[must_use]
128    pub const fn actual_charge(&self) -> &MonetaryAmount {
129        self.receipt.actual_charge()
130    }
131
132    #[must_use]
133    pub fn obligation_atom_id(&self) -> Option<&str> {
134        self.receipt.obligation_atom_id()
135    }
136
137    #[must_use]
138    pub fn obligation_atom_digest(&self) -> Option<&str> {
139        self.receipt.obligation_atom_digest()
140    }
141
142    #[must_use]
143    pub const fn obligation_atom(&self) -> Option<&ObligationAtomV1> {
144        self.receipt.obligation_atom()
145    }
146
147    #[must_use]
148    pub const fn current_view(&self) -> &VerifiedEconomicStateView {
149        &self.current_view
150    }
151
152    #[must_use]
153    pub const fn batch(&self) -> &EconomicStateBatchV1 {
154        &self.batch
155    }
156
157    #[must_use]
158    pub fn batch_id(&self) -> &str {
159        &self.batch.batch_id
160    }
161
162    #[must_use]
163    pub fn previous_checkpoint_digest(&self) -> &str {
164        &self.current_view.view().checkpoint_digest
165    }
166
167    #[must_use]
168    pub fn checkpoint_digest(&self) -> &str {
169        &self.batch.checkpoint_digest
170    }
171
172    #[must_use]
173    pub const fn batch_issued_at(&self) -> u64 {
174        self.batch.issued_at
175    }
176
177    #[must_use]
178    pub const fn terminal_lifecycle(&self) -> &ChannelLifecycleViewV1 {
179        &self.terminal_lifecycle
180    }
181
182    #[must_use]
183    pub const fn terminal_escrow(&self) -> &ChannelEscrowReservationViewV1 {
184        &self.terminal_escrow
185    }
186
187    #[must_use]
188    pub fn prior_channel_head_digest(&self) -> &str {
189        &self.prior_channel_head_digest
190    }
191
192    #[must_use]
193    pub fn prior_escrow_head_digest(&self) -> &str {
194        &self.prior_escrow_head_digest
195    }
196
197    #[must_use]
198    pub fn prior_effect_head_digest(&self) -> &str {
199        &self.prior_effect_head_digest
200    }
201
202    #[must_use]
203    pub fn terminal_channel_head_digest(&self) -> &str {
204        &self.terminal_channel_head_digest
205    }
206
207    #[must_use]
208    pub fn terminal_escrow_head_digest(&self) -> &str {
209        &self.terminal_escrow_head_digest
210    }
211
212    #[must_use]
213    pub fn terminal_effect_head_digest(&self) -> &str {
214        &self.terminal_effect_head_digest
215    }
216
217    #[must_use]
218    pub const fn effect_slot(&self) -> &EconomicEffectSlotV1 {
219        &self.completed_effect
220    }
221
222    #[must_use]
223    pub fn effect_head_digest(&self) -> &str {
224        &self.terminal_effect_head_digest
225    }
226
227    #[must_use]
228    pub fn effect_result_id(&self) -> &str {
229        &self.effect_result_id
230    }
231
232    #[must_use]
233    pub fn effect_result_digest(&self) -> &str {
234        &self.effect_result_digest
235    }
236
237    #[must_use]
238    pub fn effect_result(&self) -> &EconomicContentV1 {
239        &self.effect_result
240    }
241}
242
243pub fn verify_channel_terminal_advance(
244    open: &VerifiedChannelOpenConsentV1,
245    reservation: &VerifiedAdmittedChannelReservationV1,
246    prior_state: &VerifiedChannelStateV1,
247    next_state: &VerifiedChannelStateV1,
248    receipt: &VerifiedChannelReceiptBindingV1,
249    outcome: &VerifiedChannelTerminalOutcomeCommitmentV1,
250    advance: &VerifiedEconomicStateBatchAdvance,
251) -> Result<VerifiedChannelTerminalAdvanceV1, ChannelError> {
252    let body = &reservation.artifact().body;
253    let admitted_snapshot = reservation.snapshot();
254    let admitted_lifecycle = admitted_snapshot.lifecycle();
255    let admitted_escrow = admitted_snapshot.escrow();
256    let current_view = advance.current().view();
257    let batch = advance.batch();
258    let open_digest = open.artifact().digest()?;
259    let reservation_digest = reservation.artifact().digest()?;
260    let prior_state_digest = prior_state.digest()?;
261    let next_state_digest = next_state.digest()?;
262    let channel_key = EconomicResourceKeyV1 {
263        resource_family: CHANNEL_LIFECYCLE_RESOURCE_FAMILY.to_owned(),
264        scope_id: open.intent().body.settlement_authority_scope_id.clone(),
265        resource_id: body.channel_id.clone(),
266    };
267    let escrow_key = EconomicResourceKeyV1 {
268        resource_family: CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY.to_owned(),
269        scope_id: open.intent().body.settlement_authority_scope_id.clone(),
270        resource_id: body.channel_id.clone(),
271    };
272    if batch.transitions.len() != 3
273        || !batch.effect_slots.is_empty()
274        || !batch.request_replays.is_empty()
275        || batch
276            .transitions
277            .iter()
278            .any(|transition| transition.prepared_effect.is_some())
279        || batch.operation_id.as_deref() != Some(body.operation_id.as_str())
280        || batch.previous_checkpoint_digest.as_deref()
281            != Some(current_view.checkpoint_digest.as_str())
282        || current_view.checkpoint_sequence <= admitted_snapshot.checkpoint_sequence()
283        || current_view.checkpoint_digest == admitted_snapshot.checkpoint_digest()
284        || current_view.observed_at < admitted_snapshot.observed_at_unix_ms()
285        || batch.issued_at < current_view.observed_at
286    {
287        return Err(ChannelError::AuthorityVerification);
288    }
289    let channel_transition = exact_transition(batch, &channel_key)?;
290    let escrow_transition = exact_transition(batch, &escrow_key)?;
291    let effect_transition = batch
292        .transitions
293        .iter()
294        .find(|transition| transition.resource_key.resource_family == "effect_slot")
295        .ok_or(ChannelError::AuthorityVerification)?;
296    if effect_transition.resource_key.scope_id != channel_key.scope_id
297        || batch.transitions.iter().any(|transition| {
298            transition.resource_key != channel_key
299                && transition.resource_key != escrow_key
300                && transition.resource_key != effect_transition.resource_key
301        })
302    {
303        return Err(ChannelError::AuthorityVerification);
304    }
305    let current_channel_head = current_view
306        .head(&channel_key)
307        .ok_or(ChannelError::AuthorityVerification)?;
308    let current_escrow_head = current_view
309        .head(&escrow_key)
310        .ok_or(ChannelError::AuthorityVerification)?;
311    let current_effect_head = current_view
312        .head(&effect_transition.resource_key)
313        .ok_or(ChannelError::AuthorityVerification)?;
314    if current_channel_head != admitted_snapshot.channel_head()
315        || current_escrow_head != admitted_snapshot.escrow_head()
316    {
317        return Err(ChannelError::AuthorityVerification);
318    }
319    let prior_channel_head_digest = head_digest(current_channel_head)?;
320    let prior_escrow_head_digest = head_digest(current_escrow_head)?;
321    let prior_effect_head_digest = head_digest(current_effect_head)?;
322    if channel_transition.expected_head_digest.as_deref()
323        != Some(prior_channel_head_digest.as_str())
324        || escrow_transition.expected_head_digest.as_deref()
325            != Some(prior_escrow_head_digest.as_str())
326        || effect_transition.expected_head_digest.as_deref()
327            != Some(prior_effect_head_digest.as_str())
328    {
329        return Err(ChannelError::AuthorityVerification);
330    }
331    let terminal_lifecycle: ChannelLifecycleViewV1 = decode_head(&channel_transition.next_head)?;
332    let terminal_escrow: ChannelEscrowReservationViewV1 =
333        decode_head(&escrow_transition.next_head)?;
334    let dispatch_effect: EconomicEffectSlotV1 = decode_head(current_effect_head)?;
335    let completed_effect: EconomicEffectSlotV1 = decode_head(&effect_transition.next_head)?;
336    terminal_lifecycle.validate()?;
337    terminal_escrow.validate()?;
338    reservation
339        .ready_effect()
340        .validate_successor(&dispatch_effect)
341        .map_err(|_| ChannelError::AuthorityVerification)?;
342    dispatch_effect
343        .validate_successor(&completed_effect)
344        .map_err(|_| ChannelError::AuthorityVerification)?;
345    let expected_state_version = admitted_lifecycle
346        .state_version
347        .checked_add(1)
348        .ok_or(ChannelError::ArithmeticOverflow)?;
349    let expected_escrow_version = admitted_escrow
350        .version
351        .checked_add(1)
352        .ok_or(ChannelError::ArithmeticOverflow)?;
353    let expected_fence = admitted_lifecycle
354        .lifecycle_fence
355        .checked_add(1)
356        .ok_or(ChannelError::ArithmeticOverflow)?;
357    let expected_effect_version = current_effect_head
358        .resource_version
359        .checked_add(1)
360        .ok_or(ChannelError::ArithmeticOverflow)?;
361    let expected_effect_fence = current_effect_head
362        .lifecycle_fence
363        .checked_add(1)
364        .ok_or(ChannelError::ArithmeticOverflow)?;
365    let expected_idempotency_key = derive_channel_service_dispatch_idempotency_key(
366        &body.operation_id,
367        &body.reservation_id,
368        body.next_sequence,
369    )?;
370    let Some(EconomicEffectTerminalV1::Completed {
371        result_id,
372        result_digest,
373        result,
374    }) = completed_effect.terminal.as_ref()
375    else {
376        return Err(ChannelError::AuthorityVerification);
377    };
378    let expected_terminal_result = EconomicTerminalResultV1 {
379        result_id: result_id.clone(),
380        result_digest: result_digest.clone(),
381        result: result.clone(),
382    };
383    let obligation_matches_charge = matches!(
384        (
385            receipt.actual_charge().units,
386            receipt.obligation_atom_id(),
387            receipt.obligation_atom_digest(),
388        ),
389        (0, None, None) | (1.., Some(_), Some(_))
390    );
391    let obligation_time_is_ordered = match receipt.obligation_atom() {
392        Some(atom) => {
393            outcome.terminalized_at_unix_ms() <= atom.created_at_unix_ms()
394                && atom.created_at_unix_ms() <= batch.issued_at
395        }
396        None => true,
397    };
398    let next = next_state.body();
399    if body.channel_id != open.artifact().body.channel_id
400        || body.open_digest != open_digest
401        || body.prior_state_digest != prior_state_digest
402        || body.next_sequence != next.seq
403        || receipt.channel_id() != body.channel_id
404        || receipt.open_digest() != open_digest
405        || receipt.reservation_digest() != reservation_digest
406        || receipt.sequence() != body.next_sequence
407        || next.channel_id != body.channel_id
408        || next.prev_state_digest.as_deref() != Some(prior_state_digest.as_str())
409        || next.receipt_id.as_deref() != Some(receipt.receipt_id())
410        || next.receipt_digest.as_deref() != Some(receipt.receipt_digest())
411        || next.receipt_authority_digest.as_deref() != Some(receipt.receipt_authority_digest())
412        || next.obligation_atom_digest.as_deref() != receipt.obligation_atom_digest()
413        || next.reservation_digest.as_deref() != Some(reservation_digest.as_str())
414        || next.actual_charge.as_ref() != Some(receipt.actual_charge())
415        || !obligation_matches_charge
416        || !obligation_time_is_ordered
417        || outcome.terminal_result() != &expected_terminal_result
418        || outcome.terminalized_at_unix_ms() > batch.issued_at
419        || admitted_lifecycle.status != ChannelLifecycleStatusV1::Open
420        || admitted_lifecycle.channel_id != body.channel_id
421        || admitted_lifecycle.latest_state_digest != prior_state_digest
422        || admitted_lifecycle.latest_sequence != prior_state.body().seq
423        || admitted_lifecycle.live_reservation_id.as_deref() != Some(body.reservation_id.as_str())
424        || admitted_lifecycle.operation_id.as_deref() != Some(body.operation_id.as_str())
425        || admitted_escrow.status != ChannelEscrowReservationStatusV1::Open
426        || admitted_escrow.channel_id != body.channel_id
427        || admitted_escrow.open_digest != open_digest
428        || admitted_escrow.escrow_reference != open.intent().body.escrow_reference
429        || admitted_escrow.lifecycle_fence != admitted_lifecycle.lifecycle_fence
430        || terminal_lifecycle.status != ChannelLifecycleStatusV1::Open
431        || terminal_lifecycle.channel_id != body.channel_id
432        || terminal_lifecycle.latest_state_digest != next_state_digest
433        || terminal_lifecycle.latest_sequence != next.seq
434        || terminal_lifecycle.state_version != expected_state_version
435        || terminal_lifecycle.lifecycle_fence != expected_fence
436        || terminal_lifecycle.pending_close_body_digest.is_some()
437        || terminal_lifecycle.admitted_dispute_digest != admitted_lifecycle.admitted_dispute_digest
438        || terminal_lifecycle.live_reservation_id.is_some()
439        || terminal_lifecycle.operation_id.is_some()
440        || terminal_escrow.status != ChannelEscrowReservationStatusV1::Open
441        || terminal_escrow.channel_id != body.channel_id
442        || terminal_escrow.open_digest != open_digest
443        || terminal_escrow.escrow_reference != admitted_escrow.escrow_reference
444        || terminal_escrow.version != expected_escrow_version
445        || terminal_escrow.lifecycle_fence != expected_fence
446        || terminal_escrow.pending_close_body_digest.is_some()
447        || !released_head_matches(
448            current_channel_head,
449            channel_transition,
450            expected_state_version,
451            expected_fence,
452            batch.issued_at,
453        )?
454        || !released_head_matches(
455            current_escrow_head,
456            escrow_transition,
457            expected_escrow_version,
458            expected_fence,
459            batch.issued_at,
460        )?
461        || channel_transition.next_head.lifecycle_state != "open"
462        || escrow_transition.next_head.lifecycle_state != "open"
463        || dispatch_effect.operation_id != body.operation_id
464        || dispatch_effect.request.request_id != body.request_id
465        || dispatch_effect.effect_kind != CHANNEL_SERVICE_DISPATCH_EFFECT_KIND
466        || dispatch_effect.resource_key != channel_key
467        || dispatch_effect.resource_head_digest != prior_channel_head_digest
468        || dispatch_effect.admission_handoff.state
469            != EconomicAdmissionHandoffStateV1::DispatchCommitted
470        || dispatch_effect.parameters_digest != reservation_digest
471        || dispatch_effect.idempotency_key != expected_idempotency_key
472        || dispatch_effect.frost.is_some()
473        || dispatch_effect.state != EconomicEffectStateV1::DispatchCommitted
474        || dispatch_effect.terminal.is_some()
475        || dispatch_effect.resource_head_key() != effect_transition.resource_key
476        || current_effect_head.lifecycle_state != "dispatch_committed"
477        || current_effect_head.operation_id.as_deref() != Some(body.operation_id.as_str())
478        || current_effect_head.effect_idempotency_key.as_deref()
479            != Some(expected_idempotency_key.as_str())
480        || current_effect_head.frost.is_some()
481        || current_effect_head.terminal_result.is_some()
482        || current_effect_head.head_version != 2
483        || current_effect_head.predecessor_digest.as_deref()
484            != Some(reservation.ready_effect_head_digest())
485        || current_effect_head.resource_version != current_effect_head.head_version
486        || current_effect_head.lifecycle_fence != current_effect_head.head_version
487        || current_effect_head.trusted_clock_high_water
488            < reservation.snapshot().observed_at_unix_ms()
489        || current_effect_head.trusted_clock_high_water > current_view.observed_at
490        || completed_effect.state != EconomicEffectStateV1::Completed
491        || completed_effect.resource_head_key() != effect_transition.resource_key
492        || effect_transition.next_head.lifecycle_state != "completed"
493        || effect_transition.next_head.operation_id.as_deref() != Some(body.operation_id.as_str())
494        || effect_transition
495            .next_head
496            .effect_idempotency_key
497            .as_deref()
498            != Some(expected_idempotency_key.as_str())
499        || effect_transition.next_head.frost.is_some()
500        || effect_transition.next_head.terminal_result.as_ref() != Some(&expected_terminal_result)
501        || effect_transition.next_head.resource_version != expected_effect_version
502        || effect_transition.next_head.lifecycle_fence != expected_effect_fence
503        || effect_transition.next_head.resource_version != effect_transition.next_head.head_version
504        || effect_transition.next_head.lifecycle_fence != effect_transition.next_head.head_version
505        || effect_transition.next_head.trusted_clock_high_water != batch.issued_at
506    {
507        return Err(ChannelError::AuthorityVerification);
508    }
509    let effect_result_id = result_id.clone();
510    let effect_result_digest = result_digest.clone();
511    let effect_result = result.clone();
512    Ok(VerifiedChannelTerminalAdvanceV1 {
513        open: open.clone(),
514        reservation: reservation.clone(),
515        prior_state: prior_state.clone(),
516        next_state: next_state.clone(),
517        receipt: receipt.clone(),
518        current_view: advance.current().clone(),
519        batch: batch.clone(),
520        terminal_lifecycle,
521        terminal_escrow,
522        completed_effect,
523        open_digest,
524        reservation_digest,
525        prior_state_digest,
526        next_state_digest,
527        prior_channel_head_digest,
528        prior_escrow_head_digest,
529        prior_effect_head_digest,
530        terminal_channel_head_digest: head_digest(&channel_transition.next_head)?,
531        terminal_escrow_head_digest: head_digest(&escrow_transition.next_head)?,
532        terminal_effect_head_digest: head_digest(&effect_transition.next_head)?,
533        effect_result_id,
534        effect_result_digest,
535        effect_result,
536    })
537}
538
539fn exact_transition<'a>(
540    batch: &'a EconomicStateBatchV1,
541    key: &EconomicResourceKeyV1,
542) -> Result<&'a EconomicStateTransitionV1, ChannelError> {
543    batch
544        .transitions
545        .iter()
546        .find(|transition| transition.resource_key == *key)
547        .ok_or(ChannelError::AuthorityVerification)
548}
549
550fn decode_head<T: DeserializeOwned>(head: &EconomicResourceHeadV1) -> Result<T, ChannelError> {
551    let EconomicContentV1::Inline { value } = &head.state else {
552        return Err(ChannelError::AuthorityVerification);
553    };
554    serde_json::from_value(value.clone()).map_err(|_| ChannelError::AuthorityVerification)
555}
556
557fn head_digest(head: &EconomicResourceHeadV1) -> Result<String, ChannelError> {
558    head.digest()
559        .map_err(|_| ChannelError::AuthorityVerification)
560}
561
562fn released_head_matches(
563    current: &EconomicResourceHeadV1,
564    transition: &EconomicStateTransitionV1,
565    resource_version: u64,
566    lifecycle_fence: u64,
567    issued_at: u64,
568) -> Result<bool, ChannelError> {
569    let current_digest = head_digest(current)?;
570    Ok(transition.next_head.resource_version == resource_version
571        && transition.next_head.lifecycle_fence == lifecycle_fence
572        && transition.next_head.trusted_clock_high_water == issued_at
573        && transition.next_head.operation_id.is_none()
574        && transition.next_head.effect_idempotency_key.is_none()
575        && transition.next_head.frost.is_none()
576        && transition.next_head.terminal_result.is_none()
577        && transition.next_head.predecessor_digest == transition.expected_head_digest
578        && transition.next_head.predecessor_digest.as_deref() == Some(current_digest.as_str()))
579}