1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4pub struct VerifiedChannelTerminalProjectionV1 {
5 binding: AdmissionExactProjectionBindingV1,
6 request_namespace_digest: RequestNamespaceDigest,
7 provider_attempt: ProviderAttemptBindingV1,
8 action_parameter_hash: AdmissionDigest,
9 proposal_digest: AdmissionDigest,
10 reservation_id: AdmissionIdentifier,
11 reservation_digest: AdmissionDigest,
12 receipt_id: AdmissionIdentifier,
13 receipt_digest: AdmissionDigest,
14 receipt_authority_digest: AdmissionDigest,
15 actual_charge: MonetaryAmount,
16 obligation_atom_id: Option<AdmissionIdentifier>,
17 obligation_atom_digest: Option<AdmissionDigest>,
18 signed_reservation: SignedChannelReservationV1,
19 signed_next_state: SignedChannelStateV1,
20 terminal_lifecycle: ChannelLifecycleViewV1,
21 terminal_escrow: ChannelEscrowReservationViewV1,
22 predecessor_view: EconomicStateAnchorViewV1,
23 terminal_batch: EconomicStateBatchV1,
24 batch_id: AdmissionDigest,
25 previous_checkpoint_digest: AdmissionDigest,
26 checkpoint_digest: AdmissionDigest,
27 batch_issued_at: u64,
28 prior_channel_head_digest: AdmissionDigest,
29 prior_escrow_head_digest: AdmissionDigest,
30 prior_effect_head_digest: AdmissionDigest,
31 terminal_channel_head_digest: AdmissionDigest,
32 terminal_escrow_head_digest: AdmissionDigest,
33 completed_effect_slot: EconomicEffectSlotV1,
34 completed_effect_head_digest: AdmissionDigest,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39struct UntrustedChannelTerminalProjectionV1 {
40 binding: AdmissionExactProjectionBindingV1,
41 request_namespace_digest: RequestNamespaceDigest,
42 provider_attempt: ProviderAttemptBindingV1,
43 action_parameter_hash: AdmissionDigest,
44 proposal_digest: AdmissionDigest,
45 reservation_id: AdmissionIdentifier,
46 reservation_digest: AdmissionDigest,
47 receipt_id: AdmissionIdentifier,
48 receipt_digest: AdmissionDigest,
49 receipt_authority_digest: AdmissionDigest,
50 actual_charge: MonetaryAmount,
51 obligation_atom_id: Option<AdmissionIdentifier>,
52 obligation_atom_digest: Option<AdmissionDigest>,
53 signed_reservation: SignedChannelReservationV1,
54 signed_next_state: SignedChannelStateV1,
55 terminal_lifecycle: ChannelLifecycleViewV1,
56 terminal_escrow: ChannelEscrowReservationViewV1,
57 predecessor_view: EconomicStateAnchorViewV1,
58 terminal_batch: EconomicStateBatchV1,
59 batch_id: AdmissionDigest,
60 previous_checkpoint_digest: AdmissionDigest,
61 checkpoint_digest: AdmissionDigest,
62 batch_issued_at: u64,
63 prior_channel_head_digest: AdmissionDigest,
64 prior_escrow_head_digest: AdmissionDigest,
65 prior_effect_head_digest: AdmissionDigest,
66 terminal_channel_head_digest: AdmissionDigest,
67 terminal_escrow_head_digest: AdmissionDigest,
68 completed_effect_slot: EconomicEffectSlotV1,
69 completed_effect_head_digest: AdmissionDigest,
70}
71
72impl From<UntrustedChannelTerminalProjectionV1> for VerifiedChannelTerminalProjectionV1 {
73 fn from(value: UntrustedChannelTerminalProjectionV1) -> Self {
74 Self {
75 binding: value.binding,
76 request_namespace_digest: value.request_namespace_digest,
77 provider_attempt: value.provider_attempt,
78 action_parameter_hash: value.action_parameter_hash,
79 proposal_digest: value.proposal_digest,
80 reservation_id: value.reservation_id,
81 reservation_digest: value.reservation_digest,
82 receipt_id: value.receipt_id,
83 receipt_digest: value.receipt_digest,
84 receipt_authority_digest: value.receipt_authority_digest,
85 actual_charge: value.actual_charge,
86 obligation_atom_id: value.obligation_atom_id,
87 obligation_atom_digest: value.obligation_atom_digest,
88 signed_reservation: value.signed_reservation,
89 signed_next_state: value.signed_next_state,
90 terminal_lifecycle: value.terminal_lifecycle,
91 terminal_escrow: value.terminal_escrow,
92 predecessor_view: value.predecessor_view,
93 terminal_batch: value.terminal_batch,
94 batch_id: value.batch_id,
95 previous_checkpoint_digest: value.previous_checkpoint_digest,
96 checkpoint_digest: value.checkpoint_digest,
97 batch_issued_at: value.batch_issued_at,
98 prior_channel_head_digest: value.prior_channel_head_digest,
99 prior_escrow_head_digest: value.prior_escrow_head_digest,
100 prior_effect_head_digest: value.prior_effect_head_digest,
101 terminal_channel_head_digest: value.terminal_channel_head_digest,
102 terminal_escrow_head_digest: value.terminal_escrow_head_digest,
103 completed_effect_slot: value.completed_effect_slot,
104 completed_effect_head_digest: value.completed_effect_head_digest,
105 }
106 }
107}
108
109impl VerifiedChannelTerminalProjectionV1 {
110 #[cfg(feature = "admission-test-support")]
111 pub fn from_verified_for_test(
112 operation: &AdmissionOperationV1,
113 context: &AdmissionProjectionContext,
114 receipt: &VerifiedAdmissionReceipt,
115 tool_outcome: &ToolOutcomeTerminalEvidenceV1,
116 advance: &VerifiedChannelTerminalAdvanceV1,
117 ) -> Result<(Self, Option<ObligationProjection>), AdmissionOperationError> {
118 Self::from_verified(operation, context, receipt, tool_outcome, advance)
119 }
120
121 pub(in crate::admission_operation) fn from_canonical_record_verified(
122 canonical_json: &[u8],
123 operation: &AdmissionOperationV1,
124 context: &AdmissionProjectionContext,
125 ) -> Result<Self, AdmissionOperationError> {
126 let untrusted: UntrustedChannelTerminalProjectionV1 =
127 serde_json::from_slice(canonical_json)
128 .map_err(|_| AdmissionOperationError::TerminalProjectionBindingMismatch)?;
129 if canonical_json_bytes(&untrusted)
130 .map_err(|error| AdmissionOperationError::CanonicalJson(error.to_string()))?
131 != canonical_json
132 {
133 return Err(AdmissionOperationError::TerminalProjectionBindingMismatch);
134 }
135 let projection = Self::from(untrusted);
136 projection.validate_record_binding(operation, context)?;
137 Ok(projection)
138 }
139
140 #[allow(clippy::too_many_arguments)]
141 pub(crate) fn from_verified(
142 operation: &AdmissionOperationV1,
143 context: &AdmissionProjectionContext,
144 receipt: &VerifiedAdmissionReceipt,
145 tool_outcome: &ToolOutcomeTerminalEvidenceV1,
146 advance: &VerifiedChannelTerminalAdvanceV1,
147 ) -> Result<(Self, Option<ObligationProjection>), AdmissionOperationError> {
148 let obligation = ObligationProjection::from_verified_channel_advance(
149 operation,
150 context,
151 receipt,
152 tool_outcome,
153 advance,
154 )?;
155 let payee_signature = advance
156 .next_state()
157 .payee_signature()
158 .cloned()
159 .ok_or(AdmissionOperationError::TerminalProjectionBindingMismatch)?;
160 let projection = Self {
161 binding: AdmissionExactProjectionBindingV1::from_verified(
162 operation,
163 context,
164 AdmissionOperationState::Completed,
165 )?,
166 request_namespace_digest: operation.binding().request_namespace_digest().clone(),
167 provider_attempt: operation
168 .provider_attempt()
169 .cloned()
170 .ok_or(AdmissionOperationError::TerminalProjectionBindingMismatch)?,
171 action_parameter_hash: operation.binding().action_parameter_hash().clone(),
172 proposal_digest: AdmissionDigest::try_new(
173 "channel_reservation_proposal_digest",
174 advance
175 .reservation_proposal()
176 .artifact()
177 .body
178 .proposal_digest()
179 .map_err(|_| AdmissionOperationError::TerminalProjectionBindingMismatch)?,
180 )?,
181 reservation_id: AdmissionIdentifier::try_new(
182 "channel_reservation_id",
183 advance.reservation_id().to_owned(),
184 )?,
185 reservation_digest: AdmissionDigest::try_new(
186 "channel_reservation_digest",
187 advance.reservation_digest().to_owned(),
188 )?,
189 receipt_id: AdmissionIdentifier::try_new(
190 "channel_receipt_id",
191 advance.receipt().receipt_id().to_owned(),
192 )?,
193 receipt_digest: AdmissionDigest::try_new(
194 "channel_receipt_digest",
195 advance.receipt().receipt_digest().to_owned(),
196 )?,
197 receipt_authority_digest: AdmissionDigest::try_new(
198 "channel_receipt_authority_digest",
199 advance.receipt().receipt_authority_digest().to_owned(),
200 )?,
201 actual_charge: advance.actual_charge().clone(),
202 obligation_atom_id: advance
203 .obligation_atom_id()
204 .map(|value| AdmissionIdentifier::try_new("channel_obligation_atom_id", value))
205 .transpose()?,
206 obligation_atom_digest: advance
207 .obligation_atom_digest()
208 .map(|value| AdmissionDigest::try_new("channel_obligation_atom_digest", value))
209 .transpose()?,
210 signed_reservation: advance.reservation().artifact().clone(),
211 signed_next_state: SignedChannelStateV1 {
212 body: advance.next_state().body().clone(),
213 payee_signature,
214 },
215 terminal_lifecycle: advance.terminal_lifecycle().clone(),
216 terminal_escrow: advance.terminal_escrow().clone(),
217 predecessor_view: advance.current_view().view().clone(),
218 terminal_batch: advance.batch().clone(),
219 batch_id: AdmissionDigest::try_new(
220 "channel_terminal_batch_id",
221 advance.batch_id().to_owned(),
222 )?,
223 previous_checkpoint_digest: AdmissionDigest::try_new(
224 "channel_previous_checkpoint_digest",
225 advance.previous_checkpoint_digest().to_owned(),
226 )?,
227 checkpoint_digest: AdmissionDigest::try_new(
228 "channel_checkpoint_digest",
229 advance.checkpoint_digest().to_owned(),
230 )?,
231 batch_issued_at: advance.batch_issued_at(),
232 prior_channel_head_digest: AdmissionDigest::try_new(
233 "channel_prior_head_digest",
234 advance.prior_channel_head_digest().to_owned(),
235 )?,
236 prior_escrow_head_digest: AdmissionDigest::try_new(
237 "channel_prior_escrow_head_digest",
238 advance.prior_escrow_head_digest().to_owned(),
239 )?,
240 prior_effect_head_digest: AdmissionDigest::try_new(
241 "channel_prior_effect_head_digest",
242 advance.prior_effect_head_digest().to_owned(),
243 )?,
244 terminal_channel_head_digest: AdmissionDigest::try_new(
245 "channel_terminal_head_digest",
246 advance.terminal_channel_head_digest().to_owned(),
247 )?,
248 terminal_escrow_head_digest: AdmissionDigest::try_new(
249 "channel_terminal_escrow_head_digest",
250 advance.terminal_escrow_head_digest().to_owned(),
251 )?,
252 completed_effect_slot: advance.effect_slot().clone(),
253 completed_effect_head_digest: AdmissionDigest::try_new(
254 "channel_completed_effect_head_digest",
255 advance.effect_head_digest().to_owned(),
256 )?,
257 };
258 projection.validate_against(
259 operation,
260 context,
261 receipt,
262 tool_outcome,
263 obligation.as_ref(),
264 )?;
265 Ok((projection, obligation))
266 }
267
268 fn validate_anchored_batch(
269 &self,
270 operation: &AdmissionOperationV1,
271 context: &AdmissionProjectionContext,
272 ) -> Result<(), AdmissionOperationError> {
273 let mismatch = || AdmissionOperationError::TerminalProjectionBindingMismatch;
274 self.predecessor_view.validate().map_err(|_| mismatch())?;
275 self.terminal_batch.validate().map_err(|_| mismatch())?;
276 validate_positive_ijson("channel_terminal_batch_issued_at", self.batch_issued_at)?;
277 let channel_key = &self.completed_effect_slot.resource_key;
278 let escrow_key = EconomicResourceKeyV1 {
279 resource_family: CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY.to_owned(),
280 scope_id: channel_key.scope_id.clone(),
281 resource_id: self.signed_reservation.body.channel_id.clone(),
282 };
283 let effect_key = self.completed_effect_slot.resource_head_key();
284 let transition = |key: &EconomicResourceKeyV1| {
285 self.terminal_batch
286 .transitions
287 .iter()
288 .find(|transition| &transition.resource_key == key)
289 .ok_or_else(mismatch)
290 };
291 let channel_transition = transition(channel_key)?;
292 let escrow_transition = transition(&escrow_key)?;
293 let effect_transition = transition(&effect_key)?;
294 let predecessor_head =
295 |key: &EconomicResourceKeyV1| self.predecessor_view.head(key).ok_or_else(mismatch);
296 let prior_channel = predecessor_head(channel_key)?
297 .digest()
298 .map_err(|_| mismatch())?;
299 let prior_escrow = predecessor_head(&escrow_key)?
300 .digest()
301 .map_err(|_| mismatch())?;
302 let prior_effect = predecessor_head(&effect_key)?
303 .digest()
304 .map_err(|_| mismatch())?;
305 let terminal_channel = channel_transition
306 .next_head
307 .digest()
308 .map_err(|_| mismatch())?;
309 let terminal_escrow = escrow_transition
310 .next_head
311 .digest()
312 .map_err(|_| mismatch())?;
313 let terminal_effect = effect_transition
314 .next_head
315 .digest()
316 .map_err(|_| mismatch())?;
317 let expected_channel = EconomicContentV1::Inline {
318 value: serde_json::to_value(&self.terminal_lifecycle)
319 .map_err(|error| AdmissionOperationError::CanonicalJson(error.to_string()))?,
320 };
321 let expected_escrow = EconomicContentV1::Inline {
322 value: serde_json::to_value(&self.terminal_escrow)
323 .map_err(|error| AdmissionOperationError::CanonicalJson(error.to_string()))?,
324 };
325 let expected_effect = EconomicContentV1::Inline {
326 value: serde_json::to_value(&self.completed_effect_slot)
327 .map_err(|error| AdmissionOperationError::CanonicalJson(error.to_string()))?,
328 };
329 if self.terminal_batch.transitions.len() != 3
330 || !self.terminal_batch.effect_slots.is_empty()
331 || !self.terminal_batch.request_replays.is_empty()
332 || self
333 .terminal_batch
334 .transitions
335 .iter()
336 .any(|transition| transition.prepared_effect.is_some())
337 || self.terminal_batch.operation_id.as_deref()
338 != Some(operation.binding().operation_id().as_str())
339 || self.terminal_batch.anchor_id != self.predecessor_view.anchor_id
340 || self.terminal_batch.namespace != self.predecessor_view.namespace
341 || self.terminal_batch.signer_key_id != self.predecessor_view.signer_key_id
342 || self.terminal_batch.signer_key_epoch != self.predecessor_view.signer_key_epoch
343 || self.terminal_batch.batch_id != self.batch_id.as_str()
344 || self.terminal_batch.previous_checkpoint_digest.as_deref()
345 != Some(self.previous_checkpoint_digest.as_str())
346 || self.predecessor_view.checkpoint_digest != self.previous_checkpoint_digest.as_str()
347 || self.terminal_batch.checkpoint_digest != self.checkpoint_digest.as_str()
348 || self.terminal_batch.issued_at != self.batch_issued_at
349 || context.trusted_time_unix_ms > self.batch_issued_at
350 || self.batch_issued_at < self.predecessor_view.observed_at
351 || channel_transition.expected_head_digest.as_deref()
352 != Some(self.prior_channel_head_digest.as_str())
353 || escrow_transition.expected_head_digest.as_deref()
354 != Some(self.prior_escrow_head_digest.as_str())
355 || effect_transition.expected_head_digest.as_deref()
356 != Some(self.prior_effect_head_digest.as_str())
357 || prior_channel != self.prior_channel_head_digest.as_str()
358 || prior_escrow != self.prior_escrow_head_digest.as_str()
359 || prior_effect != self.prior_effect_head_digest.as_str()
360 || terminal_channel != self.terminal_channel_head_digest.as_str()
361 || terminal_escrow != self.terminal_escrow_head_digest.as_str()
362 || terminal_effect != self.completed_effect_head_digest.as_str()
363 || channel_transition.next_head.state != expected_channel
364 || escrow_transition.next_head.state != expected_escrow
365 || effect_transition.next_head.state != expected_effect
366 {
367 return Err(mismatch());
368 }
369 Ok(())
370 }
371
372 fn validate_record_binding(
373 &self,
374 operation: &AdmissionOperationV1,
375 context: &AdmissionProjectionContext,
376 ) -> Result<(), AdmissionOperationError> {
377 let mismatch = || AdmissionOperationError::TerminalProjectionBindingMismatch;
378 self.binding
379 .validate_against(operation, context, AdmissionOperationState::Completed)?;
380 self.completed_effect_slot
381 .validate()
382 .map_err(|_| mismatch())?;
383 self.terminal_lifecycle.validate().map_err(|_| mismatch())?;
384 self.terminal_escrow.validate().map_err(|_| mismatch())?;
385 self.validate_anchored_batch(operation, context)?;
386 let requirements = operation.binding().participant_requirements();
387 let commit = operation.dispatch_commit().ok_or_else(mismatch)?;
388 let provider_attempt = operation.provider_attempt().ok_or_else(mismatch)?;
389 let proposal_digest = AdmissionDigest::try_new(
390 "channel_reservation_proposal_digest",
391 self.signed_reservation
392 .body
393 .proposal_digest()
394 .map_err(|_| mismatch())?,
395 )?;
396 let reservation_digest = AdmissionDigest::try_new(
397 "channel_reservation_digest",
398 self.signed_reservation.digest().map_err(|_| mismatch())?,
399 )?;
400 let next_state_digest = self.signed_next_state.digest().map_err(|_| mismatch())?;
401 let obligation_shape_matches = matches!(
402 (
403 self.actual_charge.units,
404 self.obligation_atom_id.as_ref(),
405 self.obligation_atom_digest.as_ref(),
406 ),
407 (0, None, None) | (1.., Some(_), Some(_))
408 );
409 if !requirements.channel
410 || !requirements.obligation
411 || self.actual_charge.units > I_JSON_MAX_SAFE_INTEGER
412 || self.actual_charge.currency.len() != 3
413 || !self
414 .actual_charge
415 .currency
416 .bytes()
417 .all(|byte| byte.is_ascii_uppercase())
418 || self.request_namespace_digest != *operation.binding().request_namespace_digest()
419 || self.provider_attempt != *provider_attempt
420 || commit.provider_attempt.as_ref() != Some(provider_attempt)
421 || self.action_parameter_hash != *operation.binding().action_parameter_hash()
422 || self.proposal_digest != proposal_digest
423 || self.reservation_id.as_str() != self.signed_reservation.body.reservation_id
424 || self.reservation_digest != reservation_digest
425 || operation.channel_reservation_proposal_digest() != Some(&self.proposal_digest)
426 || operation.channel_reservation_digest() != Some(&self.reservation_digest)
427 || self.signed_reservation.body.operation_id
428 != operation.binding().operation_id().as_str()
429 || self.signed_reservation.body.request_id != operation.replay_key().request_id.as_str()
430 || self.signed_reservation.body.receipt_authority_digest
431 != self.receipt_authority_digest.as_str()
432 || self.actual_charge.currency != self.signed_reservation.body.maximum_charge.currency
433 || self.actual_charge.units > self.signed_reservation.body.maximum_charge.units
434 || self.completed_effect_slot.operation_id
435 != operation.binding().operation_id().as_str()
436 || self.completed_effect_slot.request.request_id
437 != operation.replay_key().request_id.as_str()
438 || self.completed_effect_slot.request.request_namespace_digest
439 != operation.replay_key().request_namespace_digest.as_str()
440 || self.completed_effect_slot.request.request_binding_digest
441 != operation.binding().request_binding_hash().as_str()
442 || self.completed_effect_slot.admission_handoff.state
443 != EconomicAdmissionHandoffStateV1::DispatchCommitted
444 || self
445 .completed_effect_slot
446 .admission_handoff
447 .operation_version
448 != commit.committed_version
449 || self.completed_effect_slot.admission_handoff.lifecycle_fence
450 != commit.coordinator_lease_epoch
451 || self.completed_effect_slot.admission_handoff.store_fence != commit.store_fence
452 || self.completed_effect_slot.target.target_id != provider_attempt.transport_id
453 || self.completed_effect_slot.target.target_key_epoch
454 != provider_attempt.transport_key_epoch
455 || self.completed_effect_slot.action_digest != self.action_parameter_hash.as_str()
456 || self.completed_effect_slot.parameters_digest != self.reservation_digest.as_str()
457 || self.completed_effect_slot.resource_head_digest
458 != self.prior_channel_head_digest.as_str()
459 || self.signed_next_state.body.receipt_id.as_deref() != Some(self.receipt_id.as_str())
460 || self.signed_next_state.body.receipt_digest.as_deref()
461 != Some(self.receipt_digest.as_str())
462 || self
463 .signed_next_state
464 .body
465 .receipt_authority_digest
466 .as_deref()
467 != Some(self.receipt_authority_digest.as_str())
468 || self
469 .signed_next_state
470 .body
471 .obligation_atom_digest
472 .as_deref()
473 != self
474 .obligation_atom_digest
475 .as_ref()
476 .map(AdmissionDigest::as_str)
477 || self.signed_next_state.body.actual_charge.as_ref() != Some(&self.actual_charge)
478 || self.signed_next_state.body.reservation_digest.as_deref()
479 != Some(self.reservation_digest.as_str())
480 || self.terminal_lifecycle.channel_id != self.signed_reservation.body.channel_id
481 || self.terminal_lifecycle.latest_state_digest != next_state_digest
482 || self.terminal_lifecycle.latest_sequence != self.signed_next_state.body.seq
483 || self.terminal_lifecycle.live_reservation_id.is_some()
484 || self.terminal_lifecycle.operation_id.is_some()
485 || self.terminal_escrow.channel_id != self.signed_reservation.body.channel_id
486 || self.terminal_escrow.open_digest != self.signed_reservation.body.open_digest
487 || self.terminal_escrow.lifecycle_fence != self.terminal_lifecycle.lifecycle_fence
488 || !obligation_shape_matches
489 {
490 return Err(mismatch());
491 }
492 Ok(())
493 }
494
495 pub(super) fn validate_against(
496 &self,
497 operation: &AdmissionOperationV1,
498 context: &AdmissionProjectionContext,
499 receipt: &VerifiedAdmissionReceipt,
500 tool_outcome: &ToolOutcomeTerminalEvidenceV1,
501 obligation: Option<&ObligationProjection>,
502 ) -> Result<(), AdmissionOperationError> {
503 let mismatch = || AdmissionOperationError::TerminalProjectionBindingMismatch;
504 self.binding
505 .validate_against(operation, context, AdmissionOperationState::Completed)?;
506 tool_outcome
507 .validate_against(operation, context)
508 .map_err(|_| mismatch())?;
509 receipt.validate_against(
510 operation,
511 context,
512 AdmissionOperationState::Completed,
513 AdmissionCompensationStatus::NotCompensated,
514 Some((tool_outcome.outcome_id(), tool_outcome.outcome_version())),
515 )?;
516 self.completed_effect_slot
517 .validate()
518 .map_err(|_| mismatch())?;
519 self.terminal_lifecycle.validate().map_err(|_| mismatch())?;
520 self.terminal_escrow.validate().map_err(|_| mismatch())?;
521 self.validate_anchored_batch(operation, context)?;
522 let requirements = operation.binding().participant_requirements();
523 let commit = operation.dispatch_commit().ok_or_else(mismatch)?;
524 let provider_attempt = operation.provider_attempt().ok_or_else(mismatch)?;
525 if let Some(obligation) = obligation {
526 obligation.validate_against(
527 operation,
528 context,
529 receipt,
530 tool_outcome.outcome_id(),
531 tool_outcome.outcome_version(),
532 )?;
533 }
534 let expected_result = EconomicContentV1::Inline {
535 value: serde_json::to_value(tool_outcome)
536 .map_err(|error| AdmissionOperationError::CanonicalJson(error.to_string()))?,
537 };
538 let expected_result_digest = AdmissionDigest::try_new(
539 "channel_effect_result_digest",
540 expected_result.digest().map_err(|_| mismatch())?,
541 )?;
542 let effect_result_matches = matches!(
543 self.completed_effect_slot.terminal.as_ref(),
544 Some(chio_core::economic_continuity::EconomicEffectTerminalV1::Completed {
545 result_id,
546 result_digest,
547 result,
548 }) if result_id == tool_outcome.outcome_id().as_str()
549 && result_digest == expected_result_digest.as_str()
550 && result == &expected_result
551 );
552 let proposal_digest = AdmissionDigest::try_new(
553 "channel_reservation_proposal_digest",
554 self.signed_reservation
555 .body
556 .proposal_digest()
557 .map_err(|_| mismatch())?,
558 )?;
559 let reservation_digest = AdmissionDigest::try_new(
560 "channel_reservation_digest",
561 self.signed_reservation.digest().map_err(|_| mismatch())?,
562 )?;
563 let next_state_digest = self.signed_next_state.digest().map_err(|_| mismatch())?;
564 let receipt_digest = receipt_digest(receipt.receipt())?;
565 let expected_receipt_authority_digest = AdmissionDigest::try_new(
566 "channel_receipt_authority_digest",
567 derive_channel_receipt_authority_digest(&receipt.receipt().kernel_key)
568 .map_err(|_| mismatch())?,
569 )?;
570 let financial = receipt
571 .receipt()
572 .financial_metadata()
573 .ok_or_else(mismatch)?;
574 let channel = receipt.receipt().channel_metadata().ok_or_else(mismatch)?;
575 let disposition_matches = match tool_outcome.settlement_disposition() {
576 SettlementDispositionV1::Capture { amount } => {
577 self.actual_charge.units > 0 && amount == &self.actual_charge
578 }
579 SettlementDispositionV1::ContractualZeroCharge { currency } => {
580 self.actual_charge.units == 0 && currency == &self.actual_charge.currency
581 }
582 SettlementDispositionV1::NotApplicable => self.actual_charge.units == 0,
583 };
584 let obligation_matches = if self.actual_charge.units == 0 {
585 obligation.is_none()
586 && self.obligation_atom_id.is_none()
587 && self.obligation_atom_digest.is_none()
588 } else {
589 let Some(obligation) = obligation else {
590 return Err(mismatch());
591 };
592 self.obligation_atom_id.as_ref() == Some(obligation.obligation_id())
593 && self.obligation_atom_digest.as_ref() == Some(obligation.atom_digest())
594 && obligation.amount() == &self.actual_charge
595 && obligation.disposition()
596 == &ObligationDispositionV1::Channelized {
597 channel_id: self.signed_reservation.body.channel_id.clone(),
598 reservation_id: self.signed_reservation.body.reservation_id.clone(),
599 }
600 };
601 if !requirements.channel
602 || !requirements.obligation
603 || self.request_namespace_digest != *operation.binding().request_namespace_digest()
604 || self.provider_attempt != *provider_attempt
605 || commit.provider_attempt.as_ref() != Some(provider_attempt)
606 || self.action_parameter_hash != *operation.binding().action_parameter_hash()
607 || self.proposal_digest != proposal_digest
608 || self.reservation_id.as_str() != self.signed_reservation.body.reservation_id
609 || self.reservation_digest != reservation_digest
610 || operation.channel_reservation_proposal_digest() != Some(&self.proposal_digest)
611 || operation.channel_reservation_digest() != Some(&self.reservation_digest)
612 || self.signed_reservation.body.operation_id
613 != operation.binding().operation_id().as_str()
614 || self.signed_reservation.body.request_id != operation.replay_key().request_id.as_str()
615 || self.signed_reservation.body.receipt_authority_digest
616 != self.receipt_authority_digest.as_str()
617 || self.completed_effect_slot.operation_id
618 != operation.binding().operation_id().as_str()
619 || self.completed_effect_slot.request.request_id
620 != operation.replay_key().request_id.as_str()
621 || self.completed_effect_slot.request.request_namespace_digest
622 != operation.replay_key().request_namespace_digest.as_str()
623 || self.completed_effect_slot.request.request_binding_digest
624 != operation.binding().request_binding_hash().as_str()
625 || self.completed_effect_slot.admission_handoff.state
626 != EconomicAdmissionHandoffStateV1::DispatchCommitted
627 || self
628 .completed_effect_slot
629 .admission_handoff
630 .operation_version
631 != commit.committed_version
632 || self.completed_effect_slot.admission_handoff.lifecycle_fence
633 != commit.coordinator_lease_epoch
634 || self.completed_effect_slot.admission_handoff.store_fence != commit.store_fence
635 || self.completed_effect_slot.target.target_id != provider_attempt.transport_id
636 || self.completed_effect_slot.target.target_key_epoch
637 != provider_attempt.transport_key_epoch
638 || self.completed_effect_slot.action_digest != self.action_parameter_hash.as_str()
639 || self.completed_effect_slot.parameters_digest != self.reservation_digest.as_str()
640 || self.completed_effect_slot.resource_head_digest
641 != self.prior_channel_head_digest.as_str()
642 || self.receipt_id.as_str() != receipt.receipt().id
643 || self.receipt_digest != receipt_digest
644 || self.receipt_authority_digest != expected_receipt_authority_digest
645 || financial.cost_charged != self.actual_charge.units
646 || financial.currency != self.actual_charge.currency
647 || !channel.is_valid()
648 || channel.channel_id != self.signed_reservation.body.channel_id
649 || channel.open_digest != self.signed_reservation.body.open_digest
650 || channel.reservation_id != self.signed_reservation.body.reservation_id
651 || channel.reservation_digest != self.reservation_digest.as_str()
652 || channel.sequence != self.signed_reservation.body.next_sequence
653 || self.signed_next_state.body.receipt_id.as_deref() != Some(self.receipt_id.as_str())
654 || self.signed_next_state.body.receipt_digest.as_deref()
655 != Some(self.receipt_digest.as_str())
656 || self
657 .signed_next_state
658 .body
659 .receipt_authority_digest
660 .as_deref()
661 != Some(self.receipt_authority_digest.as_str())
662 || self
663 .signed_next_state
664 .body
665 .obligation_atom_digest
666 .as_deref()
667 != self
668 .obligation_atom_digest
669 .as_ref()
670 .map(AdmissionDigest::as_str)
671 || self.signed_next_state.body.actual_charge.as_ref() != Some(&self.actual_charge)
672 || self.signed_next_state.body.reservation_digest.as_deref()
673 != Some(self.reservation_digest.as_str())
674 || self.terminal_lifecycle.channel_id != self.signed_reservation.body.channel_id
675 || self.terminal_lifecycle.latest_state_digest != next_state_digest
676 || self.terminal_lifecycle.latest_sequence != self.signed_next_state.body.seq
677 || self.terminal_lifecycle.live_reservation_id.is_some()
678 || self.terminal_lifecycle.operation_id.is_some()
679 || self.terminal_escrow.channel_id != self.signed_reservation.body.channel_id
680 || self.terminal_escrow.open_digest != self.signed_reservation.body.open_digest
681 || self.terminal_escrow.lifecycle_fence != self.terminal_lifecycle.lifecycle_fence
682 || !effect_result_matches
683 || !disposition_matches
684 || !obligation_matches
685 {
686 return Err(mismatch());
687 }
688 Ok(())
689 }
690
691 #[must_use]
692 pub const fn record_id(&self) -> &AdmissionIdentifier {
693 &self.reservation_id
694 }
695
696 #[must_use]
697 pub const fn operation_binding(&self) -> &AdmissionExactProjectionBindingV1 {
698 &self.binding
699 }
700
701 #[must_use]
702 pub const fn request_namespace_digest(&self) -> &RequestNamespaceDigest {
703 &self.request_namespace_digest
704 }
705
706 #[must_use]
707 pub const fn provider_attempt(&self) -> &ProviderAttemptBindingV1 {
708 &self.provider_attempt
709 }
710
711 #[must_use]
712 pub const fn reservation_digest(&self) -> &AdmissionDigest {
713 &self.reservation_digest
714 }
715
716 #[must_use]
717 pub const fn receipt_id(&self) -> &AdmissionIdentifier {
718 &self.receipt_id
719 }
720
721 #[must_use]
722 pub const fn receipt_digest(&self) -> &AdmissionDigest {
723 &self.receipt_digest
724 }
725
726 #[must_use]
727 pub const fn receipt_authority_digest(&self) -> &AdmissionDigest {
728 &self.receipt_authority_digest
729 }
730
731 #[must_use]
732 pub const fn actual_charge(&self) -> &MonetaryAmount {
733 &self.actual_charge
734 }
735
736 #[must_use]
737 pub const fn obligation_atom_id(&self) -> Option<&AdmissionIdentifier> {
738 self.obligation_atom_id.as_ref()
739 }
740
741 #[must_use]
742 pub const fn obligation_atom_digest(&self) -> Option<&AdmissionDigest> {
743 self.obligation_atom_digest.as_ref()
744 }
745
746 #[must_use]
747 pub const fn signed_reservation(&self) -> &SignedChannelReservationV1 {
748 &self.signed_reservation
749 }
750
751 #[must_use]
752 pub const fn signed_next_state(&self) -> &SignedChannelStateV1 {
753 &self.signed_next_state
754 }
755
756 #[must_use]
757 pub const fn terminal_lifecycle(&self) -> &ChannelLifecycleViewV1 {
758 &self.terminal_lifecycle
759 }
760
761 #[must_use]
762 pub const fn terminal_escrow(&self) -> &ChannelEscrowReservationViewV1 {
763 &self.terminal_escrow
764 }
765
766 #[must_use]
767 pub const fn predecessor_view(&self) -> &EconomicStateAnchorViewV1 {
768 &self.predecessor_view
769 }
770
771 #[must_use]
772 pub const fn terminal_batch(&self) -> &EconomicStateBatchV1 {
773 &self.terminal_batch
774 }
775
776 #[must_use]
777 pub const fn batch_id(&self) -> &AdmissionDigest {
778 &self.batch_id
779 }
780
781 #[must_use]
782 pub const fn previous_checkpoint_digest(&self) -> &AdmissionDigest {
783 &self.previous_checkpoint_digest
784 }
785
786 #[must_use]
787 pub const fn checkpoint_digest(&self) -> &AdmissionDigest {
788 &self.checkpoint_digest
789 }
790
791 #[must_use]
792 pub const fn batch_issued_at(&self) -> u64 {
793 self.batch_issued_at
794 }
795
796 #[must_use]
797 pub const fn prior_channel_head_digest(&self) -> &AdmissionDigest {
798 &self.prior_channel_head_digest
799 }
800
801 #[must_use]
802 pub const fn prior_escrow_head_digest(&self) -> &AdmissionDigest {
803 &self.prior_escrow_head_digest
804 }
805
806 #[must_use]
807 pub const fn prior_effect_head_digest(&self) -> &AdmissionDigest {
808 &self.prior_effect_head_digest
809 }
810
811 #[must_use]
812 pub const fn terminal_channel_head_digest(&self) -> &AdmissionDigest {
813 &self.terminal_channel_head_digest
814 }
815
816 #[must_use]
817 pub const fn terminal_escrow_head_digest(&self) -> &AdmissionDigest {
818 &self.terminal_escrow_head_digest
819 }
820
821 #[must_use]
822 pub const fn terminal_effect_head_digest(&self) -> &AdmissionDigest {
823 &self.completed_effect_head_digest
824 }
825
826 #[must_use]
827 pub const fn completed_effect_slot(&self) -> &EconomicEffectSlotV1 {
828 &self.completed_effect_slot
829 }
830
831 pub fn qualify_anchored_advance(
832 &self,
833 advance: &VerifiedEconomicStateBatchAdvance,
834 ) -> Result<&EconomicEffectSlotV1, AdmissionOperationError> {
835 self.qualify_retained_anchored_advance(advance.current().view(), advance.batch())
836 }
837
838 pub fn qualify_retained_anchored_advance(
839 &self,
840 predecessor_view: &EconomicStateAnchorViewV1,
841 terminal_batch: &EconomicStateBatchV1,
842 ) -> Result<&EconomicEffectSlotV1, AdmissionOperationError> {
843 if predecessor_view != &self.predecessor_view || terminal_batch != &self.terminal_batch {
844 return Err(AdmissionOperationError::TerminalProjectionBindingMismatch);
845 }
846 Ok(&self.completed_effect_slot)
847 }
848}