Skip to main content

chio_kernel/tool_outcome/
release.rs

1//! Closed monetary-release authority and immutable evidence bundles.
2
3use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
4
5use super::*;
6use crate::admission_operation::{
7    AdmissionAttachment, AdmissionAttachmentKind, AdmissionOperationState,
8};
9use chio_core_types::economic_continuity::{
10    EconomicAdmissionHandoffStateV1, EconomicEffectSlotV1, EconomicEffectStateV1,
11    EconomicEffectTerminalV1, EconomicNoEffectKindV1, EconomicResourceHeadV1, EconomicStateBatchV1,
12    VerifiedEconomicEffectCancellationAdvance,
13};
14use chio_core_types::provider_attempt::{
15    ProviderAttemptCheckpointV1, ProviderAttemptPhaseV1, ProviderCancellationBindingV1,
16};
17
18mod persistence;
19pub use persistence::*;
20mod terminal;
21pub use terminal::*;
22mod transport;
23
24#[allow(dead_code)]
25fn release_id(
26    field: &'static str,
27    value: impl Into<String>,
28) -> Result<AdmissionIdentifier, ToolOutcomeError> {
29    AdmissionIdentifier::try_new(field, value)
30        .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))
31}
32
33#[allow(dead_code)]
34fn imported_digest(
35    field: &'static str,
36    value: impl Into<String>,
37) -> Result<AdmissionDigest, ToolOutcomeError> {
38    AdmissionDigest::try_new(field, value)
39        .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum ReleaseEvidenceArtifactKindV1 {
45    ParticipantQuerySnapshot,
46    SignedTransportStatus,
47    EconomicEffectCancellation,
48    MonotonicAttemptCheckpoint,
49    TerminalToolOutcome,
50    TerminalPostReturnEvaluation,
51    VerifierPolicy,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct ImmutableReleaseArtifactV1 {
57    pub(super) kind: ReleaseEvidenceArtifactKindV1,
58    pub(super) evidence_id: AdmissionIdentifier,
59    pub(super) digest: AdmissionDigest,
60    pub(super) value: Value,
61}
62
63impl ImmutableReleaseArtifactV1 {
64    #[allow(dead_code)]
65    fn new(
66        kind: ReleaseEvidenceArtifactKindV1,
67        evidence_id: AdmissionIdentifier,
68        value: Value,
69    ) -> Result<Self, ToolOutcomeError> {
70        let bytes = bounded(
71            "release_artifact.value",
72            &value,
73            MAX_EVIDENCE_ARTIFACT_BYTES,
74        )?;
75        Ok(Self {
76            kind,
77            evidence_id,
78            digest: digest_bytes("release_artifact.digest", &bytes)?,
79            value,
80        })
81    }
82
83    fn validate(&self) -> Result<(), ToolOutcomeError> {
84        let digest = digest_bytes(
85            "release_artifact.digest",
86            &bounded(
87                "release_artifact.value",
88                &self.value,
89                MAX_EVIDENCE_ARTIFACT_BYTES,
90            )?,
91        )?;
92        if digest != self.digest {
93            return Err(ToolOutcomeError::Binding("release_artifact.digest"));
94        }
95        Ok(())
96    }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101struct VerifierPolicyArtifactV1 {
102    policy: Value,
103}
104
105fn parse_artifact_value<T>(artifact: &ImmutableReleaseArtifactV1) -> Result<T, ToolOutcomeError>
106where
107    T: serde::de::DeserializeOwned + Serialize,
108{
109    let parsed = serde_json::from_value::<T>(artifact.value.clone())
110        .map_err(|_| ToolOutcomeError::Invalid("release_artifact.value"))?;
111    let encoded = serde_json::to_value(&parsed)
112        .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
113    if encoded != artifact.value {
114        return Err(ToolOutcomeError::Invalid(
115            "release_artifact.noncanonical_value",
116        ));
117    }
118    Ok(parsed)
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123enum ReleaseParticipantV1 {
124    Broker,
125    Budget,
126    Approval,
127    Nonce,
128    OutcomeEligibility,
129    Payment,
130    CreditExposure,
131    Channel,
132    Transport,
133}
134
135impl ReleaseParticipantV1 {
136    const fn as_str(self) -> &'static str {
137        match self {
138            Self::Broker => "broker",
139            Self::Budget => "budget",
140            Self::Approval => "approval",
141            Self::Nonce => "nonce",
142            Self::OutcomeEligibility => "outcome-eligibility",
143            Self::Payment => "payment",
144            Self::CreditExposure => "credit-exposure",
145            Self::Channel => "channel",
146            Self::Transport => "transport",
147        }
148    }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153enum ParticipantNoEffectDispositionV1 {
154    NeverAcquired,
155    ReleasedBeforeDispatch,
156    NotDispatched,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(deny_unknown_fields)]
161struct ParticipantQueryRecordV1 {
162    source_record_id: AdmissionIdentifier,
163    source_record_digest: AdmissionDigest,
164    source_record_version: u64,
165    source_store_fence: StoreMutationFence,
166    source_commit_index: u64,
167    observed_at_unix_ms: u64,
168    source_attachments: Vec<AdmissionAttachment>,
169}
170
171impl ParticipantQueryRecordV1 {
172    fn from_qualified_operation(
173        operation: &AdmissionOperationV1,
174        participant: ReleaseParticipantV1,
175        context: &AdmissionProjectionContext,
176    ) -> Result<Self, ToolOutcomeError> {
177        #[derive(Serialize)]
178        struct SourceDigest<'a> {
179            operation: &'a AdmissionOperationV1,
180            participant: ReleaseParticipantV1,
181            store_fence: &'a StoreMutationFence,
182        }
183
184        Ok(Self {
185            source_record_id: release_id(
186                "participant_query_source_record_id",
187                format!(
188                    "{}:{}:{}",
189                    operation.binding().operation_id().as_str(),
190                    participant.as_str(),
191                    operation.version()
192                ),
193            )?,
194            source_record_digest: domain_digest(
195                "chio.qualified-predispatch-participant-query.v1",
196                &SourceDigest {
197                    operation,
198                    participant,
199                    store_fence: &context.store_fence,
200                },
201            )?,
202            source_record_version: operation.version(),
203            source_store_fence: context.store_fence.clone(),
204            source_commit_index: operation.version(),
205            observed_at_unix_ms: context.trusted_time_unix_ms,
206            source_attachments: Vec::new(),
207        })
208    }
209
210    fn from_qualified_release(
211        operation: &AdmissionOperationV1,
212        participant: ReleaseParticipantV1,
213        context: &AdmissionProjectionContext,
214    ) -> Result<Self, ToolOutcomeError> {
215        let mut record = Self::from_qualified_operation(operation, participant, context)?;
216        record.source_attachments = participant_attachments(operation, participant);
217        Ok(record)
218    }
219
220    #[cfg(test)]
221    fn for_test(
222        source_record_id: AdmissionIdentifier,
223        source_record_digest: AdmissionDigest,
224        source_attachments: Vec<AdmissionAttachment>,
225        observed_at_unix_ms: u64,
226    ) -> Self {
227        Self {
228            source_record_id,
229            source_record_digest,
230            source_record_version: 1,
231            source_store_fence: StoreMutationFence {
232                store_uuid: "store-1".to_owned(),
233                lease_id: "store-lease-1".to_owned(),
234                owner_epoch: 9,
235            },
236            source_commit_index: 1,
237            observed_at_unix_ms,
238            source_attachments,
239        }
240    }
241
242    fn validate(
243        &self,
244        verification_store_fence: &StoreMutationFence,
245        verified_at_unix_ms: u64,
246    ) -> Result<(), ToolOutcomeError> {
247        positive(
248            "participant_query.source_record_version",
249            self.source_record_version,
250        )?;
251        positive(
252            "participant_query.source_commit_index",
253            self.source_commit_index,
254        )?;
255        positive("participant_query.observed_at", self.observed_at_unix_ms)?;
256        validate_successor_fence(&self.source_store_fence, verification_store_fence)?;
257        if self.observed_at_unix_ms > verified_at_unix_ms {
258            return Err(ToolOutcomeError::Binding("participant_query.trusted_time"));
259        }
260        Ok(())
261    }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(deny_unknown_fields)]
266struct VerifiedParticipantNoEffectEvidenceV1 {
267    participant: ReleaseParticipantV1,
268    disposition: ParticipantNoEffectDispositionV1,
269    operation_id: AdmissionOperationId,
270    operation_version: u64,
271    observed_state: AdmissionOperationState,
272    source_record: ParticipantQueryRecordV1,
273    pub(super) evidence_digest: AdmissionDigest,
274}
275
276#[derive(Serialize)]
277struct ParticipantNoEffectEvidenceBody<'a> {
278    participant: ReleaseParticipantV1,
279    disposition: ParticipantNoEffectDispositionV1,
280    operation_id: &'a AdmissionOperationId,
281    operation_version: u64,
282    observed_state: AdmissionOperationState,
283    source_record: &'a ParticipantQueryRecordV1,
284}
285
286impl VerifiedParticipantNoEffectEvidenceV1 {
287    fn body(&self) -> ParticipantNoEffectEvidenceBody<'_> {
288        ParticipantNoEffectEvidenceBody {
289            participant: self.participant,
290            disposition: self.disposition,
291            operation_id: &self.operation_id,
292            operation_version: self.operation_version,
293            observed_state: self.observed_state,
294            source_record: &self.source_record,
295        }
296    }
297
298    fn from_verified_source(
299        operation: &AdmissionOperationV1,
300        participant: ReleaseParticipantV1,
301        disposition: ParticipantNoEffectDispositionV1,
302        source_record: ParticipantQueryRecordV1,
303    ) -> Result<Self, ToolOutcomeError> {
304        let operation_id = operation.binding().operation_id().clone();
305        let operation_version = operation.version();
306        let observed_state = operation.state();
307        let evidence_digest = domain_digest(
308            "chio.participant-no-effect-evidence.v1",
309            &ParticipantNoEffectEvidenceBody {
310                participant,
311                disposition,
312                operation_id: &operation_id,
313                operation_version,
314                observed_state,
315                source_record: &source_record,
316            },
317        )?;
318        Ok(Self {
319            participant,
320            disposition,
321            operation_id,
322            operation_version,
323            observed_state,
324            source_record,
325            evidence_digest,
326        })
327    }
328
329    fn validate_for(
330        &self,
331        operation: &AdmissionOperationV1,
332        participant: ReleaseParticipantV1,
333        disposition: ParticipantNoEffectDispositionV1,
334        verification_store_fence: &StoreMutationFence,
335        verified_at_unix_ms: u64,
336    ) -> Result<(), ToolOutcomeError> {
337        self.source_record
338            .validate(verification_store_fence, verified_at_unix_ms)?;
339        let expected_attachments = participant_attachments(operation, participant);
340        let attachments_match = match disposition {
341            ParticipantNoEffectDispositionV1::NeverAcquired
342            | ParticipantNoEffectDispositionV1::NotDispatched => {
343                self.source_record.source_attachments.is_empty()
344            }
345            ParticipantNoEffectDispositionV1::ReleasedBeforeDispatch => {
346                !expected_attachments.is_empty()
347                    && self.source_record.source_attachments == expected_attachments
348            }
349        };
350        if self.participant != participant
351            || self.disposition != disposition
352            || self.operation_id != *operation.binding().operation_id()
353            || self.operation_version != operation.version()
354            || self.observed_state != operation.state()
355            || !attachments_match
356            || self.evidence_digest
357                != domain_digest("chio.participant-no-effect-evidence.v1", &self.body())?
358        {
359            return Err(ToolOutcomeError::Binding(
360                "predispatch.participant_evidence",
361            ));
362        }
363        Ok(())
364    }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368#[serde(tag = "disposition", rename_all = "snake_case")]
369enum VerifiedParticipantNoEffectV1 {
370    NotRequired,
371    NeverAcquired {
372        evidence: VerifiedParticipantNoEffectEvidenceV1,
373    },
374    ReleasedBeforeDispatch {
375        evidence: VerifiedParticipantNoEffectEvidenceV1,
376    },
377    NotDispatched {
378        evidence: VerifiedParticipantNoEffectEvidenceV1,
379    },
380}
381
382impl VerifiedParticipantNoEffectV1 {
383    #[cfg(test)]
384    fn never_acquired(
385        operation: &AdmissionOperationV1,
386        participant: ReleaseParticipantV1,
387        source_record: ParticipantQueryRecordV1,
388    ) -> Result<Self, ToolOutcomeError> {
389        Ok(Self::NeverAcquired {
390            evidence: VerifiedParticipantNoEffectEvidenceV1::from_verified_source(
391                operation,
392                participant,
393                ParticipantNoEffectDispositionV1::NeverAcquired,
394                source_record,
395            )?,
396        })
397    }
398
399    fn released_before_dispatch(
400        operation: &AdmissionOperationV1,
401        participant: ReleaseParticipantV1,
402        source_record: ParticipantQueryRecordV1,
403    ) -> Result<Self, ToolOutcomeError> {
404        Ok(Self::ReleasedBeforeDispatch {
405            evidence: VerifiedParticipantNoEffectEvidenceV1::from_verified_source(
406                operation,
407                participant,
408                ParticipantNoEffectDispositionV1::ReleasedBeforeDispatch,
409                source_record,
410            )?,
411        })
412    }
413
414    #[cfg(test)]
415    fn not_dispatched(
416        operation: &AdmissionOperationV1,
417        source_record: ParticipantQueryRecordV1,
418    ) -> Result<Self, ToolOutcomeError> {
419        Ok(Self::NotDispatched {
420            evidence: VerifiedParticipantNoEffectEvidenceV1::from_verified_source(
421                operation,
422                ReleaseParticipantV1::Transport,
423                ParticipantNoEffectDispositionV1::NotDispatched,
424                source_record,
425            )?,
426        })
427    }
428
429    fn validate_for(
430        &self,
431        operation: &AdmissionOperationV1,
432        participant: ReleaseParticipantV1,
433        required: bool,
434        verification_store_fence: &StoreMutationFence,
435        verified_at_unix_ms: u64,
436    ) -> Result<(), ToolOutcomeError> {
437        let attached = !participant_attachments(operation, participant).is_empty();
438        match self {
439            Self::NotRequired if !required => Ok(()),
440            Self::NeverAcquired { evidence } if required && !attached => evidence.validate_for(
441                operation,
442                participant,
443                ParticipantNoEffectDispositionV1::NeverAcquired,
444                verification_store_fence,
445                verified_at_unix_ms,
446            ),
447            Self::ReleasedBeforeDispatch { evidence }
448                if required && attached && participant != ReleaseParticipantV1::Channel =>
449            {
450                evidence.validate_for(
451                    operation,
452                    participant,
453                    ParticipantNoEffectDispositionV1::ReleasedBeforeDispatch,
454                    verification_store_fence,
455                    verified_at_unix_ms,
456                )
457            }
458            Self::NotDispatched { evidence }
459                if participant == ReleaseParticipantV1::Transport
460                    && required
461                    && operation.dispatch_commit().is_none() =>
462            {
463                evidence.validate_for(
464                    operation,
465                    participant,
466                    ParticipantNoEffectDispositionV1::NotDispatched,
467                    verification_store_fence,
468                    verified_at_unix_ms,
469                )
470            }
471            _ => Err(ToolOutcomeError::Binding(
472                "predispatch.participant_disposition",
473            )),
474        }
475    }
476}
477
478fn participant_attachments(
479    operation: &AdmissionOperationV1,
480    participant: ReleaseParticipantV1,
481) -> Vec<AdmissionAttachment> {
482    let kinds: &[AdmissionAttachmentKind] = match participant {
483        ReleaseParticipantV1::Broker => &[AdmissionAttachmentKind::BrokerAttempt],
484        ReleaseParticipantV1::Budget => &[AdmissionAttachmentKind::BudgetHold],
485        ReleaseParticipantV1::Approval => &[
486            AdmissionAttachmentKind::ThresholdProposal,
487            AdmissionAttachmentKind::ApprovalSet,
488        ],
489        ReleaseParticipantV1::Nonce => &[AdmissionAttachmentKind::ExecutionNonce],
490        ReleaseParticipantV1::OutcomeEligibility => &[AdmissionAttachmentKind::OutcomeEligibility],
491        ReleaseParticipantV1::Payment => &[AdmissionAttachmentKind::PaymentParticipant],
492        ReleaseParticipantV1::CreditExposure => {
493            &[AdmissionAttachmentKind::CreditExposureReservation]
494        }
495        ReleaseParticipantV1::Channel => &[AdmissionAttachmentKind::ChannelReservation],
496        ReleaseParticipantV1::Transport => &[],
497    };
498    kinds
499        .iter()
500        .filter_map(|kind| operation.attachment(*kind).cloned())
501        .collect()
502}
503
504#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
505#[serde(deny_unknown_fields)]
506struct PreDispatchParticipantDispositionsV1 {
507    broker: VerifiedParticipantNoEffectV1,
508    budget: VerifiedParticipantNoEffectV1,
509    approval: VerifiedParticipantNoEffectV1,
510    nonce: VerifiedParticipantNoEffectV1,
511    outcome_eligibility: VerifiedParticipantNoEffectV1,
512    payment: VerifiedParticipantNoEffectV1,
513    credit_exposure: VerifiedParticipantNoEffectV1,
514    channel: VerifiedParticipantNoEffectV1,
515    transport: VerifiedParticipantNoEffectV1,
516}
517
518impl PreDispatchParticipantDispositionsV1 {
519    #[cfg(test)]
520    #[allow(clippy::too_many_arguments)]
521    fn from_verified_parts(
522        broker: VerifiedParticipantNoEffectV1,
523        budget: VerifiedParticipantNoEffectV1,
524        approval: VerifiedParticipantNoEffectV1,
525        nonce: VerifiedParticipantNoEffectV1,
526        outcome_eligibility: VerifiedParticipantNoEffectV1,
527        payment: VerifiedParticipantNoEffectV1,
528        credit_exposure: VerifiedParticipantNoEffectV1,
529        channel: VerifiedParticipantNoEffectV1,
530        transport: VerifiedParticipantNoEffectV1,
531    ) -> Self {
532        Self {
533            broker,
534            budget,
535            approval,
536            nonce,
537            outcome_eligibility,
538            payment,
539            credit_exposure,
540            channel,
541            transport,
542        }
543    }
544
545    fn validate_for(
546        &self,
547        operation: &AdmissionOperationV1,
548        verification_store_fence: &StoreMutationFence,
549        verified_at_unix_ms: u64,
550    ) -> Result<(), ToolOutcomeError> {
551        let required = operation.binding().participant_requirements();
552        self.broker.validate_for(
553            operation,
554            ReleaseParticipantV1::Broker,
555            required.broker_attempt,
556            verification_store_fence,
557            verified_at_unix_ms,
558        )?;
559        self.budget.validate_for(
560            operation,
561            ReleaseParticipantV1::Budget,
562            required.budget_capture,
563            verification_store_fence,
564            verified_at_unix_ms,
565        )?;
566        self.approval.validate_for(
567            operation,
568            ReleaseParticipantV1::Approval,
569            required.approval,
570            verification_store_fence,
571            verified_at_unix_ms,
572        )?;
573        self.nonce.validate_for(
574            operation,
575            ReleaseParticipantV1::Nonce,
576            required.execution_nonce,
577            verification_store_fence,
578            verified_at_unix_ms,
579        )?;
580        self.outcome_eligibility.validate_for(
581            operation,
582            ReleaseParticipantV1::OutcomeEligibility,
583            required.outcome_eligibility,
584            verification_store_fence,
585            verified_at_unix_ms,
586        )?;
587        self.payment.validate_for(
588            operation,
589            ReleaseParticipantV1::Payment,
590            required.payment,
591            verification_store_fence,
592            verified_at_unix_ms,
593        )?;
594        self.credit_exposure.validate_for(
595            operation,
596            ReleaseParticipantV1::CreditExposure,
597            required.credit_exposure,
598            verification_store_fence,
599            verified_at_unix_ms,
600        )?;
601        self.channel.validate_for(
602            operation,
603            ReleaseParticipantV1::Channel,
604            required.channel,
605            verification_store_fence,
606            verified_at_unix_ms,
607        )?;
608        self.transport.validate_for(
609            operation,
610            ReleaseParticipantV1::Transport,
611            true,
612            verification_store_fence,
613            verified_at_unix_ms,
614        )
615    }
616}
617
618fn required_release_participants(operation: &AdmissionOperationV1) -> Vec<ReleaseParticipantV1> {
619    let required = operation.binding().participant_requirements();
620    [
621        (required.broker_attempt, ReleaseParticipantV1::Broker),
622        (required.budget_capture, ReleaseParticipantV1::Budget),
623        (required.approval, ReleaseParticipantV1::Approval),
624        (required.execution_nonce, ReleaseParticipantV1::Nonce),
625        (
626            required.outcome_eligibility,
627            ReleaseParticipantV1::OutcomeEligibility,
628        ),
629        (required.payment, ReleaseParticipantV1::Payment),
630        (
631            required.credit_exposure,
632            ReleaseParticipantV1::CreditExposure,
633        ),
634        (required.channel, ReleaseParticipantV1::Channel),
635        (true, ReleaseParticipantV1::Transport),
636    ]
637    .into_iter()
638    .filter_map(|(required, participant)| required.then_some(participant))
639    .collect()
640}
641
642#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
643#[serde(deny_unknown_fields)]
644struct PreDispatchParticipantManifestV1 {
645    request_binding_hash: AdmissionDigest,
646    coordinator_lease_id: AdmissionIdentifier,
647    coordinator_lease_epoch: u64,
648    verification_store_fence: StoreMutationFence,
649    verified_at_unix_ms: u64,
650    required_participants: Vec<ReleaseParticipantV1>,
651    participant_dispositions: PreDispatchParticipantDispositionsV1,
652}
653
654impl PreDispatchParticipantManifestV1 {
655    fn validate_for(&self, operation: &AdmissionOperationV1) -> Result<(), ToolOutcomeError> {
656        positive("predispatch.verified_at", self.verified_at_unix_ms)?;
657        positive(
658            "predispatch.coordinator_lease_epoch",
659            self.coordinator_lease_epoch,
660        )?;
661        validate_store_fence(&self.verification_store_fence)?;
662        if self.request_binding_hash != *operation.binding().request_binding_hash()
663            || self.required_participants != required_release_participants(operation)
664        {
665            return Err(ToolOutcomeError::Binding(
666                "predispatch.participant_manifest",
667            ));
668        }
669        self.participant_dispositions.validate_for(
670            operation,
671            &self.verification_store_fence,
672            self.verified_at_unix_ms,
673        )
674    }
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678#[serde(deny_unknown_fields)]
679struct PreDispatchNoEffectSnapshotV1 {
680    operation_id: AdmissionOperationId,
681    operation_version: u64,
682    request_binding_hash: AdmissionDigest,
683    coordinator_lease_id: AdmissionIdentifier,
684    coordinator_lease_epoch: u64,
685    verification_store_fence: StoreMutationFence,
686    verified_at_unix_ms: u64,
687    participant_manifest: PreDispatchParticipantManifestV1,
688    complete_participant_query_root: AdmissionDigest,
689    verifier_policy_digest: AdmissionDigest,
690    artifacts: Vec<ImmutableReleaseArtifactV1>,
691}
692
693#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
694pub struct VerifiedPreDispatchNoEffect {
695    snapshot: Box<PreDispatchNoEffectSnapshotV1>,
696}
697
698#[derive(Deserialize)]
699#[serde(remote = "VerifiedPreDispatchNoEffect", deny_unknown_fields)]
700struct PreDispatchNoEffectWireV1 {
701    snapshot: Box<PreDispatchNoEffectSnapshotV1>,
702}
703
704#[derive(Deserialize)]
705struct UntrustedPreDispatchNoEffectWireV1(
706    #[serde(with = "PreDispatchNoEffectWireV1")] VerifiedPreDispatchNoEffect,
707);
708
709impl VerifiedPreDispatchNoEffect {
710    #[must_use]
711    pub fn operation_id(&self) -> &AdmissionOperationId {
712        &self.snapshot.operation_id
713    }
714
715    pub(crate) fn from_canonical_record_verified(
716        bytes: &[u8],
717        operation: &AdmissionOperationV1,
718        context: &AdmissionProjectionContext,
719    ) -> Result<Self, ToolOutcomeError> {
720        let UntrustedPreDispatchNoEffectWireV1(proof) = serde_json::from_slice(bytes)
721            .map_err(|_| ToolOutcomeError::Invalid("predispatch.release_proof"))?;
722        let canonical = canonical_json_bytes(&proof)
723            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
724        if canonical != bytes {
725            return Err(ToolOutcomeError::Binding(
726                "predispatch.release_proof_canonical",
727            ));
728        }
729        proof.validate_against(operation, context)?;
730        Ok(proof)
731    }
732
733    pub(crate) fn from_qualified_operation_snapshot(
734        operation: &AdmissionOperationV1,
735        context: &AdmissionProjectionContext,
736    ) -> Result<Self, ToolOutcomeError> {
737        validate_pre_dispatch_context(operation, context)?;
738        let requirements = operation.binding().participant_requirements();
739        let participant = |kind: ReleaseParticipantV1, required: bool| {
740            if !participant_attachments(operation, kind).is_empty() {
741                return Err(ToolOutcomeError::Binding(
742                    if kind == ReleaseParticipantV1::Channel {
743                        "predispatch.channel_cancellation_required"
744                    } else {
745                        "predispatch.acquired_participant"
746                    },
747                ));
748            }
749            if required {
750                Ok(VerifiedParticipantNoEffectV1::NeverAcquired {
751                    evidence: VerifiedParticipantNoEffectEvidenceV1::from_verified_source(
752                        operation,
753                        kind,
754                        ParticipantNoEffectDispositionV1::NeverAcquired,
755                        ParticipantQueryRecordV1::from_qualified_operation(
756                            operation, kind, context,
757                        )?,
758                    )?,
759                })
760            } else {
761                Ok(VerifiedParticipantNoEffectV1::NotRequired)
762            }
763        };
764        let channel = participant(ReleaseParticipantV1::Channel, requirements.channel)?;
765        let participant_dispositions = PreDispatchParticipantDispositionsV1 {
766            broker: participant(ReleaseParticipantV1::Broker, requirements.broker_attempt)?,
767            budget: participant(ReleaseParticipantV1::Budget, requirements.budget_capture)?,
768            approval: participant(ReleaseParticipantV1::Approval, requirements.approval)?,
769            nonce: participant(ReleaseParticipantV1::Nonce, requirements.execution_nonce)?,
770            outcome_eligibility: participant(
771                ReleaseParticipantV1::OutcomeEligibility,
772                requirements.outcome_eligibility,
773            )?,
774            payment: participant(ReleaseParticipantV1::Payment, requirements.payment)?,
775            credit_exposure: participant(
776                ReleaseParticipantV1::CreditExposure,
777                requirements.credit_exposure,
778            )?,
779            channel,
780            transport: VerifiedParticipantNoEffectV1::NotDispatched {
781                evidence: VerifiedParticipantNoEffectEvidenceV1::from_verified_source(
782                    operation,
783                    ReleaseParticipantV1::Transport,
784                    ParticipantNoEffectDispositionV1::NotDispatched,
785                    ParticipantQueryRecordV1::from_qualified_operation(
786                        operation,
787                        ReleaseParticipantV1::Transport,
788                        context,
789                    )?,
790                )?,
791            },
792        };
793        Self::from_verified_parts(
794            operation,
795            context,
796            participant_dispositions,
797            serde_json::json!({
798                "authority": "qualified_admission_projection_store",
799                "version": 1
800            }),
801        )
802    }
803
804    pub(crate) fn from_qualified_released_operation_snapshot(
805        operation: &AdmissionOperationV1,
806        context: &AdmissionProjectionContext,
807        verifier_policy: Value,
808    ) -> Result<Self, ToolOutcomeError> {
809        validate_pre_dispatch_context(operation, context)?;
810        let requirements = operation.binding().participant_requirements();
811        let participant = |kind: ReleaseParticipantV1, required: bool| {
812            let attachments = participant_attachments(operation, kind);
813            if !required {
814                return if attachments.is_empty() {
815                    Ok(VerifiedParticipantNoEffectV1::NotRequired)
816                } else {
817                    Err(ToolOutcomeError::Binding(
818                        "predispatch.unexpected_participant",
819                    ))
820                };
821            }
822            if attachments.is_empty() {
823                return Ok(VerifiedParticipantNoEffectV1::NeverAcquired {
824                    evidence: VerifiedParticipantNoEffectEvidenceV1::from_verified_source(
825                        operation,
826                        kind,
827                        ParticipantNoEffectDispositionV1::NeverAcquired,
828                        ParticipantQueryRecordV1::from_qualified_operation(
829                            operation, kind, context,
830                        )?,
831                    )?,
832                });
833            }
834            if kind == ReleaseParticipantV1::Channel {
835                return Err(ToolOutcomeError::Binding(
836                    "predispatch.channel_cancellation_required",
837                ));
838            }
839            VerifiedParticipantNoEffectV1::released_before_dispatch(
840                operation,
841                kind,
842                ParticipantQueryRecordV1::from_qualified_release(operation, kind, context)?,
843            )
844        };
845        let participant_dispositions = PreDispatchParticipantDispositionsV1 {
846            broker: participant(ReleaseParticipantV1::Broker, requirements.broker_attempt)?,
847            budget: participant(ReleaseParticipantV1::Budget, requirements.budget_capture)?,
848            approval: participant(ReleaseParticipantV1::Approval, requirements.approval)?,
849            nonce: participant(ReleaseParticipantV1::Nonce, requirements.execution_nonce)?,
850            outcome_eligibility: participant(
851                ReleaseParticipantV1::OutcomeEligibility,
852                requirements.outcome_eligibility,
853            )?,
854            payment: participant(ReleaseParticipantV1::Payment, requirements.payment)?,
855            credit_exposure: participant(
856                ReleaseParticipantV1::CreditExposure,
857                requirements.credit_exposure,
858            )?,
859            channel: participant(ReleaseParticipantV1::Channel, requirements.channel)?,
860            transport: VerifiedParticipantNoEffectV1::NotDispatched {
861                evidence: VerifiedParticipantNoEffectEvidenceV1::from_verified_source(
862                    operation,
863                    ReleaseParticipantV1::Transport,
864                    ParticipantNoEffectDispositionV1::NotDispatched,
865                    ParticipantQueryRecordV1::from_qualified_operation(
866                        operation,
867                        ReleaseParticipantV1::Transport,
868                        context,
869                    )?,
870                )?,
871            },
872        };
873        Self::from_verified_parts(
874            operation,
875            context,
876            participant_dispositions,
877            verifier_policy,
878        )
879    }
880
881    #[cfg(test)]
882    fn from_verified_snapshot(
883        operation: &AdmissionOperationV1,
884        context: &AdmissionProjectionContext,
885        participant_dispositions: PreDispatchParticipantDispositionsV1,
886        verifier_policy: Value,
887    ) -> Result<Self, ToolOutcomeError> {
888        Self::from_verified_parts(
889            operation,
890            context,
891            participant_dispositions,
892            verifier_policy,
893        )
894    }
895
896    fn from_verified_parts(
897        operation: &AdmissionOperationV1,
898        context: &AdmissionProjectionContext,
899        participant_dispositions: PreDispatchParticipantDispositionsV1,
900        verifier_policy: Value,
901    ) -> Result<Self, ToolOutcomeError> {
902        validate_pre_dispatch_context(operation, context)?;
903        let operation_id = operation.binding().operation_id().clone();
904        let participant_manifest = PreDispatchParticipantManifestV1 {
905            request_binding_hash: operation.binding().request_binding_hash().clone(),
906            coordinator_lease_id: context.coordinator_lease_id.clone(),
907            coordinator_lease_epoch: context.coordinator_lease_epoch,
908            verification_store_fence: context.store_fence.clone(),
909            verified_at_unix_ms: context.trusted_time_unix_ms,
910            required_participants: required_release_participants(operation),
911            participant_dispositions,
912        };
913        participant_manifest.validate_for(operation)?;
914        let query = ImmutableReleaseArtifactV1::new(
915            ReleaseEvidenceArtifactKindV1::ParticipantQuerySnapshot,
916            release_id(
917                "participant_query_evidence_id",
918                format!("{}:participant-query", operation_id.as_str()),
919            )?,
920            serde_json::to_value(&participant_manifest)
921                .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
922        )?;
923        let policy = ImmutableReleaseArtifactV1::new(
924            ReleaseEvidenceArtifactKindV1::VerifierPolicy,
925            release_id(
926                "verifier_policy_evidence_id",
927                format!("{}:verifier-policy", operation_id.as_str()),
928            )?,
929            serde_json::to_value(VerifierPolicyArtifactV1 {
930                policy: verifier_policy,
931            })
932            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
933        )?;
934        let snapshot = PreDispatchNoEffectSnapshotV1 {
935            operation_id,
936            operation_version: operation.version(),
937            request_binding_hash: operation.binding().request_binding_hash().clone(),
938            coordinator_lease_id: context.coordinator_lease_id.clone(),
939            coordinator_lease_epoch: context.coordinator_lease_epoch,
940            verification_store_fence: context.store_fence.clone(),
941            verified_at_unix_ms: context.trusted_time_unix_ms,
942            participant_manifest,
943            complete_participant_query_root: query.digest.clone(),
944            verifier_policy_digest: policy.digest.clone(),
945            artifacts: vec![query, policy],
946        };
947        let proof = Self {
948            snapshot: Box::new(snapshot),
949        };
950        proof.validate_against(operation, context)?;
951        Ok(proof)
952    }
953
954    pub(crate) fn validate_against(
955        &self,
956        operation: &AdmissionOperationV1,
957        context: &AdmissionProjectionContext,
958    ) -> Result<(), ToolOutcomeError> {
959        validate_pre_dispatch_context(operation, context)?;
960        self.snapshot.participant_manifest.validate_for(operation)?;
961        validate_successor_fence(
962            &self.snapshot.verification_store_fence,
963            &context.store_fence,
964        )?;
965        if self.snapshot.operation_id != *operation.binding().operation_id()
966            || self.snapshot.operation_version != operation.version()
967            || self.snapshot.operation_version != context.expected_operation_version
968            || self.snapshot.request_binding_hash != *operation.binding().request_binding_hash()
969            || self.snapshot.coordinator_lease_epoch != context.coordinator_lease_epoch
970            || self.snapshot.verified_at_unix_ms > context.trusted_time_unix_ms
971            || (self.snapshot.verification_store_fence == context.store_fence
972                && self.snapshot.coordinator_lease_id != context.coordinator_lease_id)
973            || self.snapshot.participant_manifest.request_binding_hash
974                != self.snapshot.request_binding_hash
975            || self.snapshot.participant_manifest.coordinator_lease_id
976                != self.snapshot.coordinator_lease_id
977            || self.snapshot.participant_manifest.coordinator_lease_epoch
978                != self.snapshot.coordinator_lease_epoch
979            || self.snapshot.participant_manifest.verification_store_fence
980                != self.snapshot.verification_store_fence
981            || self.snapshot.participant_manifest.verified_at_unix_ms
982                != self.snapshot.verified_at_unix_ms
983            || self.snapshot.artifacts.len() != 2
984            || self.snapshot.artifacts[0].kind
985                != ReleaseEvidenceArtifactKindV1::ParticipantQuerySnapshot
986            || self.snapshot.artifacts[1].kind != ReleaseEvidenceArtifactKindV1::VerifierPolicy
987            || self.snapshot.complete_participant_query_root != self.snapshot.artifacts[0].digest
988            || self.snapshot.verifier_policy_digest != self.snapshot.artifacts[1].digest
989        {
990            return Err(ToolOutcomeError::Binding("predispatch.projection_context"));
991        }
992        let artifact_manifest: PreDispatchParticipantManifestV1 =
993            parse_artifact_value(&self.snapshot.artifacts[0])?;
994        let _: VerifierPolicyArtifactV1 = parse_artifact_value(&self.snapshot.artifacts[1])?;
995        if self.snapshot.participant_manifest != artifact_manifest {
996            return Err(ToolOutcomeError::Binding(
997                "predispatch.participant_artifact",
998            ));
999        }
1000        self.snapshot
1001            .artifacts
1002            .iter()
1003            .try_for_each(ImmutableReleaseArtifactV1::validate)
1004    }
1005}
1006
1007mod predispatch_authority_sealed {
1008    pub trait Sealed {}
1009}
1010
1011#[allow(dead_code)]
1012pub(crate) trait QualifiedPreDispatchNoEffectAuthority:
1013    predispatch_authority_sealed::Sealed + Send + Sync
1014{
1015    fn verify_no_effect(
1016        &self,
1017        operation: &AdmissionOperationV1,
1018        context: &AdmissionProjectionContext,
1019    ) -> Result<VerifiedPreDispatchNoEffect, ToolOutcomeError>;
1020}
1021
1022fn validate_pre_dispatch_context(
1023    operation: &AdmissionOperationV1,
1024    context: &AdmissionProjectionContext,
1025) -> Result<(), ToolOutcomeError> {
1026    context
1027        .validate()
1028        .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1029    operation
1030        .validate()
1031        .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1032    if operation.dispatch_commit().is_some()
1033        || !matches!(
1034            operation.state(),
1035            crate::admission_operation::AdmissionOperationState::Prepared
1036                | crate::admission_operation::AdmissionOperationState::BrokerAttemptRegistered
1037                | crate::admission_operation::AdmissionOperationState::BudgetAuthorized
1038                | crate::admission_operation::AdmissionOperationState::ApprovalReserved
1039                | crate::admission_operation::AdmissionOperationState::ReadyToDispatch
1040                | crate::admission_operation::AdmissionOperationState::CapturePending
1041        )
1042        || context.operation_id != *operation.binding().operation_id()
1043        || context.request_id != operation.replay_key().request_id
1044        || context.expected_operation_version != operation.version()
1045        || context.coordinator_lease_epoch != operation.coordinator_lease_epoch()
1046    {
1047        return Err(ToolOutcomeError::Binding("predispatch.operation_context"));
1048    }
1049    Ok(())
1050}
1051
1052#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1053#[serde(deny_unknown_fields)]
1054struct SignedTransportStatusArtifactV1 {
1055    cancellation: ProviderCancellationBindingV1,
1056    no_acceptance_proof_base64: String,
1057    qualification_digest: AdmissionDigest,
1058    observed_at_unix_ms: u64,
1059    verifier_identity: AdmissionIdentifier,
1060}
1061
1062#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1063#[serde(deny_unknown_fields)]
1064struct EconomicEffectCancellationArtifactV1 {
1065    slot: EconomicEffectSlotV1,
1066    expected_head_version: u64,
1067    expected_head_digest: AdmissionDigest,
1068    expected_lifecycle_fence: u64,
1069    resulting_head_version: u64,
1070    resulting_head_digest: AdmissionDigest,
1071    resulting_lifecycle_fence: u64,
1072    checkpoint_sequence: u64,
1073    checkpoint_digest: AdmissionDigest,
1074}
1075
1076#[derive(Debug, Clone, Serialize)]
1077pub struct VerifiedTransportNotAccepted {
1078    operation_id: AdmissionOperationId,
1079    operation_version: u64,
1080    request_id: AdmissionIdentifier,
1081    request_binding_hash: AdmissionDigest,
1082    dispatch_operation_version: u64,
1083    dispatch_fence: u64,
1084    projection_coordinator_lease_id: AdmissionIdentifier,
1085    projection_coordinator_lease_epoch: u64,
1086    projection_store_fence: StoreMutationFence,
1087    transport_attempt_id: AdmissionIdentifier,
1088    transport_identity: AdmissionIdentifier,
1089    transport_key_epoch: u64,
1090    signed_status_digest: AdmissionDigest,
1091    qualification_digest: AdmissionDigest,
1092    cancellation_fence: u64,
1093    verified_at_unix_ms: u64,
1094    verifier_identity: AdmissionIdentifier,
1095    monotonic_checkpoint_digest: AdmissionDigest,
1096    verifier_policy_digest: AdmissionDigest,
1097    artifacts: Vec<ImmutableReleaseArtifactV1>,
1098}
1099
1100#[derive(Deserialize)]
1101#[serde(remote = "VerifiedTransportNotAccepted", deny_unknown_fields)]
1102struct TransportNotAcceptedWireV1 {
1103    operation_id: AdmissionOperationId,
1104    operation_version: u64,
1105    request_id: AdmissionIdentifier,
1106    request_binding_hash: AdmissionDigest,
1107    dispatch_operation_version: u64,
1108    dispatch_fence: u64,
1109    projection_coordinator_lease_id: AdmissionIdentifier,
1110    projection_coordinator_lease_epoch: u64,
1111    projection_store_fence: StoreMutationFence,
1112    transport_attempt_id: AdmissionIdentifier,
1113    transport_identity: AdmissionIdentifier,
1114    transport_key_epoch: u64,
1115    signed_status_digest: AdmissionDigest,
1116    qualification_digest: AdmissionDigest,
1117    cancellation_fence: u64,
1118    verified_at_unix_ms: u64,
1119    verifier_identity: AdmissionIdentifier,
1120    monotonic_checkpoint_digest: AdmissionDigest,
1121    verifier_policy_digest: AdmissionDigest,
1122    artifacts: Vec<ImmutableReleaseArtifactV1>,
1123}
1124
1125#[derive(Deserialize)]
1126struct UntrustedTransportNotAcceptedWireV1(
1127    #[serde(with = "TransportNotAcceptedWireV1")] VerifiedTransportNotAccepted,
1128);
1129
1130impl VerifiedTransportNotAccepted {
1131    pub(crate) fn uses_economic_effect_cancellation(&self) -> bool {
1132        matches!(
1133            self.artifacts.first().map(|artifact| artifact.kind),
1134            Some(ReleaseEvidenceArtifactKindV1::EconomicEffectCancellation)
1135        )
1136    }
1137
1138    pub(crate) fn from_canonical_record_verified(
1139        bytes: &[u8],
1140        operation: &AdmissionOperationV1,
1141        context: &AdmissionProjectionContext,
1142    ) -> Result<Self, ToolOutcomeError> {
1143        let UntrustedTransportNotAcceptedWireV1(proof) = serde_json::from_slice(bytes)
1144            .map_err(|_| ToolOutcomeError::Invalid("transport_not_accepted.release_proof"))?;
1145        let canonical = canonical_json_bytes(&proof)
1146            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1147        if canonical != bytes {
1148            return Err(ToolOutcomeError::Binding(
1149                "transport_not_accepted.release_proof_canonical",
1150            ));
1151        }
1152        proof.validate_against(operation, context)?;
1153        Ok(proof)
1154    }
1155
1156    pub(crate) fn from_verified_economic_effect(
1157        cancellation: &VerifiedEconomicEffectCancellationAdvance,
1158        operation: &AdmissionOperationV1,
1159        context: &AdmissionProjectionContext,
1160    ) -> Result<Self, ToolOutcomeError> {
1161        validate_projection_context(operation, context)?;
1162        if cancellation.kind() != EconomicNoEffectKindV1::VerifiedTransportNotAccepted
1163            || operation.state() != AdmissionOperationState::DispatchCommitted
1164        {
1165            return Err(ToolOutcomeError::Binding(
1166                "transport_not_accepted.economic_kind",
1167            ));
1168        }
1169        let commit = operation
1170            .dispatch_commit()
1171            .ok_or(ToolOutcomeError::Binding(
1172                "transport_not_accepted.dispatch_commit",
1173            ))?;
1174        validate_retained_dispatch_commit(operation, commit)?;
1175        let attempt = operation
1176            .provider_attempt()
1177            .ok_or(ToolOutcomeError::Binding(
1178                "transport_not_accepted.provider_attempt",
1179            ))?;
1180        let slot = cancellation.slot();
1181        slot.validate()
1182            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1183        let Some(EconomicEffectTerminalV1::NoEffect {
1184            kind: EconomicNoEffectKindV1::VerifiedTransportNotAccepted,
1185            proof_id,
1186            proof_digest,
1187            ..
1188        }) = &slot.terminal
1189        else {
1190            return Err(ToolOutcomeError::Binding(
1191                "transport_not_accepted.economic_terminal",
1192            ));
1193        };
1194        if slot.state != EconomicEffectStateV1::NoEffect
1195            || slot.operation_id != operation.binding().operation_id().as_str()
1196            || slot.request.request_namespace_digest
1197                != operation.replay_key().request_namespace_digest.as_str()
1198            || slot.request.request_id != operation.replay_key().request_id.as_str()
1199            || slot.request.request_binding_digest
1200                != operation.binding().request_binding_hash().as_str()
1201            || slot.admission_handoff.state != EconomicAdmissionHandoffStateV1::DispatchCommitted
1202            || slot.admission_handoff.operation_version != operation.version()
1203            || slot.admission_handoff.lifecycle_fence != operation.coordinator_lease_epoch()
1204            || slot.admission_handoff.store_fence != commit.store_fence
1205            || slot.target.target_id != attempt.transport_id
1206            || slot.target.target_key_epoch != attempt.transport_key_epoch
1207            || cancellation.resulting_head_version() <= cancellation.expected_head_version()
1208        {
1209            return Err(ToolOutcomeError::Binding(
1210                "transport_not_accepted.economic_binding",
1211            ));
1212        }
1213        let cancellation_artifact = EconomicEffectCancellationArtifactV1 {
1214            slot: slot.clone(),
1215            expected_head_version: cancellation.expected_head_version(),
1216            expected_head_digest: imported_digest(
1217                "economic_cancellation.expected_head_digest",
1218                cancellation.expected_head_digest(),
1219            )?,
1220            expected_lifecycle_fence: cancellation.expected_lifecycle_fence(),
1221            resulting_head_version: cancellation.resulting_head_version(),
1222            resulting_head_digest: imported_digest(
1223                "economic_cancellation.resulting_head_digest",
1224                cancellation.resulting_head_digest(),
1225            )?,
1226            resulting_lifecycle_fence: cancellation.resulting_lifecycle_fence(),
1227            checkpoint_sequence: cancellation.checkpoint_sequence(),
1228            checkpoint_digest: imported_digest(
1229                "economic_cancellation.checkpoint_digest",
1230                cancellation.checkpoint_digest(),
1231            )?,
1232        };
1233        let cancellation_evidence = ImmutableReleaseArtifactV1::new(
1234            ReleaseEvidenceArtifactKindV1::EconomicEffectCancellation,
1235            release_id("economic_cancellation_evidence_id", proof_id.clone())?,
1236            serde_json::to_value(&cancellation_artifact)
1237                .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1238        )?;
1239        let policy = ImmutableReleaseArtifactV1::new(
1240            ReleaseEvidenceArtifactKindV1::VerifierPolicy,
1241            release_id(
1242                "verifier_policy_evidence_id",
1243                format!("{}:economic-anchor-policy", slot.operation_id),
1244            )?,
1245            serde_json::to_value(VerifierPolicyArtifactV1 {
1246                policy: serde_json::json!({
1247                    "anchorId": slot.anchor_id,
1248                    "namespace": slot.namespace,
1249                    "targetQualificationDigest": slot.target.qualification_digest,
1250                }),
1251            })
1252            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1253        )?;
1254        let proof = Self {
1255            operation_id: operation.binding().operation_id().clone(),
1256            operation_version: operation.version(),
1257            request_id: operation.replay_key().request_id,
1258            request_binding_hash: operation.binding().request_binding_hash().clone(),
1259            dispatch_operation_version: commit.committed_version,
1260            dispatch_fence: commit.store_fence.owner_epoch,
1261            projection_coordinator_lease_id: context.coordinator_lease_id.clone(),
1262            projection_coordinator_lease_epoch: context.coordinator_lease_epoch,
1263            projection_store_fence: context.store_fence.clone(),
1264            transport_attempt_id: release_id("transport_attempt_id", attempt.attempt_id.clone())?,
1265            transport_identity: release_id("transport_identity", attempt.transport_id.clone())?,
1266            transport_key_epoch: attempt.transport_key_epoch,
1267            signed_status_digest: imported_digest(
1268                "transport_not_accepted.signed_status_digest",
1269                proof_digest,
1270            )?,
1271            qualification_digest: imported_digest(
1272                "transport_not_accepted.qualification_digest",
1273                slot.target.qualification_digest.clone(),
1274            )?,
1275            cancellation_fence: cancellation.resulting_head_version(),
1276            verified_at_unix_ms: context.trusted_time_unix_ms,
1277            verifier_identity: release_id(
1278                "transport_not_accepted.verifier_identity",
1279                slot.anchor_id.clone(),
1280            )?,
1281            monotonic_checkpoint_digest: cancellation_artifact.checkpoint_digest.clone(),
1282            verifier_policy_digest: policy.digest.clone(),
1283            artifacts: vec![cancellation_evidence, policy],
1284        };
1285        proof.validate_against(operation, context)?;
1286        Ok(proof)
1287    }
1288
1289    pub(crate) fn verify_economic_cancellation_binding(
1290        &self,
1291        slot: &EconomicEffectSlotV1,
1292        expected_head: &EconomicResourceHeadV1,
1293        resulting_head: &EconomicResourceHeadV1,
1294        batch: &EconomicStateBatchV1,
1295    ) -> Result<(), ToolOutcomeError> {
1296        let artifact: EconomicEffectCancellationArtifactV1 = self
1297            .artifacts
1298            .first()
1299            .ok_or(ToolOutcomeError::Binding(
1300                "transport_not_accepted.economic_artifact_shape",
1301            ))
1302            .and_then(parse_artifact_value)?;
1303        let expected = EconomicEffectCancellationArtifactV1 {
1304            slot: slot.clone(),
1305            expected_head_version: expected_head.head_version,
1306            expected_head_digest: imported_digest(
1307                "economic_cancellation.expected_head_digest",
1308                expected_head
1309                    .digest()
1310                    .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1311            )?,
1312            expected_lifecycle_fence: expected_head.lifecycle_fence,
1313            resulting_head_version: resulting_head.head_version,
1314            resulting_head_digest: imported_digest(
1315                "economic_cancellation.resulting_head_digest",
1316                resulting_head
1317                    .digest()
1318                    .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1319            )?,
1320            resulting_lifecycle_fence: resulting_head.lifecycle_fence,
1321            checkpoint_sequence: batch.checkpoint_sequence,
1322            checkpoint_digest: imported_digest(
1323                "economic_cancellation.checkpoint_digest",
1324                batch.checkpoint_digest.clone(),
1325            )?,
1326        };
1327        if artifact != expected {
1328            return Err(ToolOutcomeError::Binding(
1329                "transport_not_accepted.economic_cancellation_binding",
1330            ));
1331        }
1332        Ok(())
1333    }
1334
1335    #[allow(dead_code)]
1336    #[allow(clippy::too_many_arguments)]
1337    pub(crate) fn from_verified_provider(
1338        status: &VerifiedProviderNotAccepted,
1339        qualification: &QualifiedDispatchStatusProvider,
1340        query: &DispatchStatusQuery,
1341        operation: &AdmissionOperationV1,
1342        commit: &AdmissionDispatchCommitBindingV1,
1343        context: &AdmissionProjectionContext,
1344        verifier_identity: AdmissionIdentifier,
1345        verifier_policy: Value,
1346    ) -> Result<Self, ToolOutcomeError> {
1347        query
1348            .validate()
1349            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1350        status
1351            .checkpoint()
1352            .validate()
1353            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1354        status
1355            .cancellation()
1356            .validate()
1357            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1358        positive("transport_not_accepted.observed_at", query.observed_at)?;
1359        positive(
1360            "transport_not_accepted.cancelled_at",
1361            status.cancellation().cancelled_at,
1362        )?;
1363        positive(
1364            "transport_not_accepted.cancellation_fence",
1365            status.cancellation().cancellation_fence,
1366        )?;
1367        validate_retained_dispatch_commit(operation, commit)?;
1368        validate_projection_context(operation, context)?;
1369        let attempt = &status.checkpoint().attempt;
1370        let invocation_blob = status.checkpoint().phase.invocation_blob();
1371        let ProviderAttemptPhaseV1::Cancelled {
1372            cancellation: checkpoint_cancellation,
1373            ..
1374        } = &status.checkpoint().phase
1375        else {
1376            return Err(ToolOutcomeError::Binding(
1377                "transport_not_accepted.checkpoint_phase",
1378            ));
1379        };
1380        if query.attempt != *attempt
1381            || status.cancellation() != checkpoint_cancellation
1382            || query.observed_at < status.cancellation().cancelled_at
1383            || query.observed_at > context.trusted_time_unix_ms
1384            || attempt.operation_id != operation.binding().operation_id().as_str()
1385            || operation.provider_attempt() != Some(attempt)
1386            || invocation_blob.request_digest != operation.binding().request_binding_hash().as_str()
1387            || attempt.transport_id != qualification.transport_id()
1388            || attempt.transport_key_epoch != qualification.transport_key_epoch()
1389            || status.qualification_digest() != qualification.qualification_digest()
1390        {
1391            return Err(ToolOutcomeError::Binding("transport_not_accepted.provider"));
1392        }
1393        let operation_id = operation.binding().operation_id().clone();
1394        let cancellation_digest = imported_digest(
1395            "transport_not_accepted.signed_status_digest",
1396            status
1397                .cancellation()
1398                .digest()
1399                .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1400        )?;
1401        let qualification_digest = imported_digest(
1402            "transport_not_accepted.qualification_digest",
1403            status.qualification_digest().to_owned(),
1404        )?;
1405        let signed_status = ImmutableReleaseArtifactV1::new(
1406            ReleaseEvidenceArtifactKindV1::SignedTransportStatus,
1407            release_id(
1408                "signed_transport_status_evidence_id",
1409                status.cancellation().cancellation_ref.clone(),
1410            )?,
1411            serde_json::to_value(SignedTransportStatusArtifactV1 {
1412                cancellation: status.cancellation().clone(),
1413                no_acceptance_proof_base64: BASE64.encode(status.proof()),
1414                qualification_digest: qualification_digest.clone(),
1415                observed_at_unix_ms: query.observed_at,
1416                verifier_identity: verifier_identity.clone(),
1417            })
1418            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1419        )?;
1420        let checkpoint = ImmutableReleaseArtifactV1::new(
1421            ReleaseEvidenceArtifactKindV1::MonotonicAttemptCheckpoint,
1422            release_id(
1423                "checkpoint_evidence_id",
1424                format!("{}:checkpoint", attempt.attempt_id),
1425            )?,
1426            serde_json::to_value(status.checkpoint())
1427                .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1428        )?;
1429        let policy = ImmutableReleaseArtifactV1::new(
1430            ReleaseEvidenceArtifactKindV1::VerifierPolicy,
1431            release_id(
1432                "verifier_policy_evidence_id",
1433                format!("{}:verifier-policy", operation_id.as_str()),
1434            )?,
1435            serde_json::to_value(VerifierPolicyArtifactV1 {
1436                policy: verifier_policy,
1437            })
1438            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1439        )?;
1440        let proof = Self {
1441            operation_id,
1442            operation_version: operation.version(),
1443            request_id: operation.replay_key().request_id,
1444            request_binding_hash: operation.binding().request_binding_hash().clone(),
1445            dispatch_operation_version: commit.committed_version,
1446            dispatch_fence: commit.store_fence.owner_epoch,
1447            projection_coordinator_lease_id: context.coordinator_lease_id.clone(),
1448            projection_coordinator_lease_epoch: context.coordinator_lease_epoch,
1449            projection_store_fence: context.store_fence.clone(),
1450            transport_attempt_id: release_id("transport_attempt_id", attempt.attempt_id.clone())?,
1451            transport_identity: release_id("transport_identity", attempt.transport_id.clone())?,
1452            transport_key_epoch: attempt.transport_key_epoch,
1453            signed_status_digest: cancellation_digest,
1454            qualification_digest,
1455            cancellation_fence: status.cancellation().cancellation_fence,
1456            verified_at_unix_ms: context.trusted_time_unix_ms,
1457            verifier_identity,
1458            monotonic_checkpoint_digest: imported_digest(
1459                "transport_not_accepted.checkpoint_digest",
1460                status.checkpoint_digest().to_owned(),
1461            )?,
1462            verifier_policy_digest: policy.digest.clone(),
1463            artifacts: vec![signed_status, checkpoint, policy],
1464        };
1465        proof.validate_against(operation, context)?;
1466        Ok(proof)
1467    }
1468
1469    pub(crate) fn validate_against(
1470        &self,
1471        operation: &AdmissionOperationV1,
1472        context: &AdmissionProjectionContext,
1473    ) -> Result<(), ToolOutcomeError> {
1474        validate_projection_context(operation, context)?;
1475        let commit = operation
1476            .dispatch_commit()
1477            .ok_or(ToolOutcomeError::Binding(
1478                "transport_not_accepted.dispatch_commit",
1479            ))?;
1480        validate_retained_dispatch_commit(operation, commit)?;
1481        validate_successor_fence(&self.projection_store_fence, &context.store_fence)?;
1482        if self.operation_id != *operation.binding().operation_id()
1483            || self.operation_id != context.operation_id
1484            || self.request_id != operation.replay_key().request_id
1485            || self.request_id != context.request_id
1486            || self.request_binding_hash != *operation.binding().request_binding_hash()
1487            || self.operation_version != operation.version()
1488            || self.operation_version != context.expected_operation_version
1489            || self.dispatch_operation_version != commit.committed_version
1490            || self.dispatch_fence != commit.store_fence.owner_epoch
1491            || self.projection_coordinator_lease_epoch != context.coordinator_lease_epoch
1492            || self.verified_at_unix_ms == 0
1493            || self.verified_at_unix_ms > context.trusted_time_unix_ms
1494            || (self.projection_store_fence == context.store_fence
1495                && self.projection_coordinator_lease_id != context.coordinator_lease_id)
1496        {
1497            return Err(ToolOutcomeError::Binding(
1498                "transport_not_accepted.projection_context",
1499            ));
1500        }
1501        match self.artifacts.first().map(|artifact| artifact.kind) {
1502            Some(ReleaseEvidenceArtifactKindV1::SignedTransportStatus) => {
1503                self.validate_provider_evidence(operation)
1504            }
1505            Some(ReleaseEvidenceArtifactKindV1::EconomicEffectCancellation) => {
1506                self.validate_economic_evidence(operation)
1507            }
1508            _ => Err(ToolOutcomeError::Binding(
1509                "transport_not_accepted.artifact_kind",
1510            )),
1511        }
1512    }
1513
1514    fn validate_provider_evidence(
1515        &self,
1516        operation: &AdmissionOperationV1,
1517    ) -> Result<(), ToolOutcomeError> {
1518        if self.artifacts.len() != 3
1519            || self.artifacts[1].kind != ReleaseEvidenceArtifactKindV1::MonotonicAttemptCheckpoint
1520            || self.artifacts[2].kind != ReleaseEvidenceArtifactKindV1::VerifierPolicy
1521            || self.verifier_policy_digest != self.artifacts[2].digest
1522        {
1523            return Err(ToolOutcomeError::Binding(
1524                "transport_not_accepted.provider_artifact_shape",
1525            ));
1526        }
1527        let evidence = transport::validate_artifacts(&self.artifacts, self.verified_at_unix_ms)?;
1528        let signed = &evidence.signed;
1529        let checkpoint = &evidence.checkpoint;
1530        let attempt = &checkpoint.attempt;
1531        let invocation_blob = checkpoint.phase.invocation_blob();
1532        if signed.verifier_identity != self.verifier_identity
1533            || signed.qualification_digest != self.qualification_digest
1534            || evidence.cancellation_digest != self.signed_status_digest
1535            || signed.cancellation.cancellation_fence != self.cancellation_fence
1536            || evidence.checkpoint_digest != self.monotonic_checkpoint_digest
1537            || attempt.operation_id != self.operation_id.as_str()
1538            || attempt.attempt_id != self.transport_attempt_id.as_str()
1539            || operation.provider_attempt() != Some(attempt)
1540            || attempt.transport_id != self.transport_identity.as_str()
1541            || attempt.transport_key_epoch != self.transport_key_epoch
1542            || invocation_blob.request_digest != self.request_binding_hash.as_str()
1543        {
1544            return Err(ToolOutcomeError::Binding(
1545                "transport_not_accepted.artifacts",
1546            ));
1547        }
1548        self.artifacts
1549            .iter()
1550            .try_for_each(ImmutableReleaseArtifactV1::validate)
1551    }
1552
1553    fn validate_economic_evidence(
1554        &self,
1555        operation: &AdmissionOperationV1,
1556    ) -> Result<(), ToolOutcomeError> {
1557        if self.artifacts.len() != 2
1558            || self.artifacts[1].kind != ReleaseEvidenceArtifactKindV1::VerifierPolicy
1559            || self.verifier_policy_digest != self.artifacts[1].digest
1560        {
1561            return Err(ToolOutcomeError::Binding(
1562                "transport_not_accepted.economic_artifact_shape",
1563            ));
1564        }
1565        let artifact: EconomicEffectCancellationArtifactV1 =
1566            parse_artifact_value(&self.artifacts[0])?;
1567        let _: VerifierPolicyArtifactV1 = parse_artifact_value(&self.artifacts[1])?;
1568        artifact
1569            .slot
1570            .validate()
1571            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1572        let attempt = operation
1573            .provider_attempt()
1574            .ok_or(ToolOutcomeError::Binding(
1575                "transport_not_accepted.provider_attempt",
1576            ))?;
1577        let commit = operation
1578            .dispatch_commit()
1579            .ok_or(ToolOutcomeError::Binding(
1580                "transport_not_accepted.dispatch_commit",
1581            ))?;
1582        let Some(EconomicEffectTerminalV1::NoEffect {
1583            kind: EconomicNoEffectKindV1::VerifiedTransportNotAccepted,
1584            proof_digest,
1585            ..
1586        }) = &artifact.slot.terminal
1587        else {
1588            return Err(ToolOutcomeError::Binding(
1589                "transport_not_accepted.economic_terminal",
1590            ));
1591        };
1592        if artifact.slot.state != EconomicEffectStateV1::NoEffect
1593            || artifact.slot.operation_id != self.operation_id.as_str()
1594            || artifact.slot.request.request_namespace_digest
1595                != operation.replay_key().request_namespace_digest.as_str()
1596            || artifact.slot.request.request_id != self.request_id.as_str()
1597            || artifact.slot.request.request_binding_digest != self.request_binding_hash.as_str()
1598            || artifact.slot.admission_handoff.state
1599                != EconomicAdmissionHandoffStateV1::DispatchCommitted
1600            || artifact.slot.admission_handoff.operation_version != self.operation_version
1601            || artifact.slot.admission_handoff.lifecycle_fence
1602                != self.projection_coordinator_lease_epoch
1603            || artifact.slot.admission_handoff.store_fence != commit.store_fence
1604            || artifact.slot.target.target_id != attempt.transport_id
1605            || artifact.slot.target.target_key_epoch != attempt.transport_key_epoch
1606            || artifact.slot.target.qualification_digest != self.qualification_digest.as_str()
1607            || artifact.expected_head_version == 0
1608            || artifact.resulting_head_version <= artifact.expected_head_version
1609            || artifact.expected_lifecycle_fence == 0
1610            || artifact.resulting_lifecycle_fence <= artifact.expected_lifecycle_fence
1611            || artifact.checkpoint_sequence == 0
1612            || artifact.checkpoint_digest != self.monotonic_checkpoint_digest
1613            || artifact.resulting_head_version != self.cancellation_fence
1614            || proof_digest != self.signed_status_digest.as_str()
1615            || attempt.attempt_id != self.transport_attempt_id.as_str()
1616            || attempt.transport_id != self.transport_identity.as_str()
1617            || attempt.transport_key_epoch != self.transport_key_epoch
1618            || artifact.slot.anchor_id != self.verifier_identity.as_str()
1619        {
1620            return Err(ToolOutcomeError::Binding(
1621                "transport_not_accepted.economic_artifacts",
1622            ));
1623        }
1624        self.artifacts
1625            .iter()
1626            .try_for_each(ImmutableReleaseArtifactV1::validate)
1627    }
1628}
1629
1630#[derive(Debug, Clone, Serialize)]
1631pub struct VerifiedContractualZeroCharge {
1632    operation_id: AdmissionOperationId,
1633    request_id: AdmissionIdentifier,
1634    request_binding_hash: AdmissionDigest,
1635    pub(super) operation_version: u64,
1636    pub(super) outcome_version: u64,
1637    projection_coordinator_lease_id: AdmissionIdentifier,
1638    projection_coordinator_lease_epoch: u64,
1639    projection_store_fence: StoreMutationFence,
1640    verified_at_unix_ms: u64,
1641    tool_outcome_id: AdmissionDigest,
1642    evaluation_id: AdmissionDigest,
1643    pricing_verdict_digest: AdmissionDigest,
1644    currency: String,
1645    verifier_policy_digest: AdmissionDigest,
1646    artifacts: Vec<ImmutableReleaseArtifactV1>,
1647}
1648
1649impl VerifiedContractualZeroCharge {
1650    pub(crate) fn from_records(
1651        operation: &AdmissionOperationV1,
1652        context: &AdmissionProjectionContext,
1653        outcome: &ToolOutcomeRecordV1,
1654        evaluation: &PostReturnEvaluationRecordV1,
1655    ) -> Result<Self, ToolOutcomeError> {
1656        validate_projection_context(operation, context)?;
1657        outcome.validate_against(operation)?;
1658        ToolOutcomeTerminalEvidenceV1::from_records(operation, context, outcome, evaluation)?;
1659        let ResolvedToolOutcomeV1::Resolved {
1660            pricing_verdict_digest,
1661            settlement_disposition: SettlementDispositionV1::ContractualZeroCharge { currency },
1662            ..
1663        } = &outcome.disposition
1664        else {
1665            return Err(ToolOutcomeError::Invalid("zero_charge.disposition"));
1666        };
1667        let outcome_artifact = ImmutableReleaseArtifactV1::new(
1668            ReleaseEvidenceArtifactKindV1::TerminalToolOutcome,
1669            release_id(
1670                "terminal_tool_outcome_evidence_id",
1671                format!("{}:outcome", outcome.outcome_id.as_str()),
1672            )?,
1673            serde_json::to_value(outcome.to_persisted())
1674                .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1675        )?;
1676        let evaluation_artifact = ImmutableReleaseArtifactV1::new(
1677            ReleaseEvidenceArtifactKindV1::TerminalPostReturnEvaluation,
1678            release_id(
1679                "terminal_evaluation_evidence_id",
1680                format!("{}:evaluation", evaluation.evaluation_id.as_str()),
1681            )?,
1682            serde_json::to_value(evaluation.to_persisted())
1683                .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1684        )?;
1685        let policy = ImmutableReleaseArtifactV1::new(
1686            ReleaseEvidenceArtifactKindV1::VerifierPolicy,
1687            release_id(
1688                "verifier_policy_evidence_id",
1689                format!("{}:policy", evaluation.plan_digest.as_str()),
1690            )?,
1691            serde_json::to_value(VerifierPolicyArtifactV1 {
1692                policy: serde_json::json!({ "plan_digest": evaluation.plan_digest }),
1693            })
1694            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
1695        )?;
1696        let proof = Self {
1697            operation_id: operation.binding().operation_id().clone(),
1698            request_id: operation.replay_key().request_id,
1699            request_binding_hash: operation.binding().request_binding_hash().clone(),
1700            operation_version: operation.version(),
1701            outcome_version: outcome.version,
1702            projection_coordinator_lease_id: context.coordinator_lease_id.clone(),
1703            projection_coordinator_lease_epoch: context.coordinator_lease_epoch,
1704            projection_store_fence: context.store_fence.clone(),
1705            verified_at_unix_ms: context.trusted_time_unix_ms,
1706            tool_outcome_id: outcome.outcome_id.clone(),
1707            evaluation_id: evaluation.evaluation_id.clone(),
1708            pricing_verdict_digest: pricing_verdict_digest.clone(),
1709            currency: currency.clone(),
1710            verifier_policy_digest: policy.digest.clone(),
1711            artifacts: vec![outcome_artifact, evaluation_artifact, policy],
1712        };
1713        proof.validate_against(operation, context)?;
1714        Ok(proof)
1715    }
1716
1717    #[allow(dead_code)]
1718    pub(crate) fn validate_against(
1719        &self,
1720        operation: &AdmissionOperationV1,
1721        context: &AdmissionProjectionContext,
1722    ) -> Result<(), ToolOutcomeError> {
1723        validate_projection_context(operation, context)?;
1724        validate_successor_fence(&self.projection_store_fence, &context.store_fence)?;
1725        if self.operation_id != *operation.binding().operation_id()
1726            || self.request_id != operation.replay_key().request_id
1727            || self.request_id != context.request_id
1728            || self.request_binding_hash != *operation.binding().request_binding_hash()
1729            || self.operation_version != operation.version()
1730            || self.operation_version != context.expected_operation_version
1731            || self.outcome_version == 0
1732            || self.projection_coordinator_lease_epoch != context.coordinator_lease_epoch
1733            || self.verified_at_unix_ms == 0
1734            || self.verified_at_unix_ms > context.trusted_time_unix_ms
1735            || (self.projection_store_fence == context.store_fence
1736                && self.projection_coordinator_lease_id != context.coordinator_lease_id)
1737            || self.artifacts.len() != 3
1738            || self.artifacts[0].kind != ReleaseEvidenceArtifactKindV1::TerminalToolOutcome
1739            || self.artifacts[1].kind != ReleaseEvidenceArtifactKindV1::TerminalPostReturnEvaluation
1740            || self.artifacts[2].kind != ReleaseEvidenceArtifactKindV1::VerifierPolicy
1741            || self.verifier_policy_digest != self.artifacts[2].digest
1742        {
1743            return Err(ToolOutcomeError::Binding("zero_charge.projection_context"));
1744        }
1745        let outcome_persisted: PersistedToolOutcomeRecordV1 =
1746            parse_artifact_value(&self.artifacts[0])?;
1747        let evaluation_persisted: PersistedPostReturnEvaluationRecordV1 =
1748            parse_artifact_value(&self.artifacts[1])?;
1749        let _: VerifierPolicyArtifactV1 = parse_artifact_value(&self.artifacts[2])?;
1750        let outcome = ToolOutcomeRecordV1::from_persisted(outcome_persisted)?;
1751        let evaluation = PostReturnEvaluationRecordV1::from_persisted(evaluation_persisted)?;
1752        outcome.validate_against(operation)?;
1753        evaluation.validate_against(operation, &outcome)?;
1754        let ResolvedToolOutcomeV1::Resolved {
1755            evaluation_id,
1756            pricing_verdict_digest,
1757            settlement_disposition: SettlementDispositionV1::ContractualZeroCharge { currency },
1758            ..
1759        } = &outcome.disposition
1760        else {
1761            return Err(ToolOutcomeError::Binding(
1762                "zero_charge.artifact_disposition",
1763            ));
1764        };
1765        if outcome.operation_id != self.operation_id
1766            || outcome.request_id != self.request_id
1767            || outcome.outcome_id != self.tool_outcome_id
1768            || outcome.version != self.outcome_version
1769            || outcome.recorded_at_unix_ms > self.verified_at_unix_ms
1770            || evaluation.operation_id != self.operation_id
1771            || evaluation.tool_outcome_id != self.tool_outcome_id
1772            || evaluation.evaluation_id != self.evaluation_id
1773            || evaluation.trusted_time_unix_ms > self.verified_at_unix_ms
1774            || evaluation_id != &self.evaluation_id
1775            || pricing_verdict_digest != &self.pricing_verdict_digest
1776            || currency != &self.currency
1777        {
1778            return Err(ToolOutcomeError::Binding("zero_charge.artifacts"));
1779        }
1780        self.artifacts
1781            .iter()
1782            .try_for_each(ImmutableReleaseArtifactV1::validate)
1783    }
1784}
1785
1786fn validate_projection_context(
1787    operation: &AdmissionOperationV1,
1788    context: &AdmissionProjectionContext,
1789) -> Result<(), ToolOutcomeError> {
1790    context
1791        .validate()
1792        .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1793    operation
1794        .validate()
1795        .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
1796    if let Some(commit) = operation.dispatch_commit() {
1797        validate_successor_fence(&commit.store_fence, &context.store_fence)?;
1798    }
1799    if context.operation_id != *operation.binding().operation_id()
1800        || context.request_id != operation.replay_key().request_id
1801        || context.expected_operation_version != operation.version()
1802        || context.coordinator_lease_epoch != operation.coordinator_lease_epoch()
1803    {
1804        return Err(ToolOutcomeError::Binding("release.projection_context"));
1805    }
1806    Ok(())
1807}
1808
1809#[derive(Debug, Clone, Serialize)]
1810#[serde(tag = "kind", rename_all = "snake_case")]
1811pub enum VerifiedNoEffectProof {
1812    BeforeDispatch(VerifiedPreDispatchNoEffect),
1813    NotAcceptedAfterDispatch(Box<VerifiedTransportNotAccepted>),
1814}
1815
1816#[derive(Debug, Clone, Serialize)]
1817#[serde(tag = "kind", rename_all = "snake_case")]
1818pub enum MonetaryReleaseAuthority {
1819    NoEffect(VerifiedNoEffectProof),
1820    ContractualZeroCharge(Box<VerifiedContractualZeroCharge>),
1821}
1822
1823#[cfg(test)]
1824mod tests;