chio_kernel/tool_outcome/release/
terminal.rs1use 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}