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}