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}