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 before_start = request.wire().method() == IcManagementMethodRecord::StartCanister;
113    let lifecycle_matches = observation.targets().iter().all(|target| {
114        if before_start && target.target != binding.target() {
115            target.state != CanisterStatusType::Stopping
116        } else {
117            target.state == CanisterStatusType::Stopped
118        }
119    });
120    if !lifecycle_matches {
121        return Err(RestoreSafetyError::TargetNotStopped);
122    }
123    if before_start
124        && observation
125            .targets()
126            .iter()
127            .any(|target| target.restored_acceptance.is_none())
128    {
129        return Err(RestoreSafetyError::RestoredAcceptanceRequired);
130    }
131    validate_safety(requirement, observation.safety(), before_start)?;
132    Ok(RestoreSafetyView {
133        request: digest,
134        requirement: requirement.digest(),
135        observation,
136    })
137}
138fn validate_safety(
139    requirement: &RestoreSafetyRequirementRecord,
140    evidence: &RestoreSafetyEvidence,
141    before_start: bool,
142) -> Result<(), RestoreSafetyError> {
143    match (requirement.safety(), evidence) {
144        (_, RestoreSafetyEvidence::Unresolved(_)) => {
145            return Err(RestoreSafetyError::UnresolvedExternalObligations);
146        }
147        (
148            RestoreSafetyLaneRecord::NoIrreversibleEffects,
149            RestoreSafetyEvidence::NoIrreversibleEffects(_),
150        ) => {}
151        (
152            RestoreSafetyLaneRecord::ApplicationFenced,
153            RestoreSafetyEvidence::ApplicationFenced(fence),
154        ) => {
155            let original = requirement
156                .expected_fence()
157                .ok_or(RestoreSafetyError::FenceMismatch)?;
158            if fence.binding.identity != original.identity {
159                return Err(RestoreSafetyError::FenceMismatch);
160            }
161            if fence.binding.membership_revision != original.membership_revision {
162                return Err(RestoreSafetyError::MembershipRevisionMismatch);
163            }
164            if fence.binding.external_obligations_revision != original.external_obligations_revision
165            {
166                return Err(RestoreSafetyError::ExternalObligationsRevisionMismatch);
167            }
168            if fence.state != ApplicationFenceState::Active {
169                return Err(RestoreSafetyError::FenceNotActive);
170            }
171            if before_start && fence.controlled_execution.is_none() {
172                return Err(RestoreSafetyError::ControlledExecutionRequired);
173            }
174        }
175        _ => return Err(RestoreSafetyError::SafetyLaneMismatch),
176    }
177    Ok(())
178}
179/// Typed fresh safety rejection; retains original spending, obligations and source references.
180#[derive(Debug, Eq, Error, PartialEq)]
181pub enum RestoreSafetyError {
182    /// Original request, source, safety, operation or challenge differs.
183    #[error("restore safety request mismatch")]
184    RequestMismatch,
185    /// Actual current context differs from original restore context.
186    #[error("restore safety observed {0} mismatch")]
187    ContextMismatch(&'static str),
188    /// Actual complete inventory differs, including unselected metadata.
189    #[error("restore safety inventory mismatch")]
190    InventoryMismatch,
191    /// Actual rows do not equal the exact original selected set.
192    #[error("restore safety selection mismatch")]
193    SelectionMismatch,
194    /// Observed source plan differs from original retention.
195    #[error("restore safety source plan mismatch")]
196    SourcePlanMismatch,
197    /// Observed artifact binding differs from original retention.
198    #[error("restore safety source artifacts mismatch")]
199    SourceArtifactsMismatch,
200    /// Current application evidence changes the original lane.
201    #[error("restore safety evidence lane mismatch")]
202    SafetyLaneMismatch,
203    /// External obligations/replayed work are known unresolved or unsafe.
204    #[error("restore safety external obligations unresolved")]
205    UnresolvedExternalObligations,
206    /// Required stopped/drained lifecycle has not been observed.
207    #[error("restore safety target is not stopped")]
208    TargetNotStopped,
209    /// Every selected restored state needs application acceptance before start.
210    #[error("restore safety restored-state acceptance required")]
211    RestoredAcceptanceRequired,
212    /// Exact retained fence identity differs.
213    #[error("restore safety fence identity mismatch")]
214    FenceMismatch,
215    /// Original membership revision changed; never silently rebind a fence.
216    #[error("restore safety membership revision mismatch")]
217    MembershipRevisionMismatch,
218    /// Original external-obligation authority revision changed.
219    #[error("restore safety external-obligations revision mismatch")]
220    ExternalObligationsRevisionMismatch,
221    /// Retained exact fence is currently known inactive.
222    #[error("restore safety fence not active")]
223    FenceNotActive,
224    /// Isolated execution under retained custody has not been qualified before start.
225    #[error("restore safety controlled execution evidence required")]
226    ControlledExecutionRequired,
227    /// Call reporting exceeds the descriptive ceiling.
228    #[error("restore safety reports {reported} observations above ceiling {limit}")]
229    ObservationLimitExceeded {
230        /// Original descriptive ceiling; no allowance.
231        limit: u32,
232        /// Actual reported calls.
233        reported: u32,
234    },
235}
236
237#[cfg(test)]
238mod tests;