Skip to main content

chio_settle/channel/
projection.rs

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