Skip to main content

ic_backup/model/consistency/requirement/
mod.rs

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