delivery_evidence.rs (5255B)
1 //! Delivery facts authorized by the backend's original immutable claim receipt. 2 3 use super::{ 4 AuthoredAtomicCommand, AuthoredAtomicOutcome, AuthoredAtomicReceipt, ClaimAuthoredTarget, 5 ClaimAuthoredWork, 6 }; 7 use crate::{ 8 Error, 9 authored::{AuthoredArtifactId, WorkClaim}, 10 authored_delivery::{AuthoredDeliveryPlan, AuthoredDeliveryPlanId, DeliveryAttemptOutcome}, 11 }; 12 13 /// Retains a completed sink result without extending a lease or granting work. 14 #[derive(Clone, Debug, Eq, PartialEq)] 15 pub struct RecordDeliveryFact { 16 plan_id: AuthoredDeliveryPlanId, 17 artifact_id: AuthoredArtifactId, 18 claim: WorkClaim, 19 outcome: DeliveryAttemptOutcome, 20 observed_at_unix_ms: u64, 21 } 22 23 impl RecordDeliveryFact { 24 pub fn new( 25 plan_id: AuthoredDeliveryPlanId, 26 artifact_id: AuthoredArtifactId, 27 claim: WorkClaim, 28 outcome: DeliveryAttemptOutcome, 29 observed_at_unix_ms: u64, 30 ) -> Result<Self, Error> { 31 claim.validate()?; 32 if observed_at_unix_ms < claim.acquired_at_unix_ms() { 33 return Err(Error::AtomicWorkflowMismatch); 34 } 35 Ok(Self { 36 plan_id, 37 artifact_id, 38 claim, 39 outcome, 40 observed_at_unix_ms, 41 }) 42 } 43 44 pub const fn plan_id(&self) -> AuthoredDeliveryPlanId { 45 self.plan_id 46 } 47 pub const fn artifact_id(&self) -> AuthoredArtifactId { 48 self.artifact_id 49 } 50 pub const fn claim(&self) -> &WorkClaim { 51 &self.claim 52 } 53 pub const fn outcome(&self) -> &DeliveryAttemptOutcome { 54 &self.outcome 55 } 56 pub const fn observed_at_unix_ms(&self) -> u64 { 57 self.observed_at_unix_ms 58 } 59 60 pub fn claim_command(&self) -> AuthoredAtomicCommand { 61 AuthoredAtomicCommand::Claim(ClaimAuthoredWork::new( 62 ClaimAuthoredTarget::DeliveryPlan(self.plan_id), 63 self.claim.clone(), 64 )) 65 } 66 67 /// The backend must retrieve `original_claim` inside its own transaction. 68 /// Caller-provided receipts must never substitute for persisted provenance. 69 pub fn apply_to( 70 &self, 71 plan: &mut AuthoredDeliveryPlan, 72 original_claim: &AuthoredAtomicReceipt, 73 ) -> Result<(), Error> { 74 let AuthoredAtomicOutcome::DeliveryPlan(original) = original_claim.outcome() else { 75 return Err(Error::AtomicWorkflowMismatch); 76 }; 77 original.validate()?; 78 plan.validate()?; 79 if !original_claim.matches_command(&self.claim_command()) 80 || original_claim.committed_at_unix_ms() != self.claim.acquired_at_unix_ms() 81 || original.claim_evidence() != Some(&self.claim) 82 || original.plan_id() != self.plan_id 83 || plan.plan_id() != self.plan_id 84 || original.artifact_id() != self.artifact_id 85 || plan.artifact_id() != self.artifact_id 86 || original.request().is_none() 87 || original.request() != plan.request() 88 || original.intent() != plan.intent() 89 || original.created_at_unix_ms() != plan.created_at_unix_ms() 90 || original.revision() > plan.revision() 91 || original.updated_at_unix_ms() > plan.updated_at_unix_ms() 92 { 93 return Err(Error::AtomicWorkflowMismatch); 94 } 95 plan.record_delivery_fact( 96 self.claim.clone(), 97 self.outcome.clone(), 98 self.observed_at_unix_ms, 99 ) 100 } 101 102 pub(super) fn hash_outcome(&self, hasher: &mut sha2::Sha256) { 103 use sha2::Digest; 104 let entries = match &self.outcome { 105 DeliveryAttemptOutcome::Receipt(receipt) => { 106 hasher.update([0]); 107 super::hash_field(hasher, receipt.request_id().as_str().as_bytes()); 108 receipt.target_receipts() 109 } 110 DeliveryAttemptOutcome::SinkFailure(failure) => { 111 hasher.update([1]); 112 hasher.update([failure.retry_after_unix_ms().is_some() as u8]); 113 hasher.update( 114 failure 115 .retry_after_unix_ms() 116 .unwrap_or_default() 117 .to_be_bytes(), 118 ); 119 hash_optional(hasher, failure.message()); 120 failure.partial_evidence() 121 } 122 }; 123 hasher.update( 124 u64::try_from(entries.len()) 125 .unwrap_or(u64::MAX) 126 .to_be_bytes(), 127 ); 128 super::hash_delivery(hasher, &self.outcome); 129 for entry in entries { 130 hash_optional(hasher, entry.target().label().map(|label| label.as_str())); 131 } 132 } 133 134 pub(super) fn matches_plan(&self, plan: &AuthoredDeliveryPlan) -> bool { 135 plan.plan_id() == self.plan_id 136 && plan.artifact_id() == self.artifact_id 137 && plan 138 .delivery_facts() 139 .iter() 140 .any(|fact| fact.claim() == &self.claim && fact.outcome() == &self.outcome) 141 } 142 } 143 144 fn hash_optional(hasher: &mut sha2::Sha256, value: Option<&str>) { 145 use sha2::Digest; 146 hasher.update([value.is_some() as u8]); 147 if let Some(value) = value { 148 super::hash_field(hasher, value.as_bytes()); 149 } 150 }