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 crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    consistency::ApplicationFenceState,
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            if fence.binding.identity != original.identity {
167                return Err(RestoreSafetyError::FenceMismatch);
168            }
169            if fence.binding.membership_revision != original.membership_revision {
170                return Err(RestoreSafetyError::MembershipRevisionMismatch);
171            }
172            if fence.binding.external_obligations_revision != original.external_obligations_revision
173            {
174                return Err(RestoreSafetyError::ExternalObligationsRevisionMismatch);
175            }
176            if fence.state != ApplicationFenceState::Active {
177                return Err(RestoreSafetyError::FenceNotActive);
178            }
179            if method == IcManagementMethodRecord::StartCanister
180                && fence.controlled_execution.is_none()
181            {
182                return Err(RestoreSafetyError::ControlledExecutionRequired);
183            }
184        }
185        _ => return Err(RestoreSafetyError::SafetyLaneMismatch),
186    }
187    Ok(())
188}
189/// Typed fresh safety rejection; retains original spending, obligations and source references.
190#[derive(Debug, Eq, Error, PartialEq)]
191pub enum RestoreSafetyError {
192    /// Original request, source, safety, operation or challenge differs.
193    #[error("restore safety request mismatch")]
194    RequestMismatch,
195    /// Actual current context differs from original restore context.
196    #[error("restore safety observed {0} mismatch")]
197    ContextMismatch(&'static str),
198    /// Actual complete inventory differs, including unselected metadata.
199    #[error("restore safety inventory mismatch")]
200    InventoryMismatch,
201    /// Actual rows do not equal the exact original selected set.
202    #[error("restore safety selection mismatch")]
203    SelectionMismatch,
204    /// Observed source plan differs from original retention.
205    #[error("restore safety source plan mismatch")]
206    SourcePlanMismatch,
207    /// Observed artifact binding differs from original retention.
208    #[error("restore safety source artifacts mismatch")]
209    SourceArtifactsMismatch,
210    /// Current application evidence changes the original lane.
211    #[error("restore safety evidence lane mismatch")]
212    SafetyLaneMismatch,
213    /// External obligations/replayed work are known unresolved or unsafe.
214    #[error("restore safety external obligations unresolved")]
215    UnresolvedExternalObligations,
216    /// Required stopped/drained lifecycle has not been observed.
217    #[error("restore safety target is not stopped")]
218    TargetNotStopped,
219    /// Every selected restored state needs application acceptance before start.
220    #[error("restore safety restored-state acceptance required")]
221    RestoredAcceptanceRequired,
222    /// Exact retained fence identity differs.
223    #[error("restore safety fence identity mismatch")]
224    FenceMismatch,
225    /// Original membership revision changed; never silently rebind a fence.
226    #[error("restore safety membership revision mismatch")]
227    MembershipRevisionMismatch,
228    /// Original external-obligation authority revision changed.
229    #[error("restore safety external-obligations revision mismatch")]
230    ExternalObligationsRevisionMismatch,
231    /// Retained exact fence is currently known inactive.
232    #[error("restore safety fence not active")]
233    FenceNotActive,
234    /// Isolated execution under retained custody has not been qualified before start.
235    #[error("restore safety controlled execution evidence required")]
236    ControlledExecutionRequired,
237    /// Call reporting exceeds the descriptive ceiling.
238    #[error("restore safety reports {reported} observations above ceiling {limit}")]
239    ObservationLimitExceeded {
240        /// Original descriptive ceiling; no allowance.
241        limit: u32,
242        /// Actual reported calls.
243        reported: u32,
244    },
245}
246
247#[cfg(test)]
248mod tests;