delivery_reconciliation.rs (4369B)
1 //! Current-authority command for reconciling already retained delivery facts. 2 3 use super::{AuthoredAtomicCommand, RecordDeliveryFact, WorkFence}; 4 use crate::{ 5 Error, 6 authored::RetrySchedule, 7 authored_delivery::{AuthoredDeliveryHistory, AuthoredDeliveryPlan, AuthoredDeliveryPlanId}, 8 }; 9 use core::num::NonZeroU64; 10 use sha2::{Digest, Sha256}; 11 12 #[derive(Clone, Debug, Eq, PartialEq)] 13 pub struct ReconcileDeliveryFacts { 14 plan_id: AuthoredDeliveryPlanId, 15 revision: NonZeroU64, 16 facts_digest: [u8; 32], 17 fence: Option<WorkFence>, 18 retry: Option<RetrySchedule>, 19 reconciled_at_unix_ms: u64, 20 } 21 22 impl ReconcileDeliveryFacts { 23 pub fn new( 24 plan: &AuthoredDeliveryPlan, 25 fence: Option<WorkFence>, 26 retry: Option<RetrySchedule>, 27 reconciled_at_unix_ms: u64, 28 ) -> Result<Self, Error> { 29 plan.validate()?; 30 if reconciled_at_unix_ms == 0 || plan.pending_delivery_facts().next().is_none() { 31 return Err(Error::AtomicWorkflowMismatch); 32 } 33 Ok(Self { 34 plan_id: plan.plan_id(), 35 revision: plan.revision(), 36 facts_digest: facts_digest(plan)?, 37 fence, 38 retry, 39 reconciled_at_unix_ms, 40 }) 41 } 42 pub const fn plan_id(&self) -> AuthoredDeliveryPlanId { 43 self.plan_id 44 } 45 pub const fn revision(&self) -> NonZeroU64 { 46 self.revision 47 } 48 pub const fn reconciled_at_unix_ms(&self) -> u64 { 49 self.reconciled_at_unix_ms 50 } 51 /// Backends supply their own consistent original-claim history. 52 pub fn apply_to( 53 &self, 54 history: &AuthoredDeliveryHistory, 55 ) -> Result<AuthoredDeliveryPlan, Error> { 56 history.require_pending_fact_provenance()?; 57 let mut plan = history.plan().clone(); 58 if plan.plan_id() != self.plan_id 59 || plan.revision() != self.revision 60 || facts_digest(&plan)? != self.facts_digest 61 { 62 return Err(Error::DeliveryPlanClaimConflict); 63 } 64 plan.reconcile_delivery_facts( 65 self.fence.as_ref(), 66 self.retry.clone(), 67 self.reconciled_at_unix_ms, 68 history.claims(), 69 )?; 70 Ok(plan) 71 } 72 73 pub(super) fn matches_plan(&self, plan: &AuthoredDeliveryPlan) -> bool { 74 plan.plan_id() == self.plan_id 75 && self.revision.get().checked_add(1) == Some(plan.revision().get()) 76 && plan.updated_at_unix_ms() == self.reconciled_at_unix_ms 77 && plan.claim_evidence().is_none() 78 && plan.stop_requested_at_unix_ms().is_none() 79 && plan.pending_delivery_facts().next().is_none() 80 && facts_digest(plan).ok() == Some(self.facts_digest) 81 && plan.retry() == self.retry.as_ref() 82 } 83 84 pub(super) fn hash_into(&self, hasher: &mut Sha256) { 85 hasher.update(self.revision.get().to_be_bytes()); 86 hasher.update(self.facts_digest); 87 hasher.update(self.reconciled_at_unix_ms.to_be_bytes()); 88 hasher.update([self.fence.is_some() as u8]); 89 if let Some(fence) = &self.fence { 90 hasher.update(fence.token()); 91 hasher.update(fence.generation().get().to_be_bytes()); 92 hasher.update(fence.row_revision().get().to_be_bytes()); 93 } 94 hasher.update([self.retry.is_some() as u8]); 95 if let Some(retry) = &self.retry { 96 hasher.update(retry.attempt().get().to_be_bytes()); 97 hasher.update(retry.not_before_unix_ms().to_be_bytes()); 98 super::hash_failure(hasher, Some(retry.failure())); 99 } 100 } 101 } 102 103 fn facts_digest(plan: &AuthoredDeliveryPlan) -> Result<[u8; 32], Error> { 104 let mut hasher = Sha256::new(); 105 super::hash_field(&mut hasher, b"radroots.authored.delivery-facts.v1"); 106 hasher.update(plan.plan_id().as_bytes()); 107 hasher.update(plan.artifact_id().as_bytes()); 108 for fact in plan.delivery_facts() { 109 let command = AuthoredAtomicCommand::RecordDelivery(RecordDeliveryFact::new( 110 plan.plan_id(), 111 plan.artifact_id(), 112 fact.claim().clone(), 113 fact.outcome().clone(), 114 fact.observed_at_unix_ms(), 115 )?); 116 hasher.update(command.digest().as_bytes()); 117 hasher.update(fact.observed_at_unix_ms().to_be_bytes()); 118 } 119 Ok(hasher.finalize().into()) 120 }