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}