Skip to main content

ic_backup/policy/restore_safety/
mod.rs

1//! Pure same-source and current application safety matching; no IO, effects or settlement.
2
3use super::restore_fence::{self, RestoreFenceMismatch};
4use crate::model::{
5    artifacts::ArtifactChecksumRecord,
6    ic_request::IcManagementMethodRecord,
7    restore_safety::{
8        RestoreFenceEvidence, RestoreSafetyEvidence, RestoreSafetyLaneRecord,
9        RestoreSafetyObservation, RestoreSafetyRequest, RestoreSafetyRequirementRecord,
10        TargetRestoreEvidence,
11    },
12};
13use ic_platform::management_canister_types::CanisterStatusType;
14use thiserror::Error;
15
16/// Read-only matched current safety; never a load/start, spending or release permit.
17#[derive(Clone, Debug)]
18pub struct RestoreSafetyView<'a> {
19    request: ArtifactChecksumRecord,
20    requirement: ArtifactChecksumRecord,
21    observation: &'a RestoreSafetyObservation,
22}
23impl RestoreSafetyView<'_> {
24    /// Read exact current source/operation/challenge-bound request identity.
25    #[must_use]
26    pub const fn request(&self) -> &ArtifactChecksumRecord {
27        &self.request
28    }
29    /// Read original retained source/safety requirement identity.
30    #[must_use]
31    pub const fn requirement(&self) -> &ArtifactChecksumRecord {
32        &self.requirement
33    }
34    /// Read canonical actual selected lifecycle and restored-acceptance rows.
35    #[must_use]
36    pub fn targets(&self) -> &[TargetRestoreEvidence] {
37        self.observation.targets()
38    }
39    /// Read current matched fence, when required; never a release token.
40    #[must_use]
41    pub fn fence(&self) -> Option<&RestoreFenceEvidence> {
42        match self.observation.safety() {
43            RestoreSafetyEvidence::ApplicationFenced(fence) => Some(fence),
44            RestoreSafetyEvidence::NoIrreversibleEffects(_)
45            | RestoreSafetyEvidence::Unresolved(_) => None,
46        }
47    }
48    /// Read opaque current evidence identity.
49    #[must_use]
50    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
51        self.observation.evidence()
52    }
53    /// Read descriptive actual calls; not prior accounting or fresh allowance.
54    #[must_use]
55    pub const fn remote_observations(&self) -> u32 {
56        self.observation.remote_observations()
57    }
58}
59/// Match original source/lane/fence, actual full context/inventory/selection and lifecycle.
60///
61/// Before load all selected targets must be stopped. Before start its exact
62/// target must be stopped and every selected target must have source-specific
63/// application acceptance. Other targets may already be running under the
64/// integration's controlled-execution contract; a still-stopping member rejects.
65/// Canonical order supplies no
66/// dispatch order. The original graph and actual receipts are separate admission.
67///
68/// Integrations qualify actual freshness/source custody/drain, irreversible-work
69/// absence or outside-snapshot fence custody/replay safety. Hash equality cannot
70/// prove those properties or load completion. Policy invokes no provider, performs
71/// no IO/serialization and changes no journal, fence, allowance or reference.
72/// # Errors
73/// Rejects changed request/context/inventory/source/selection/lane/revisions,
74/// excessive calls, unresolved obligations, inactive fences or absent start evidence.
75pub fn validate<'a>(
76    request: &RestoreSafetyRequest<'_>,
77    observation: &'a RestoreSafetyObservation,
78) -> Result<RestoreSafetyView<'a>, RestoreSafetyError> {
79    let digest = request.digest();
80    let binding = request.binding();
81    super::observed_context::admit_request_context(
82        &digest,
83        observation.request(),
84        binding,
85        observation.context(),
86        RestoreSafetyError::RequestMismatch,
87        RestoreSafetyError::ContextMismatch,
88    )?;
89    if observation.inventory() != request.inventory() {
90        return Err(RestoreSafetyError::InventoryMismatch);
91    }
92    if !observation
93        .targets()
94        .iter()
95        .map(|target| &target.target)
96        .eq(request.selected_targets())
97    {
98        return Err(RestoreSafetyError::SelectionMismatch);
99    }
100    let requirement = request.requirement();
101    if observation.source_plan_intent().hash() != requirement.source_plan_intent() {
102        return Err(RestoreSafetyError::SourcePlanMismatch);
103    }
104    if observation.source_artifacts() != requirement.source_artifacts() {
105        return Err(RestoreSafetyError::SourceArtifactsMismatch);
106    }
107    super::observed_context::admit_limit(
108        request.max_remote_observations(),
109        observation.remote_observations(),
110        |limit, reported| RestoreSafetyError::ObservationLimitExceeded { limit, reported },
111    )?;
112    let method = request.wire().method();
113    validate_targets(method, binding.target(), observation.targets())?;
114    validate_safety(requirement, observation.safety(), method)?;
115    Ok(RestoreSafetyView {
116        request: digest,
117        requirement: requirement.digest(),
118        observation,
119    })
120}
121// Load requires the complete selected set stopped. Start allows previously
122// restored peers to run, but requires acceptance for the complete selected set.
123fn validate_targets(
124    method: IcManagementMethodRecord,
125    current_target: &str,
126    targets: &[TargetRestoreEvidence],
127) -> Result<(), RestoreSafetyError> {
128    if !targets.iter().all(|target| match method {
129        IcManagementMethodRecord::StartCanister if target.target != current_target => {
130            target.state != CanisterStatusType::Stopping
131        }
132        _ => target.state == CanisterStatusType::Stopped,
133    }) {
134        return Err(RestoreSafetyError::TargetNotStopped);
135    }
136    if method == IcManagementMethodRecord::StartCanister
137        && targets
138            .iter()
139            .any(|target| target.restored_acceptance.is_none())
140    {
141        return Err(RestoreSafetyError::RestoredAcceptanceRequired);
142    }
143    Ok(())
144}
145
146fn validate_safety(
147    requirement: &RestoreSafetyRequirementRecord,
148    evidence: &RestoreSafetyEvidence,
149    method: IcManagementMethodRecord,
150) -> Result<(), RestoreSafetyError> {
151    match (requirement.safety(), evidence) {
152        (_, RestoreSafetyEvidence::Unresolved(_)) => {
153            return Err(RestoreSafetyError::UnresolvedExternalObligations);
154        }
155        (
156            RestoreSafetyLaneRecord::NoIrreversibleEffects,
157            RestoreSafetyEvidence::NoIrreversibleEffects(_),
158        ) => {}
159        (
160            RestoreSafetyLaneRecord::ApplicationFenced,
161            RestoreSafetyEvidence::ApplicationFenced(fence),
162        ) => {
163            let original = requirement
164                .expected_fence()
165                .ok_or(RestoreSafetyError::FenceMismatch)?;
166            restore_fence::validate(original, fence).map_err(|mismatch| match mismatch {
167                RestoreFenceMismatch::Identity => RestoreSafetyError::FenceMismatch,
168                RestoreFenceMismatch::MembershipRevision => {
169                    RestoreSafetyError::MembershipRevisionMismatch
170                }
171                RestoreFenceMismatch::ExternalObligationsRevision => {
172                    RestoreSafetyError::ExternalObligationsRevisionMismatch
173                }
174                RestoreFenceMismatch::Inactive => RestoreSafetyError::FenceNotActive,
175            })?;
176            if method == IcManagementMethodRecord::StartCanister
177                && fence.controlled_execution.is_none()
178            {
179                return Err(RestoreSafetyError::ControlledExecutionRequired);
180            }
181        }
182        _ => return Err(RestoreSafetyError::SafetyLaneMismatch),
183    }
184    Ok(())
185}
186/// Typed fresh safety rejection; retains original spending, obligations and source references.
187#[derive(Debug, Eq, Error, PartialEq)]
188pub enum RestoreSafetyError {
189    /// Original request, source, safety, operation or challenge differs.
190    #[error("restore safety request mismatch")]
191    RequestMismatch,
192    /// Actual current context differs from original restore context.
193    #[error("restore safety observed {0} mismatch")]
194    ContextMismatch(&'static str),
195    /// Actual complete inventory differs, including unselected metadata.
196    #[error("restore safety inventory mismatch")]
197    InventoryMismatch,
198    /// Actual rows do not equal the exact original selected set.
199    #[error("restore safety selection mismatch")]
200    SelectionMismatch,
201    /// Observed source plan differs from original retention.
202    #[error("restore safety source plan mismatch")]
203    SourcePlanMismatch,
204    /// Observed artifact binding differs from original retention.
205    #[error("restore safety source artifacts mismatch")]
206    SourceArtifactsMismatch,
207    /// Current application evidence changes the original lane.
208    #[error("restore safety evidence lane mismatch")]
209    SafetyLaneMismatch,
210    /// External obligations/replayed work are known unresolved or unsafe.
211    #[error("restore safety external obligations unresolved")]
212    UnresolvedExternalObligations,
213    /// Required stopped/drained lifecycle has not been observed.
214    #[error("restore safety target is not stopped")]
215    TargetNotStopped,
216    /// Every selected restored state needs application acceptance before start.
217    #[error("restore safety restored-state acceptance required")]
218    RestoredAcceptanceRequired,
219    /// Exact retained fence identity differs.
220    #[error("restore safety fence identity mismatch")]
221    FenceMismatch,
222    /// Original membership revision changed; never silently rebind a fence.
223    #[error("restore safety membership revision mismatch")]
224    MembershipRevisionMismatch,
225    /// Original external-obligation authority revision changed.
226    #[error("restore safety external-obligations revision mismatch")]
227    ExternalObligationsRevisionMismatch,
228    /// Retained exact fence is currently known inactive.
229    #[error("restore safety fence not active")]
230    FenceNotActive,
231    /// Isolated execution under retained custody has not been qualified before start.
232    #[error("restore safety controlled execution evidence required")]
233    ControlledExecutionRequired,
234    /// Call reporting exceeds the descriptive ceiling.
235    #[error("restore safety reports {reported} observations above ceiling {limit}")]
236    ObservationLimitExceeded {
237        /// Original descriptive ceiling; no allowance.
238        limit: u32,
239        /// Actual reported calls.
240        reported: u32,
241    },
242}
243
244#[cfg(test)]
245mod tests;