lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }