lib

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

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 }