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},
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: ArtifactChecksumRecord::from_hash(&fields.plan_intent)?
55                .hash()
56                .into(),
57            guarantee: fields.guarantee,
58        })
59    }
60}
61impl ConsistencyRequirementRecord {
62    /// Declare a guarantee for the full original plan; does not establish consistency.
63    #[must_use]
64    pub fn new(plan: &OperationPlanRecord, guarantee: ConsistencyGuaranteeRecord) -> Self {
65        Self {
66            version: 1,
67            plan_intent: plan.digest().hash().into(),
68            guarantee,
69        }
70    }
71    /// Read canonical original full plan intent, including selection and original budgets.
72    #[must_use]
73    pub fn plan_intent(&self) -> &str {
74        &self.plan_intent
75    }
76    /// Read the immutable requested guarantee.
77    #[must_use]
78    pub const fn guarantee(&self) -> ConsistencyGuaranteeRecord {
79        self.guarantee
80    }
81    /// Match the exact original plan; declarations never upgrade a guarantee on recovery.
82    /// # Errors
83    /// Rejects a different full plan intent.
84    pub fn validate_plan(
85        &self,
86        plan: &OperationPlanRecord,
87    ) -> Result<(), ConsistencyRequirementError> {
88        if self.plan_intent != plan.digest().hash() {
89            return Err(ConsistencyRequirementError::PlanMismatch);
90        }
91        Ok(())
92    }
93    /// Hash NUL-terminated v1 ASCII domain, 64 ASCII original-intent bytes and guarantee tag.
94    ///
95    /// Tags are `per_canister=0` and `application_coordinated=1`. No current evidence,
96    /// timestamps or editable derived hashes are included or retained in this record.
97    #[must_use]
98    pub fn digest(&self) -> ArtifactChecksumRecord {
99        let mut bytes = b"ic-backup/consistency-requirement/v1\0".to_vec();
100        bytes.extend_from_slice(self.plan_intent.as_bytes());
101        bytes.push(self.guarantee.tag());
102        ArtifactChecksumRecord::from_bytes(&bytes)
103    }
104}
105/// Typed immutable declaration rejection; no error changes original authority.
106#[derive(Debug, Error)]
107pub enum ConsistencyRequirementError {
108    /// Only v1 is maintained.
109    #[error("unsupported consistency requirement version {0}")]
110    UnsupportedVersion(u16),
111    /// Requirement belongs to another full original plan.
112    #[error("consistency requirement original plan mismatch")]
113    PlanMismatch,
114    /// Original intent is not a canonicalizable SHA-256 value.
115    #[error(transparent)]
116    Checksum(#[from] ChecksumError),
117}