Skip to main content

ic_backup/policy/consistency/
mod.rs

1//! Pure original-guarantee and current stopped/drained/fence matching; no effects.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    consistency::{
6        ApplicationFenceEvidence, ApplicationFenceState, CaptureState, ConsistencyEvidence,
7        ConsistencyGuaranteeRecord, ConsistencyObservation, ConsistencyRequest,
8        TargetCaptureEvidence,
9    },
10};
11use thiserror::Error;
12
13/// Read-only matched current evidence; no capture, restart, release or spending permit.
14#[derive(Clone, Debug)]
15pub struct ConsistencyView<'a> {
16    request: ArtifactChecksumRecord,
17    requirement: ArtifactChecksumRecord,
18    observation: &'a ConsistencyObservation,
19}
20impl ConsistencyView<'_> {
21    /// Read exact challenge/boundary/fence-bound current request digest.
22    #[must_use]
23    pub const fn request(&self) -> &ArtifactChecksumRecord {
24        &self.request
25    }
26    /// Read immutable original consistency requirement digest.
27    #[must_use]
28    pub const fn requirement(&self) -> &ArtifactChecksumRecord {
29        &self.requirement
30    }
31    /// Read matched exact selected stopped/drained target evidence.
32    #[must_use]
33    pub fn targets(&self) -> &[TargetCaptureEvidence] {
34        self.observation.targets()
35    }
36    /// Read matched active application fence when the original guarantee requires one.
37    #[must_use]
38    pub fn fence(&self) -> Option<&ApplicationFenceEvidence> {
39        match self.observation.consistency() {
40            ConsistencyEvidence::PerCanister => None,
41            ConsistencyEvidence::ApplicationCoordinated(fence) => Some(fence),
42        }
43    }
44    /// Read opaque integration-qualified current evidence.
45    #[must_use]
46    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
47        self.observation.evidence()
48    }
49    /// Read actual call reporting; grants no paid-call admission/replenishment.
50    #[must_use]
51    pub const fn remote_observations(&self) -> u32 {
52        self.observation.remote_observations()
53    }
54}
55/// Match original guarantee, current context/inventory/selection/stopped state and exact fence.
56///
57/// Integrations qualify evidence authenticity, actual freshness/drain and continuous
58/// fence custody. Policy cannot prove these through hashes, echoed labels or matching
59/// before/after observations. It performs no IO/serialization/provider calls, plan or
60/// journal changes, dispatch, restart or fence release. Restore safety remains separate.
61/// # Errors
62/// Rejects changed request/context/inventory/selection/lane, non-stopped targets,
63/// excess calls, changed retained fence identity or unbound membership revision.
64pub fn validate<'a>(
65    request: &ConsistencyRequest<'_>,
66    observation: &'a ConsistencyObservation,
67) -> Result<ConsistencyView<'a>, ConsistencyError> {
68    let digest = request.digest();
69    if *observation.request() != digest {
70        return Err(ConsistencyError::RequestMismatch);
71    }
72    let binding = request.binding();
73    for (field, expected, actual) in [
74        (
75            "network",
76            binding.network(),
77            observation.context().network(),
78        ),
79        ("caller", binding.caller(), observation.context().caller()),
80        (
81            "release",
82            binding.release(),
83            observation.context().release(),
84        ),
85    ] {
86        if actual != expected {
87            return Err(ConsistencyError::ContextMismatch(field));
88        }
89    }
90    if observation.inventory() != request.inventory() {
91        return Err(ConsistencyError::InventoryMismatch);
92    }
93    if !observation
94        .targets()
95        .iter()
96        .map(|target| &target.target)
97        .eq(request.selected_targets())
98    {
99        return Err(ConsistencyError::SelectionMismatch);
100    }
101    if observation.remote_observations() > request.max_remote_observations() {
102        return Err(ConsistencyError::ObservationLimitExceeded {
103            limit: request.max_remote_observations(),
104            reported: observation.remote_observations(),
105        });
106    }
107    if observation
108        .targets()
109        .iter()
110        .any(|target| target.state != CaptureState::Stopped)
111    {
112        return Err(ConsistencyError::TargetNotStopped);
113    }
114    match (request.requirement().guarantee(), observation.consistency()) {
115        (ConsistencyGuaranteeRecord::PerCanister, ConsistencyEvidence::PerCanister) => {}
116        (
117            ConsistencyGuaranteeRecord::ApplicationCoordinated,
118            ConsistencyEvidence::ApplicationCoordinated(fence),
119        ) => {
120            let expected = request
121                .expected_fence()
122                .ok_or(ConsistencyError::FenceMismatch)?;
123            if fence.identity != expected.identity {
124                return Err(ConsistencyError::FenceMismatch);
125            }
126            if fence.state != ApplicationFenceState::Active {
127                return Err(ConsistencyError::FenceNotActive);
128            }
129            let original_revision_matches =
130                observation.membership_revision() == Some(&expected.membership_revision);
131            let fence_revision_matches = fence.membership_revision == expected.membership_revision;
132            if !original_revision_matches || !fence_revision_matches {
133                return Err(ConsistencyError::MembershipRevisionMismatch);
134            }
135        }
136        _ => return Err(ConsistencyError::GuaranteeMismatch),
137    }
138    Ok(ConsistencyView {
139        request: digest,
140        requirement: request.requirement().digest(),
141        observation,
142    })
143}
144/// Typed current evidence denial; no failure releases fences or replenishes allowances.
145#[derive(Debug, Eq, Error, PartialEq)]
146pub enum ConsistencyError {
147    /// Original requirement/operation/challenge/boundary/fence/ceiling differs.
148    #[error("consistency request mismatch")]
149    RequestMismatch,
150    /// Actually observed context differs.
151    #[error("consistency observed {0} mismatch")]
152    ContextMismatch(&'static str),
153    /// Complete actually observed inventory differs, including unselected parent metadata.
154    #[error("consistency inventory mismatch")]
155    InventoryMismatch,
156    /// Actual target evidence differs from the exact original selected set.
157    #[error("consistency selected targets mismatch")]
158    SelectionMismatch,
159    /// Evidence cannot upgrade or downgrade the original declared guarantee.
160    #[error("consistency evidence guarantee mismatch")]
161    GuaranteeMismatch,
162    /// At least one actual target is running or still stopping.
163    #[error("consistency target is not stopped")]
164    TargetNotStopped,
165    /// Actual fence differs from the retained exact obligation.
166    #[error("consistency fence identity mismatch")]
167    FenceMismatch,
168    /// Current exact fence is known inactive; retain the obligation and deny capture.
169    #[error("consistency fence is not active")]
170    FenceNotActive,
171    /// Missing/changed actual revision is not bound to the application fence.
172    #[error("consistency fence membership revision mismatch")]
173    MembershipRevisionMismatch,
174    /// Actual call reporting exceeds the descriptive ceiling.
175    #[error("consistency reports {reported} observations above ceiling {limit}")]
176    ObservationLimitExceeded {
177        /// Original descriptive ceiling, separate from allowances.
178        limit: u32,
179        /// Reported actual calls.
180        reported: u32,
181    },
182}
183
184#[cfg(test)]
185mod tests;