Skip to main content

chio_kernel/admission_operation/projection/
channel_terminal.rs

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}