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