Skip to main content

chio_kernel/tool_outcome/release/
terminal.rs

1//! Closed terminal outcome/evaluation projection evidence.
2
3use super::*;
4use crate::admission_operation::VerifiedAdmissionReceipt;
5use crate::Keypair;
6use chio_core::canonical::canonical_json_bytes;
7use chio_core::crypto::sha256_hex;
8use chio_core::economic_continuity::{EconomicContentV1, EconomicTerminalResultV1};
9use chio_settle::channel::{
10    verify_channel_terminal_outcome_commitment, ChannelTerminalOutcomeCommitmentBodyV1,
11    SignedChannelTerminalOutcomeCommitmentV1, VerifiedAdmittedChannelReservationV1,
12    CHANNEL_TERMINAL_OUTCOME_COMMITMENT_SCHEMA,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16pub struct ToolOutcomeTerminalEvidenceV1 {
17    operation_id: AdmissionOperationId,
18    request_id: AdmissionIdentifier,
19    operation_version: u64,
20    dispatch_commit: AdmissionDispatchCommitBindingV1,
21    outcome_recording_fence: StoreMutationFence,
22    outcome_recorded_at_unix_ms: u64,
23    projection_coordinator_lease_id: AdmissionIdentifier,
24    projection_coordinator_lease_epoch: u64,
25    projection_store_fence: StoreMutationFence,
26    trusted_time_unix_ms: u64,
27    outcome_id: AdmissionDigest,
28    outcome_version: u64,
29    outcome_lifecycle_digest: AdmissionDigest,
30    tool_server: AdmissionIdentifier,
31    tool_name: AdmissionIdentifier,
32    evaluation_id: AdmissionDigest,
33    evaluation_lifecycle_digest: AdmissionDigest,
34    raw_output_digest: AdmissionDigest,
35    resolved_output_digest: AdmissionDigest,
36    terminal_dependency_root_digest: AdmissionDigest,
37    post_guard_decision_digest: AdmissionDigest,
38    pricing_verdict_digest: AdmissionDigest,
39    settlement_disposition: SettlementDispositionV1,
40}
41
42impl ToolOutcomeTerminalEvidenceV1 {
43    #[cfg(feature = "admission-test-support")]
44    pub fn from_records_for_test(
45        operation: &AdmissionOperationV1,
46        context: &AdmissionProjectionContext,
47        outcome: &ToolOutcomeRecordV1,
48        evaluation: &PostReturnEvaluationRecordV1,
49    ) -> Result<Self, ToolOutcomeError> {
50        Self::from_records(operation, context, outcome, evaluation)
51    }
52
53    pub(crate) fn from_records(
54        operation: &AdmissionOperationV1,
55        context: &AdmissionProjectionContext,
56        outcome: &ToolOutcomeRecordV1,
57        evaluation: &PostReturnEvaluationRecordV1,
58    ) -> Result<Self, ToolOutcomeError> {
59        validate_projection_context(operation, context)?;
60        outcome.validate_against(operation)?;
61        evaluation.validate_against(operation, outcome)?;
62        let ResolvedToolOutcomeV1::Resolved {
63            evaluation_id,
64            resolved_output,
65            terminal_dependency_root_digest,
66            post_guard_decision_digest,
67            pricing_verdict_digest,
68            settlement_disposition,
69            ..
70        } = &outcome.disposition
71        else {
72            return Err(ToolOutcomeError::Invalid("terminal_projection.outcome"));
73        };
74        let PostReturnEvaluationStateV1::Resolved { resolution } = &evaluation.state else {
75            return Err(ToolOutcomeError::Invalid("terminal_projection.evaluation"));
76        };
77        if outcome.operation_id != evaluation.operation_id
78            || outcome.outcome_id != evaluation.tool_outcome_id
79            || evaluation.tool_outcome_version.checked_add(1) != Some(outcome.version)
80            || outcome.raw_output.digest() != &evaluation.raw_output_digest
81            || evaluation_id != &evaluation.evaluation_id
82            || resolved_output != &resolution.resolved_output
83            || terminal_dependency_root_digest != &resolution.terminal_dependency_root_digest
84            || post_guard_decision_digest != &resolution.post_guard_decision_digest
85            || pricing_verdict_digest != &resolution.pricing_verdict_digest
86            || settlement_disposition != &resolution.settlement_disposition
87        {
88            return Err(ToolOutcomeError::Binding("terminal_projection.records"));
89        }
90        let evidence = Self {
91            operation_id: outcome.operation_id.clone(),
92            request_id: outcome.request_id.clone(),
93            operation_version: operation.version(),
94            dispatch_commit: outcome.dispatch_commit.clone(),
95            outcome_recording_fence: outcome.recording_fence.clone(),
96            outcome_recorded_at_unix_ms: outcome.recorded_at_unix_ms,
97            projection_coordinator_lease_id: context.coordinator_lease_id.clone(),
98            projection_coordinator_lease_epoch: context.coordinator_lease_epoch,
99            projection_store_fence: context.store_fence.clone(),
100            trusted_time_unix_ms: context.trusted_time_unix_ms,
101            outcome_id: outcome.outcome_id.clone(),
102            outcome_version: outcome.version,
103            outcome_lifecycle_digest: outcome.lifecycle_digest.clone(),
104            tool_server: outcome.tool_server.clone(),
105            tool_name: outcome.tool_name.clone(),
106            evaluation_id: evaluation.evaluation_id.clone(),
107            evaluation_lifecycle_digest: evaluation.lifecycle_digest.clone(),
108            raw_output_digest: outcome.raw_output.digest().clone(),
109            resolved_output_digest: resolved_output.digest().clone(),
110            terminal_dependency_root_digest: terminal_dependency_root_digest.clone(),
111            post_guard_decision_digest: post_guard_decision_digest.clone(),
112            pricing_verdict_digest: pricing_verdict_digest.clone(),
113            settlement_disposition: settlement_disposition.clone(),
114        };
115        evidence.validate_against(operation, context)?;
116        Ok(evidence)
117    }
118
119    pub(crate) fn validate_against(
120        &self,
121        operation: &AdmissionOperationV1,
122        context: &AdmissionProjectionContext,
123    ) -> Result<(), ToolOutcomeError> {
124        validate_projection_context(operation, context)?;
125        let commit = operation
126            .dispatch_commit()
127            .ok_or(ToolOutcomeError::Binding(
128                "terminal_projection.dispatch_commit",
129            ))?;
130        validate_retained_dispatch_commit(operation, commit)?;
131        validate_successor_fence(
132            &self.dispatch_commit.store_fence,
133            &self.outcome_recording_fence,
134        )?;
135        validate_successor_fence(&self.outcome_recording_fence, &self.projection_store_fence)?;
136        validate_successor_fence(&self.projection_store_fence, &context.store_fence)?;
137        positive(
138            "terminal_projection.outcome_recorded_at",
139            self.outcome_recorded_at_unix_ms,
140        )?;
141        positive(
142            "terminal_projection.trusted_time",
143            self.trusted_time_unix_ms,
144        )?;
145        if self.operation_id != *operation.binding().operation_id()
146            || self.request_id != operation.replay_key().request_id
147            || self.request_id != context.request_id
148            || self.operation_version != operation.version()
149            || self.operation_version != context.expected_operation_version
150            || self.dispatch_commit != *commit
151            || self.dispatch_commit.committed_version == 0
152            || self.projection_coordinator_lease_epoch != context.coordinator_lease_epoch
153            || self.trusted_time_unix_ms > context.trusted_time_unix_ms
154            || (self.projection_store_fence == context.store_fence
155                && self.projection_coordinator_lease_id != context.coordinator_lease_id)
156            || self.outcome_version == 0
157        {
158            return Err(ToolOutcomeError::Binding(
159                "terminal_projection.projection_context",
160            ));
161        }
162        Ok(())
163    }
164
165    pub(crate) fn outcome_id(&self) -> &AdmissionDigest {
166        &self.outcome_id
167    }
168
169    pub(crate) fn outcome_version(&self) -> u64 {
170        self.outcome_version
171    }
172
173    pub(crate) fn tool_server(&self) -> &AdmissionIdentifier {
174        &self.tool_server
175    }
176
177    pub(crate) fn tool_name(&self) -> &AdmissionIdentifier {
178        &self.tool_name
179    }
180
181    pub(crate) fn resolved_output_digest(&self) -> &AdmissionDigest {
182        &self.resolved_output_digest
183    }
184
185    pub(crate) const fn settlement_disposition(&self) -> &SettlementDispositionV1 {
186        &self.settlement_disposition
187    }
188}
189
190#[cfg(feature = "admission-test-support")]
191pub fn sign_channel_terminal_outcome_commitment_for_test(
192    operation: &AdmissionOperationV1,
193    reservation: &VerifiedAdmittedChannelReservationV1,
194    receipt: &VerifiedAdmissionReceipt,
195    tool_outcome: &ToolOutcomeTerminalEvidenceV1,
196    context: &AdmissionProjectionContext,
197    kernel_keypair: &Keypair,
198) -> Result<SignedChannelTerminalOutcomeCommitmentV1, ToolOutcomeError> {
199    sign_channel_terminal_outcome_commitment(
200        operation,
201        reservation,
202        receipt,
203        tool_outcome,
204        context,
205        kernel_keypair,
206    )
207}
208
209pub(crate) fn sign_channel_terminal_outcome_commitment(
210    operation: &AdmissionOperationV1,
211    reservation: &VerifiedAdmittedChannelReservationV1,
212    receipt: &VerifiedAdmissionReceipt,
213    tool_outcome: &ToolOutcomeTerminalEvidenceV1,
214    context: &AdmissionProjectionContext,
215    kernel_keypair: &Keypair,
216) -> Result<SignedChannelTerminalOutcomeCommitmentV1, ToolOutcomeError> {
217    let mismatch = || ToolOutcomeError::Binding("channel_terminal_outcome_commitment");
218    tool_outcome.validate_against(operation, context)?;
219    receipt
220        .validate_against(
221            operation,
222            context,
223            AdmissionOperationState::Completed,
224            crate::admission_operation::AdmissionCompensationStatus::NotCompensated,
225            Some((tool_outcome.outcome_id(), tool_outcome.outcome_version())),
226        )
227        .map_err(|_| mismatch())?;
228    if reservation.artifact().body.operation_id != operation.binding().operation_id().as_str() {
229        return Err(mismatch());
230    }
231    let result = EconomicContentV1::Inline {
232        value: serde_json::to_value(tool_outcome)
233            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
234    };
235    let terminal_result = EconomicTerminalResultV1 {
236        result_id: tool_outcome.outcome_id.as_str().to_owned(),
237        result_digest: result.digest().map_err(|_| mismatch())?,
238        result,
239    };
240    let receipt_digest = sha256_hex(
241        &canonical_json_bytes(receipt.receipt())
242            .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
243    );
244    let body = ChannelTerminalOutcomeCommitmentBodyV1 {
245        schema: CHANNEL_TERMINAL_OUTCOME_COMMITMENT_SCHEMA.to_owned(),
246        operation_id: reservation.artifact().body.operation_id.clone(),
247        reservation_id: reservation.artifact().body.reservation_id.clone(),
248        reservation_digest: reservation.artifact().digest().map_err(|_| mismatch())?,
249        receipt_id: receipt.receipt().id.clone(),
250        receipt_digest,
251        terminal_result,
252        outcome_recorded_at_unix_ms: tool_outcome.outcome_recorded_at_unix_ms,
253        terminalized_at_unix_ms: context.trusted_time_unix_ms,
254    };
255    let kernel_signature = kernel_keypair.sign(&body.signing_bytes().map_err(|_| mismatch())?);
256    let signed = SignedChannelTerminalOutcomeCommitmentV1 {
257        body,
258        kernel_key: kernel_keypair.public_key(),
259        kernel_signature,
260    };
261    verify_channel_terminal_outcome_commitment(
262        &signed,
263        &kernel_keypair.public_key(),
264        reservation,
265        receipt.receipt(),
266    )
267    .map_err(|_| mismatch())?;
268    Ok(signed)
269}
270
271pub(crate) trait QualifiedDurableOutcomeAuthority: Send + Sync {
272    fn verify_terminal_outcome(
273        &self,
274        operation: &AdmissionOperationV1,
275        context: &AdmissionProjectionContext,
276    ) -> Result<ToolOutcomeTerminalEvidenceV1, ToolOutcomeError>;
277
278    fn verify_contractual_zero_charge(
279        &self,
280        operation: &AdmissionOperationV1,
281        context: &AdmissionProjectionContext,
282    ) -> Result<VerifiedContractualZeroCharge, ToolOutcomeError>;
283}