Skip to main content

ic_backup/model/consistency/requirement/
mod.rs

1//! Immutable declared consistency choice; never current fence evidence.
2
3use crate::model::{
4    artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
5    operation_plan::OperationPlanRecord,
6};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10/// Maximum raw input and canonical output bytes for a retained requirement.
11pub const MAX_CONSISTENCY_REQUIREMENT_BYTES: u64 = 1024;
12/// Explicit maintained consistency declaration; neither choice is proven by a record.
13#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ConsistencyGuaranteeRecord {
16    /// Each stopped target is separately qualified; no atomic application checkpoint.
17    PerCanister,
18    /// Application owns a retained whole-selection fence and drained-work proof.
19    ApplicationCoordinated,
20}
21impl ConsistencyGuaranteeRecord {
22    pub(super) const fn tag(self) -> u8 {
23        match self {
24            Self::PerCanister => 0,
25            Self::ApplicationCoordinated => 1,
26        }
27    }
28}
29/// Immutable v1 original-plan-bound requested guarantee, separate from fresh observations.
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(try_from = "RequirementFields")]
32pub struct ConsistencyRequirementRecord {
33    version: u16,
34    plan_intent: String,
35    guarantee: ConsistencyGuaranteeRecord,
36}
37#[derive(Deserialize)]
38#[serde(deny_unknown_fields)]
39struct RequirementFields {
40    version: u16,
41    plan_intent: String,
42    guarantee: ConsistencyGuaranteeRecord,
43}
44impl TryFrom<RequirementFields> for ConsistencyRequirementRecord {
45    type Error = ConsistencyRequirementError;
46    fn try_from(fields: RequirementFields) -> Result<Self, Self::Error> {
47        if fields.version != 1 {
48            return Err(ConsistencyRequirementError::UnsupportedVersion(
49                fields.version,
50            ));
51        }
52        Ok(Self {
53            version: 1,
54            plan_intent: canonical_hash(&fields.plan_intent)?,
55            guarantee: fields.guarantee,
56        })
57    }
58}
59impl ConsistencyRequirementRecord {
60    /// Declare a guarantee for the full original plan; does not establish consistency.
61    #[must_use]
62    pub fn new(plan: &OperationPlanRecord, guarantee: ConsistencyGuaranteeRecord) -> Self {
63        Self {
64            version: 1,
65            plan_intent: plan.digest().hash().into(),
66            guarantee,
67        }
68    }
69    /// Read canonical original full plan intent, including selection and original budgets.
70    #[must_use]
71    pub fn plan_intent(&self) -> &str {
72        &self.plan_intent
73    }
74    /// Read the immutable requested guarantee.
75    #[must_use]
76    pub const fn guarantee(&self) -> ConsistencyGuaranteeRecord {
77        self.guarantee
78    }
79    /// Match the exact original plan; declarations never upgrade a guarantee on recovery.
80    /// # Errors
81    /// Rejects a different full plan intent.
82    pub fn validate_plan(
83        &self,
84        plan: &OperationPlanRecord,
85    ) -> Result<(), ConsistencyRequirementError> {
86        if self.plan_intent != plan.digest().hash() {
87            return Err(ConsistencyRequirementError::PlanMismatch);
88        }
89        Ok(())
90    }
91    /// Hash NUL-terminated v1 ASCII domain, 64 ASCII original-intent bytes and guarantee tag.
92    ///
93    /// Tags are `per_canister=0` and `application_coordinated=1`. No current evidence,
94    /// timestamps or editable derived hashes are included or retained in this record.
95    #[must_use]
96    pub fn digest(&self) -> ArtifactChecksumRecord {
97        let mut bytes = b"ic-backup/consistency-requirement/v1\0".to_vec();
98        bytes.extend_from_slice(self.plan_intent.as_bytes());
99        bytes.push(self.guarantee.tag());
100        ArtifactChecksumRecord::from_bytes(&bytes)
101    }
102}
103/// Typed immutable declaration rejection; no error changes original authority.
104#[derive(Debug, Error)]
105pub enum ConsistencyRequirementError {
106    /// Only v1 is maintained.
107    #[error("unsupported consistency requirement version {0}")]
108    UnsupportedVersion(u16),
109    /// Requirement belongs to another full original plan.
110    #[error("consistency requirement original plan mismatch")]
111    PlanMismatch,
112    /// Original intent is not a canonicalizable SHA-256 value.
113    #[error(transparent)]
114    Checksum(#[from] ChecksumError),
115}