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