1use super::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum MonetaryReleaseEvidenceKindV1 {
8 BeforeDispatchNoEffect,
9 NotAcceptedAfterDispatch,
10 ContractualZeroCharge,
11}
12
13#[derive(Debug, Clone, PartialEq, Serialize)]
14pub struct MonetaryReleaseEvidenceV1 {
15 pub(super) schema: &'static str,
16 pub(super) evidence_id: AdmissionDigest,
17 pub(super) evidence_kind: MonetaryReleaseEvidenceKindV1,
18 pub(super) operation_id: AdmissionOperationId,
19 pub(super) operation_version: u64,
20 pub(super) verifier_policy_digest: AdmissionDigest,
21 pub(super) source_binding_base64: String,
22 pub(super) source_binding_digest: AdmissionDigest,
23 pub(super) source_artifacts: Vec<ImmutableReleaseArtifactV1>,
24 pub(super) bundle_digest: AdmissionDigest,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct PersistedImmutableReleaseArtifactV1 {
30 pub kind: ReleaseEvidenceArtifactKindV1,
31 pub evidence_id: AdmissionIdentifier,
32 pub digest: AdmissionDigest,
33 pub value: Value,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct PersistedMonetaryReleaseEvidenceV1 {
39 pub schema: String,
40 pub evidence_id: AdmissionDigest,
41 pub evidence_kind: MonetaryReleaseEvidenceKindV1,
42 pub operation_id: AdmissionOperationId,
43 pub operation_version: u64,
44 pub verifier_policy_digest: AdmissionDigest,
45 pub source_binding_base64: String,
46 pub source_binding_digest: AdmissionDigest,
47 pub source_artifacts: Vec<PersistedImmutableReleaseArtifactV1>,
48 pub bundle_digest: AdmissionDigest,
49}
50
51#[derive(Serialize)]
52struct ReleaseEvidenceBody<'a> {
53 schema: &'a str,
54 evidence_kind: MonetaryReleaseEvidenceKindV1,
55 operation_id: &'a AdmissionOperationId,
56 operation_version: u64,
57 verifier_policy_digest: &'a AdmissionDigest,
58 source_binding_base64: &'a str,
59 source_binding_digest: &'a AdmissionDigest,
60 source_artifacts: &'a [ImmutableReleaseArtifactV1],
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64#[serde(tag = "kind", content = "binding", rename_all = "snake_case")]
65enum ReleaseSourceBindingV1 {
66 BeforeDispatchNoEffect(Box<PreDispatchReleaseSourceV1>),
67 NotAcceptedAfterDispatch(Box<TransportReleaseSourceV1>),
68 ContractualZeroCharge(Box<ZeroChargeReleaseSourceV1>),
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73struct PreDispatchReleaseSourceV1 {
74 operation_id: AdmissionOperationId,
75 operation_version: u64,
76 request_binding_hash: AdmissionDigest,
77 coordinator_lease_id: AdmissionIdentifier,
78 coordinator_lease_epoch: u64,
79 verification_store_fence: StoreMutationFence,
80 verified_at_unix_ms: u64,
81 participant_manifest: PreDispatchParticipantManifestV1,
82 complete_participant_query_root: AdmissionDigest,
83 verifier_policy_digest: AdmissionDigest,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88struct TransportReleaseSourceV1 {
89 operation_id: AdmissionOperationId,
90 operation_version: u64,
91 request_id: AdmissionIdentifier,
92 request_binding_hash: AdmissionDigest,
93 dispatch_operation_version: u64,
94 dispatch_fence: u64,
95 projection_coordinator_lease_id: AdmissionIdentifier,
96 projection_coordinator_lease_epoch: u64,
97 projection_store_fence: StoreMutationFence,
98 transport_attempt_id: AdmissionIdentifier,
99 transport_identity: AdmissionIdentifier,
100 transport_key_epoch: u64,
101 signed_status_digest: AdmissionDigest,
102 qualification_digest: AdmissionDigest,
103 cancellation_fence: u64,
104 verified_at_unix_ms: u64,
105 verifier_identity: AdmissionIdentifier,
106 monotonic_checkpoint_digest: AdmissionDigest,
107 verifier_policy_digest: AdmissionDigest,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(deny_unknown_fields)]
112struct ZeroChargeReleaseSourceV1 {
113 operation_id: AdmissionOperationId,
114 request_id: AdmissionIdentifier,
115 request_binding_hash: AdmissionDigest,
116 operation_version: u64,
117 outcome_version: u64,
118 projection_coordinator_lease_id: AdmissionIdentifier,
119 projection_coordinator_lease_epoch: u64,
120 projection_store_fence: StoreMutationFence,
121 verified_at_unix_ms: u64,
122 tool_outcome_id: AdmissionDigest,
123 evaluation_id: AdmissionDigest,
124 pricing_verdict_digest: AdmissionDigest,
125 currency: String,
126 verifier_policy_digest: AdmissionDigest,
127}
128
129impl From<&ImmutableReleaseArtifactV1> for PersistedImmutableReleaseArtifactV1 {
130 fn from(value: &ImmutableReleaseArtifactV1) -> Self {
131 Self {
132 kind: value.kind,
133 evidence_id: value.evidence_id.clone(),
134 digest: value.digest.clone(),
135 value: value.value.clone(),
136 }
137 }
138}
139
140impl From<PersistedImmutableReleaseArtifactV1> for ImmutableReleaseArtifactV1 {
141 fn from(value: PersistedImmutableReleaseArtifactV1) -> Self {
142 Self {
143 kind: value.kind,
144 evidence_id: value.evidence_id,
145 digest: value.digest,
146 value: value.value,
147 }
148 }
149}
150
151impl MonetaryReleaseAuthority {
152 pub fn evidence_bundle(&self) -> Result<MonetaryReleaseEvidenceV1, ToolOutcomeError> {
153 let (kind, operation_id, operation_version, policy, artifacts, source) = match self {
154 Self::NoEffect(VerifiedNoEffectProof::BeforeDispatch(proof)) => (
155 MonetaryReleaseEvidenceKindV1::BeforeDispatchNoEffect,
156 &proof.snapshot.operation_id,
157 proof.snapshot.operation_version,
158 &proof.snapshot.verifier_policy_digest,
159 &proof.snapshot.artifacts,
160 ReleaseSourceBindingV1::BeforeDispatchNoEffect(Box::new(
161 PreDispatchReleaseSourceV1 {
162 operation_id: proof.snapshot.operation_id.clone(),
163 operation_version: proof.snapshot.operation_version,
164 request_binding_hash: proof.snapshot.request_binding_hash.clone(),
165 coordinator_lease_id: proof.snapshot.coordinator_lease_id.clone(),
166 coordinator_lease_epoch: proof.snapshot.coordinator_lease_epoch,
167 verification_store_fence: proof.snapshot.verification_store_fence.clone(),
168 verified_at_unix_ms: proof.snapshot.verified_at_unix_ms,
169 participant_manifest: proof.snapshot.participant_manifest.clone(),
170 complete_participant_query_root: proof
171 .snapshot
172 .complete_participant_query_root
173 .clone(),
174 verifier_policy_digest: proof.snapshot.verifier_policy_digest.clone(),
175 },
176 )),
177 ),
178 Self::NoEffect(VerifiedNoEffectProof::NotAcceptedAfterDispatch(proof)) => (
179 MonetaryReleaseEvidenceKindV1::NotAcceptedAfterDispatch,
180 &proof.operation_id,
181 proof.operation_version,
182 &proof.verifier_policy_digest,
183 &proof.artifacts,
184 ReleaseSourceBindingV1::NotAcceptedAfterDispatch(Box::new(
185 TransportReleaseSourceV1 {
186 operation_id: proof.operation_id.clone(),
187 operation_version: proof.operation_version,
188 request_id: proof.request_id.clone(),
189 request_binding_hash: proof.request_binding_hash.clone(),
190 dispatch_operation_version: proof.dispatch_operation_version,
191 dispatch_fence: proof.dispatch_fence,
192 projection_coordinator_lease_id: proof
193 .projection_coordinator_lease_id
194 .clone(),
195 projection_coordinator_lease_epoch: proof
196 .projection_coordinator_lease_epoch,
197 projection_store_fence: proof.projection_store_fence.clone(),
198 transport_attempt_id: proof.transport_attempt_id.clone(),
199 transport_identity: proof.transport_identity.clone(),
200 transport_key_epoch: proof.transport_key_epoch,
201 signed_status_digest: proof.signed_status_digest.clone(),
202 qualification_digest: proof.qualification_digest.clone(),
203 cancellation_fence: proof.cancellation_fence,
204 verified_at_unix_ms: proof.verified_at_unix_ms,
205 verifier_identity: proof.verifier_identity.clone(),
206 monotonic_checkpoint_digest: proof.monotonic_checkpoint_digest.clone(),
207 verifier_policy_digest: proof.verifier_policy_digest.clone(),
208 },
209 )),
210 ),
211 Self::ContractualZeroCharge(proof) => (
212 MonetaryReleaseEvidenceKindV1::ContractualZeroCharge,
213 &proof.operation_id,
214 proof.operation_version,
215 &proof.verifier_policy_digest,
216 &proof.artifacts,
217 ReleaseSourceBindingV1::ContractualZeroCharge(Box::new(
218 ZeroChargeReleaseSourceV1 {
219 operation_id: proof.operation_id.clone(),
220 request_id: proof.request_id.clone(),
221 request_binding_hash: proof.request_binding_hash.clone(),
222 operation_version: proof.operation_version,
223 outcome_version: proof.outcome_version,
224 projection_coordinator_lease_id: proof
225 .projection_coordinator_lease_id
226 .clone(),
227 projection_coordinator_lease_epoch: proof
228 .projection_coordinator_lease_epoch,
229 projection_store_fence: proof.projection_store_fence.clone(),
230 verified_at_unix_ms: proof.verified_at_unix_ms,
231 tool_outcome_id: proof.tool_outcome_id.clone(),
232 evaluation_id: proof.evaluation_id.clone(),
233 pricing_verdict_digest: proof.pricing_verdict_digest.clone(),
234 currency: proof.currency.clone(),
235 verifier_policy_digest: proof.verifier_policy_digest.clone(),
236 },
237 )),
238 ),
239 };
240 MonetaryReleaseEvidenceV1::new(
241 kind,
242 operation_id.clone(),
243 operation_version,
244 policy.clone(),
245 artifacts.clone(),
246 source,
247 )
248 }
249}
250
251impl MonetaryReleaseEvidenceV1 {
252 fn new(
253 evidence_kind: MonetaryReleaseEvidenceKindV1,
254 operation_id: AdmissionOperationId,
255 operation_version: u64,
256 verifier_policy_digest: AdmissionDigest,
257 source_artifacts: Vec<ImmutableReleaseArtifactV1>,
258 source: ReleaseSourceBindingV1,
259 ) -> Result<Self, ToolOutcomeError> {
260 let source_binding = canonical(&source)?;
261 let source_binding_base64 = BASE64.encode(&source_binding);
262 let source_binding_digest = digest_bytes("release.source_binding_digest", &source_binding)?;
263 let body = ReleaseEvidenceBody {
264 schema: MONETARY_RELEASE_EVIDENCE_SCHEMA,
265 evidence_kind,
266 operation_id: &operation_id,
267 operation_version,
268 verifier_policy_digest: &verifier_policy_digest,
269 source_binding_base64: &source_binding_base64,
270 source_binding_digest: &source_binding_digest,
271 source_artifacts: &source_artifacts,
272 };
273 let body_bytes = bounded(
274 "monetary_release_evidence",
275 &body,
276 MAX_MONETARY_RELEASE_EVIDENCE_BYTES,
277 )?;
278 let bundle = Self {
279 schema: MONETARY_RELEASE_EVIDENCE_SCHEMA,
280 evidence_id: domain_digest("chio.monetary-release-evidence.identity.v1", &body)?,
281 evidence_kind,
282 operation_id,
283 operation_version,
284 verifier_policy_digest,
285 source_binding_base64,
286 source_binding_digest,
287 source_artifacts,
288 bundle_digest: digest_bytes("release.bundle_digest", &body_bytes)?,
289 };
290 bundle.validate()?;
291 Ok(bundle)
292 }
293
294 #[must_use]
295 pub fn evidence_id(&self) -> &AdmissionDigest {
296 &self.evidence_id
297 }
298
299 #[must_use]
300 pub fn bundle_digest(&self) -> &AdmissionDigest {
301 &self.bundle_digest
302 }
303
304 pub fn to_persisted(&self) -> PersistedMonetaryReleaseEvidenceV1 {
305 PersistedMonetaryReleaseEvidenceV1 {
306 schema: self.schema.to_owned(),
307 evidence_id: self.evidence_id.clone(),
308 evidence_kind: self.evidence_kind,
309 operation_id: self.operation_id.clone(),
310 operation_version: self.operation_version,
311 verifier_policy_digest: self.verifier_policy_digest.clone(),
312 source_binding_base64: self.source_binding_base64.clone(),
313 source_binding_digest: self.source_binding_digest.clone(),
314 source_artifacts: self.source_artifacts.iter().map(Into::into).collect(),
315 bundle_digest: self.bundle_digest.clone(),
316 }
317 }
318
319 pub fn from_persisted(
320 value: PersistedMonetaryReleaseEvidenceV1,
321 ) -> Result<Self, ToolOutcomeError> {
322 if value.schema != MONETARY_RELEASE_EVIDENCE_SCHEMA {
323 return Err(ToolOutcomeError::Invalid("release_evidence.schema"));
324 }
325 let bundle = Self {
326 schema: MONETARY_RELEASE_EVIDENCE_SCHEMA,
327 evidence_id: value.evidence_id,
328 evidence_kind: value.evidence_kind,
329 operation_id: value.operation_id,
330 operation_version: value.operation_version,
331 verifier_policy_digest: value.verifier_policy_digest,
332 source_binding_base64: value.source_binding_base64,
333 source_binding_digest: value.source_binding_digest,
334 source_artifacts: value.source_artifacts.into_iter().map(Into::into).collect(),
335 bundle_digest: value.bundle_digest,
336 };
337 bundle.validate()?;
338 Ok(bundle)
339 }
340
341 pub fn canonical_bytes(&self) -> Result<Vec<u8>, ToolOutcomeError> {
342 bounded(
343 "monetary_release_evidence",
344 &self.to_persisted(),
345 MAX_MONETARY_RELEASE_EVIDENCE_BYTES,
346 )
347 }
348
349 pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ToolOutcomeError> {
350 if bytes.len() > MAX_MONETARY_RELEASE_EVIDENCE_BYTES {
351 return Err(ToolOutcomeError::TooLarge {
352 field: "monetary_release_evidence",
353 actual: bytes.len(),
354 maximum: MAX_MONETARY_RELEASE_EVIDENCE_BYTES,
355 });
356 }
357 let persisted = serde_json::from_slice::<PersistedMonetaryReleaseEvidenceV1>(bytes)
358 .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
359 let bundle = Self::from_persisted(persisted)?;
360 if bundle.canonical_bytes()? != bytes {
361 return Err(ToolOutcomeError::Invalid(
362 "release_evidence.noncanonical_bytes",
363 ));
364 }
365 Ok(bundle)
366 }
367
368 pub(super) fn validate(&self) -> Result<(), ToolOutcomeError> {
369 let kinds: &[ReleaseEvidenceArtifactKindV1] = match self.evidence_kind {
370 MonetaryReleaseEvidenceKindV1::BeforeDispatchNoEffect => &[
371 ReleaseEvidenceArtifactKindV1::ParticipantQuerySnapshot,
372 ReleaseEvidenceArtifactKindV1::VerifierPolicy,
373 ],
374 MonetaryReleaseEvidenceKindV1::NotAcceptedAfterDispatch => &[
375 ReleaseEvidenceArtifactKindV1::SignedTransportStatus,
376 ReleaseEvidenceArtifactKindV1::MonotonicAttemptCheckpoint,
377 ReleaseEvidenceArtifactKindV1::VerifierPolicy,
378 ],
379 MonetaryReleaseEvidenceKindV1::ContractualZeroCharge => &[
380 ReleaseEvidenceArtifactKindV1::TerminalToolOutcome,
381 ReleaseEvidenceArtifactKindV1::TerminalPostReturnEvaluation,
382 ReleaseEvidenceArtifactKindV1::VerifierPolicy,
383 ],
384 };
385 if self.schema != MONETARY_RELEASE_EVIDENCE_SCHEMA
386 || self.source_artifacts.len() != kinds.len()
387 {
388 return Err(ToolOutcomeError::Invalid("release_evidence.shape"));
389 }
390 positive("release_evidence.operation_version", self.operation_version)?;
391 for (artifact, kind) in self.source_artifacts.iter().zip(kinds) {
392 artifact.validate()?;
393 if artifact.kind != *kind {
394 return Err(ToolOutcomeError::Binding("release_evidence.artifact_kind"));
395 }
396 }
397 let source = self.source_binding()?;
398 self.validate_source(&source)?;
399 let body = self.body();
400 let bytes = bounded(
401 "monetary_release_evidence",
402 &body,
403 MAX_MONETARY_RELEASE_EVIDENCE_BYTES,
404 )?;
405 if digest_bytes("release.bundle_digest", &bytes)? != self.bundle_digest
406 || domain_digest("chio.monetary-release-evidence.identity.v1", &body)?
407 != self.evidence_id
408 {
409 return Err(ToolOutcomeError::Binding("release_evidence.bundle"));
410 }
411 Ok(())
412 }
413
414 pub(crate) fn validate_recovery_context(
415 &self,
416 operation: &AdmissionOperationV1,
417 context: &AdmissionProjectionContext,
418 ) -> Result<(), ToolOutcomeError> {
419 self.validate()?;
420 if self.operation_id != *operation.binding().operation_id()
421 || self.operation_version != operation.version()
422 || self.operation_version != context.expected_operation_version
423 {
424 return Err(ToolOutcomeError::Binding("release_evidence.operation"));
425 }
426 match self.source_binding()? {
427 ReleaseSourceBindingV1::BeforeDispatchNoEffect(source) => {
428 validate_pre_dispatch_context(operation, context)?;
429 source.participant_manifest.validate_for(operation)?;
430 validate_source_projection(
431 &source.verification_store_fence,
432 &source.coordinator_lease_id,
433 source.coordinator_lease_epoch,
434 source.verified_at_unix_ms,
435 context,
436 )
437 }
438 ReleaseSourceBindingV1::NotAcceptedAfterDispatch(source) => {
439 validate_projection_context(operation, context)?;
440 let commit = operation
441 .dispatch_commit()
442 .ok_or(ToolOutcomeError::Binding(
443 "release_evidence.transport_dispatch_commit",
444 ))?;
445 if source.request_id != operation.replay_key().request_id
446 || source.request_id != context.request_id
447 || source.request_binding_hash != *operation.binding().request_binding_hash()
448 || source.dispatch_operation_version != commit.committed_version
449 || source.dispatch_fence != commit.store_fence.owner_epoch
450 {
451 return Err(ToolOutcomeError::Binding(
452 "release_evidence.transport_operation",
453 ));
454 }
455 validate_source_projection(
456 &source.projection_store_fence,
457 &source.projection_coordinator_lease_id,
458 source.projection_coordinator_lease_epoch,
459 source.verified_at_unix_ms,
460 context,
461 )
462 }
463 ReleaseSourceBindingV1::ContractualZeroCharge(source) => {
464 validate_projection_context(operation, context)?;
465 if source.request_id != operation.replay_key().request_id
466 || source.request_id != context.request_id
467 || source.request_binding_hash != *operation.binding().request_binding_hash()
468 {
469 return Err(ToolOutcomeError::Binding(
470 "release_evidence.zero_charge_operation",
471 ));
472 }
473 validate_source_projection(
474 &source.projection_store_fence,
475 &source.projection_coordinator_lease_id,
476 source.projection_coordinator_lease_epoch,
477 source.verified_at_unix_ms,
478 context,
479 )?;
480 validate_zero_charge_records(&source, &self.source_artifacts, Some(operation))
481 }
482 }
483 }
484
485 pub fn revalidate_authority(
486 &self,
487 operation: &AdmissionOperationV1,
488 context: &AdmissionProjectionContext,
489 ) -> Result<MonetaryReleaseAuthority, ToolOutcomeError> {
490 self.validate_recovery_context(operation, context)?;
491 Err(ToolOutcomeError::ReleaseAuthorityUnavailable(
492 "persisted evidence requires a qualified co-located authority query",
493 ))
494 }
495
496 #[cfg(test)]
497 pub(super) fn with_projection_epoch_for_test(
498 &self,
499 coordinator_lease_epoch: u64,
500 ) -> Result<Self, ToolOutcomeError> {
501 let mut source = self.source_binding()?;
502 let mut source_artifacts = self.source_artifacts.clone();
503 match &mut source {
504 ReleaseSourceBindingV1::BeforeDispatchNoEffect(source) => {
505 source.coordinator_lease_epoch = coordinator_lease_epoch;
506 source.participant_manifest.coordinator_lease_epoch = coordinator_lease_epoch;
507 source_artifacts[0] = ImmutableReleaseArtifactV1::new(
508 ReleaseEvidenceArtifactKindV1::ParticipantQuerySnapshot,
509 source_artifacts[0].evidence_id.clone(),
510 serde_json::to_value(&source.participant_manifest)
511 .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
512 )?;
513 source.complete_participant_query_root = source_artifacts[0].digest.clone();
514 }
515 ReleaseSourceBindingV1::NotAcceptedAfterDispatch(source) => {
516 source.projection_coordinator_lease_epoch = coordinator_lease_epoch;
517 }
518 ReleaseSourceBindingV1::ContractualZeroCharge(source) => {
519 source.projection_coordinator_lease_epoch = coordinator_lease_epoch;
520 }
521 }
522 Self::new(
523 self.evidence_kind,
524 self.operation_id.clone(),
525 self.operation_version,
526 self.verifier_policy_digest.clone(),
527 source_artifacts,
528 source,
529 )
530 }
531
532 #[cfg(test)]
533 pub(super) fn with_request_binding_hash_for_test(
534 &self,
535 request_binding_hash: AdmissionDigest,
536 ) -> Result<Self, ToolOutcomeError> {
537 let mut source = self.source_binding()?;
538 let mut source_artifacts = self.source_artifacts.clone();
539 match &mut source {
540 ReleaseSourceBindingV1::BeforeDispatchNoEffect(source) => {
541 source.request_binding_hash = request_binding_hash.clone();
542 source.participant_manifest.request_binding_hash = request_binding_hash;
543 source_artifacts[0] = ImmutableReleaseArtifactV1::new(
544 ReleaseEvidenceArtifactKindV1::ParticipantQuerySnapshot,
545 source_artifacts[0].evidence_id.clone(),
546 serde_json::to_value(&source.participant_manifest)
547 .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?,
548 )?;
549 source.complete_participant_query_root = source_artifacts[0].digest.clone();
550 }
551 ReleaseSourceBindingV1::NotAcceptedAfterDispatch(source) => {
552 source.request_binding_hash = request_binding_hash;
553 }
554 ReleaseSourceBindingV1::ContractualZeroCharge(source) => {
555 source.request_binding_hash = request_binding_hash;
556 }
557 }
558 Self::new(
559 self.evidence_kind,
560 self.operation_id.clone(),
561 self.operation_version,
562 self.verifier_policy_digest.clone(),
563 source_artifacts,
564 source,
565 )
566 }
567
568 fn body(&self) -> ReleaseEvidenceBody<'_> {
569 ReleaseEvidenceBody {
570 schema: self.schema,
571 evidence_kind: self.evidence_kind,
572 operation_id: &self.operation_id,
573 operation_version: self.operation_version,
574 verifier_policy_digest: &self.verifier_policy_digest,
575 source_binding_base64: &self.source_binding_base64,
576 source_binding_digest: &self.source_binding_digest,
577 source_artifacts: &self.source_artifacts,
578 }
579 }
580
581 fn source_binding(&self) -> Result<ReleaseSourceBindingV1, ToolOutcomeError> {
582 let bytes = BASE64
583 .decode(&self.source_binding_base64)
584 .map_err(|_| ToolOutcomeError::Invalid("release_evidence.source_binding_base64"))?;
585 if BASE64.encode(&bytes) != self.source_binding_base64
586 || digest_bytes("release.source_binding_digest", &bytes)? != self.source_binding_digest
587 {
588 return Err(ToolOutcomeError::Binding("release_evidence.source_binding"));
589 }
590 let source = serde_json::from_slice::<ReleaseSourceBindingV1>(&bytes)
591 .map_err(|error| ToolOutcomeError::Canonical(error.to_string()))?;
592 if canonical(&source)? != bytes {
593 return Err(ToolOutcomeError::Invalid(
594 "release_evidence.source_binding_noncanonical",
595 ));
596 }
597 Ok(source)
598 }
599
600 fn validate_source(&self, source: &ReleaseSourceBindingV1) -> Result<(), ToolOutcomeError> {
601 match (self.evidence_kind, source) {
602 (
603 MonetaryReleaseEvidenceKindV1::BeforeDispatchNoEffect,
604 ReleaseSourceBindingV1::BeforeDispatchNoEffect(source),
605 ) => validate_pre_dispatch_source(self, source),
606 (
607 MonetaryReleaseEvidenceKindV1::NotAcceptedAfterDispatch,
608 ReleaseSourceBindingV1::NotAcceptedAfterDispatch(source),
609 ) => validate_transport_source(self, source),
610 (
611 MonetaryReleaseEvidenceKindV1::ContractualZeroCharge,
612 ReleaseSourceBindingV1::ContractualZeroCharge(source),
613 ) => validate_zero_charge_source(self, source),
614 _ => Err(ToolOutcomeError::Binding("release_evidence.source_kind")),
615 }
616 }
617}
618
619fn validate_source_projection(
620 historical_fence: &StoreMutationFence,
621 historical_coordinator_lease_id: &AdmissionIdentifier,
622 historical_coordinator_lease_epoch: u64,
623 verified_at_unix_ms: u64,
624 context: &AdmissionProjectionContext,
625) -> Result<(), ToolOutcomeError> {
626 validate_successor_fence(historical_fence, &context.store_fence)?;
627 positive("release_evidence.verified_at", verified_at_unix_ms)?;
628 if verified_at_unix_ms > context.trusted_time_unix_ms
629 || historical_coordinator_lease_epoch != context.coordinator_lease_epoch
630 || (historical_fence == &context.store_fence
631 && historical_coordinator_lease_id != &context.coordinator_lease_id)
632 {
633 return Err(ToolOutcomeError::Binding(
634 "release_evidence.projection_lineage",
635 ));
636 }
637 Ok(())
638}
639
640fn validate_pre_dispatch_source(
641 bundle: &MonetaryReleaseEvidenceV1,
642 source: &PreDispatchReleaseSourceV1,
643) -> Result<(), ToolOutcomeError> {
644 let manifest: PreDispatchParticipantManifestV1 =
645 parse_artifact_value(&bundle.source_artifacts[0])?;
646 let _: VerifierPolicyArtifactV1 = parse_artifact_value(&bundle.source_artifacts[1])?;
647 positive("release_evidence.verified_at", source.verified_at_unix_ms)?;
648 validate_store_fence(&source.verification_store_fence)?;
649 if source.operation_id != bundle.operation_id
650 || source.operation_version != bundle.operation_version
651 || source.verifier_policy_digest != bundle.verifier_policy_digest
652 || source.complete_participant_query_root != bundle.source_artifacts[0].digest
653 || source.verifier_policy_digest != bundle.source_artifacts[1].digest
654 || source.participant_manifest != manifest
655 || source.request_binding_hash != manifest.request_binding_hash
656 || source.coordinator_lease_id != manifest.coordinator_lease_id
657 || source.coordinator_lease_epoch != manifest.coordinator_lease_epoch
658 || source.verification_store_fence != manifest.verification_store_fence
659 || source.verified_at_unix_ms != manifest.verified_at_unix_ms
660 {
661 return Err(ToolOutcomeError::Binding(
662 "release_evidence.predispatch_source",
663 ));
664 }
665 Ok(())
666}
667
668fn validate_transport_source(
669 bundle: &MonetaryReleaseEvidenceV1,
670 source: &TransportReleaseSourceV1,
671) -> Result<(), ToolOutcomeError> {
672 let evidence =
673 transport::validate_artifacts(&bundle.source_artifacts, source.verified_at_unix_ms)?;
674 let signed = &evidence.signed;
675 let checkpoint = &evidence.checkpoint;
676 let attempt = &checkpoint.attempt;
677 let invocation_blob = checkpoint.phase.invocation_blob();
678 validate_store_fence(&source.projection_store_fence)?;
679 if source.operation_id != bundle.operation_id
680 || source.operation_version != bundle.operation_version
681 || source.verifier_policy_digest != bundle.verifier_policy_digest
682 || source.verifier_policy_digest != bundle.source_artifacts[2].digest
683 || signed.verifier_identity != source.verifier_identity
684 || signed.qualification_digest != source.qualification_digest
685 || evidence.cancellation_digest != source.signed_status_digest
686 || signed.cancellation.cancellation_fence != source.cancellation_fence
687 || evidence.checkpoint_digest != source.monotonic_checkpoint_digest
688 || attempt.operation_id != source.operation_id.as_str()
689 || attempt.attempt_id != source.transport_attempt_id.as_str()
690 || attempt.transport_id != source.transport_identity.as_str()
691 || attempt.transport_key_epoch != source.transport_key_epoch
692 || invocation_blob.request_digest != source.request_binding_hash.as_str()
693 {
694 return Err(ToolOutcomeError::Binding(
695 "release_evidence.transport_source",
696 ));
697 }
698 Ok(())
699}
700
701fn validate_zero_charge_source(
702 bundle: &MonetaryReleaseEvidenceV1,
703 source: &ZeroChargeReleaseSourceV1,
704) -> Result<(), ToolOutcomeError> {
705 if source.operation_id != bundle.operation_id
706 || source.operation_version != bundle.operation_version
707 || source.verifier_policy_digest != bundle.verifier_policy_digest
708 || source.verifier_policy_digest != bundle.source_artifacts[2].digest
709 {
710 return Err(ToolOutcomeError::Binding(
711 "release_evidence.zero_charge_source",
712 ));
713 }
714 validate_zero_charge_records(source, &bundle.source_artifacts, None)
715}
716
717fn validate_zero_charge_records(
718 source: &ZeroChargeReleaseSourceV1,
719 artifacts: &[ImmutableReleaseArtifactV1],
720 operation: Option<&AdmissionOperationV1>,
721) -> Result<(), ToolOutcomeError> {
722 let outcome = ToolOutcomeRecordV1::from_persisted(parse_artifact_value(&artifacts[0])?)?;
723 let evaluation =
724 PostReturnEvaluationRecordV1::from_persisted(parse_artifact_value(&artifacts[1])?)?;
725 let _: VerifierPolicyArtifactV1 = parse_artifact_value(&artifacts[2])?;
726 if let Some(operation) = operation {
727 outcome.validate_against(operation)?;
728 evaluation.validate_against(operation, &outcome)?;
729 }
730 let ResolvedToolOutcomeV1::Resolved {
731 evaluation_id,
732 pricing_verdict_digest,
733 settlement_disposition: SettlementDispositionV1::ContractualZeroCharge { currency },
734 ..
735 } = &outcome.disposition
736 else {
737 return Err(ToolOutcomeError::Binding(
738 "release_evidence.zero_charge_disposition",
739 ));
740 };
741 if outcome.operation_id != source.operation_id
742 || outcome.request_id != source.request_id
743 || outcome.outcome_id != source.tool_outcome_id
744 || outcome.version != source.outcome_version
745 || outcome.recorded_at_unix_ms > source.verified_at_unix_ms
746 || evaluation.operation_id != source.operation_id
747 || evaluation.tool_outcome_id != source.tool_outcome_id
748 || evaluation.evaluation_id != source.evaluation_id
749 || evaluation.trusted_time_unix_ms > source.verified_at_unix_ms
750 || evaluation_id != &source.evaluation_id
751 || pricing_verdict_digest != &source.pricing_verdict_digest
752 || currency != &source.currency
753 {
754 return Err(ToolOutcomeError::Binding(
755 "release_evidence.zero_charge_records",
756 ));
757 }
758 Ok(())
759}