Skip to main content

ic_backup/model/restore_safety/
mod.rs

1//! Original restore/source requirements and ephemeral application-owned safety evidence.
2
3mod requirement;
4pub use requirement::{
5    MAX_RESTORE_SAFETY_REQUIREMENT_BYTES, RestoreFenceBindingRecord, RestoreSafetyLaneRecord,
6    RestoreSafetyRequirementError, RestoreSafetyRequirementRecord, RestoreSafetyRequirementRequest,
7};
8
9use crate::model::{
10    artifacts::ArtifactChecksumRecord,
11    attempt_journal::OperationBindingRecord,
12    evidence_digest::EvidenceDigest,
13    ic_request::{IcManagementMethodRecord, IcManagementRequestRecord, IcRequestError},
14    inventory::{InventoryRecord, MAX_INVENTORY_TARGETS},
15    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
16};
17use ic_platform::management_canister_types::CanisterStatusType;
18use thiserror::Error;
19
20/// Actually observed application fence state; never serialized authority.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ApplicationFenceState {
23    /// Exact retained fence is qualified active with continuous custody.
24    Active,
25    /// The retained fence is known inactive; restore is denied.
26    Inactive,
27}
28
29/// Maximum descriptive remote observations per invocation; no spending allowance.
30pub const MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS: u32 = 1024;
31/// Passive current parameters; the exact load/start boundary is derived from original wire bytes.
32#[derive(Clone, Debug)]
33pub struct RestoreSafetyRequestInput {
34    /// Exact original load/start operation sequence.
35    pub operation_sequence: u64,
36    /// Integration-owned fresh challenge, qualified independently of hash equality.
37    pub challenge: ArtifactChecksumRecord,
38    /// Descriptive call ceiling; every actual call needs separate prior accounting.
39    pub max_remote_observations: u32,
40}
41/// Ephemeral original-source/safety/challenge request; no dispatch or paid-call permit.
42#[derive(Clone, Debug)]
43pub struct RestoreSafetyRequest<'a> {
44    plan: &'a OperationPlanRecord,
45    requirement: &'a RestoreSafetyRequirementRecord,
46    binding: OperationBindingRecord,
47    wire: &'a IcManagementRequestRecord,
48    input: RestoreSafetyRequestInput,
49}
50impl<'a> RestoreSafetyRequest<'a> {
51    /// Validate exact original plans, requirement and load/start mutation bytes.
52    ///
53    /// The integration admits the retained requirement before effect-boundary
54    /// use and retains original source artifacts and fence obligations. This
55    /// constructor performs no IO and cannot prove that custody.
56    /// # Errors
57    /// Rejects changed source/plan/payload, other methods or excessive descriptive calls.
58    pub fn new(
59        plan: &'a OperationPlanRecord,
60        source: &OperationPlanRecord,
61        requirement: &'a RestoreSafetyRequirementRecord,
62        wire: &'a IcManagementRequestRecord,
63        input: RestoreSafetyRequestInput,
64    ) -> Result<Self, RestoreSafetyRequestError> {
65        requirement.validate_plans(plan, source)?;
66        if input.max_remote_observations > MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS {
67            return Err(RestoreSafetyRequestError::ObservationLimitTooLarge);
68        }
69        if !matches!(
70            wire.method(),
71            IcManagementMethodRecord::LoadCanisterSnapshot
72                | IcManagementMethodRecord::StartCanister
73        ) {
74            return Err(RestoreSafetyRequestError::UnsupportedMethod);
75        }
76        let binding = plan.operation_binding(input.operation_sequence)?;
77        wire.validate_mutation_binding(&binding)?;
78        Ok(Self {
79            plan,
80            requirement,
81            binding,
82            wire,
83            input,
84        })
85    }
86    /// Read exact original operation, context, target and request identity.
87    #[must_use]
88    pub const fn binding(&self) -> &OperationBindingRecord {
89        &self.binding
90    }
91    /// Read original full inventory, including unselected metadata.
92    #[must_use]
93    pub const fn inventory(&self) -> &InventoryRecord {
94        self.plan.inventory()
95    }
96    /// Read exact original selected IDs in canonical order, not dispatch order.
97    #[must_use]
98    pub fn selected_targets(&self) -> &[String] {
99        self.plan.selected_targets()
100    }
101    /// Read original immutable source and safety requirement.
102    #[must_use]
103    pub const fn requirement(&self) -> &RestoreSafetyRequirementRecord {
104        self.requirement
105    }
106    /// Read exact original load/start wire request; codecs confer no load settlement.
107    #[must_use]
108    pub const fn wire(&self) -> &IcManagementRequestRecord {
109        self.wire
110    }
111    /// Read challenge; matching values alone prove no freshness.
112    #[must_use]
113    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
114        &self.input.challenge
115    }
116    /// Read descriptive call ceiling without spending/replenishment.
117    #[must_use]
118    pub const fn max_remote_observations(&self) -> u32 {
119        self.input.max_remote_observations
120    }
121    /// Hash v1 NUL-terminated ASCII domain, 64 ASCII requirement-digest bytes,
122    /// u64 BE sequence, 64 ASCII wire-digest bytes, 64 ASCII challenge bytes and
123    /// u32 BE descriptive ceiling. Wire digest distinguishes load from start.
124    #[must_use]
125    pub fn digest(&self) -> ArtifactChecksumRecord {
126        let mut framing = EvidenceDigest::new(b"ic-backup/restore-safety-request/v1\0");
127        framing.checksum(&self.requirement.digest());
128        framing.u64_be(self.binding.operation_sequence());
129        framing.checksum(&self.wire.digest());
130        framing.checksum(self.challenge());
131        framing.u32_be(self.max_remote_observations());
132        framing.finish()
133    }
134}
135/// Passive actual lifecycle and integration-qualified source-specific restored acceptance.
136#[derive(Clone, Debug)]
137pub struct TargetRestoreEvidence {
138    /// Actual physical target, normalized at model admission.
139    pub target: String,
140    /// Actual upstream lifecycle state; never inferred from a process exit status.
141    pub state: CanisterStatusType,
142    /// Evidence qualifying lifecycle and, when stopped, drained application work.
143    pub lifecycle_evidence: ArtifactChecksumRecord,
144    /// Current application acceptance of this exact source's restored state, required before start.
145    /// A module hash or load acknowledgement alone cannot supply this evidence.
146    pub restored_acceptance: Option<ArtifactChecksumRecord>,
147}
148/// Passive exact fence evidence; every opaque field requires application qualification.
149#[derive(Clone, Debug)]
150pub struct RestoreFenceEvidence {
151    /// Actually observed active/inactive custody; unknown custody fails at the provider.
152    pub state: ApplicationFenceState,
153    /// Actually observed fence and current authority revisions, matched to original retention.
154    pub binding: RestoreFenceBindingRecord,
155    /// Whole-selection write, membership, timer, external-work fencing and drained-work evidence.
156    pub whole_selection: ArtifactChecksumRecord,
157    /// Fence and obligation custody outside the source's rewindable state, across failure/recovery.
158    pub rewind_independent_custody: ArtifactChecksumRecord,
159    /// Qualified external-obligation disposition and prevention of replayed irreversible work.
160    /// Settlement alone is insufficient if restored intent can replay that work.
161    pub external_obligations_and_replay: ArtifactChecksumRecord,
162    /// Current allowance for isolated restored execution under the retained fence, required before start.
163    /// This is application evidence, not a release token, future guarantee or dispatch permit.
164    pub controlled_execution: Option<ArtifactChecksumRecord>,
165}
166/// Actual application safety lane; no generic default or serialized Proven admission.
167#[derive(Clone, Debug)]
168pub enum RestoreSafetyEvidence {
169    /// Qualified absence of irreversible external effects, including restored intent/timers.
170    NoIrreversibleEffects(ArtifactChecksumRecord),
171    /// Qualified whole-selection custody outside rewindable state and external-work replay safety.
172    ApplicationFenced(Box<RestoreFenceEvidence>),
173    /// Known unsafe or unresolved external obligations; pure policy rejects this evidence.
174    Unresolved(ArtifactChecksumRecord),
175}
176/// Passive provider data; exact original context/source and current evidence are independent inputs.
177#[derive(Clone, Debug)]
178pub struct RestoreSafetyObservationInput {
179    /// Exact current challenge-bound request digest.
180    pub request: ArtifactChecksumRecord,
181    /// Actual current canonical network/caller/release, not echoed expected labels.
182    pub context: PlanContextRecord,
183    /// Complete actual current inventory, including unselected parent metadata.
184    pub inventory: InventoryRecord,
185    /// Qualified exact original source plan identity.
186    pub source_plan_intent: ArtifactChecksumRecord,
187    /// Qualified complete original source artifacts in stable retained custody,
188    /// including exact target-local load snapshot association when checking load.
189    pub source_artifacts: ArtifactChecksumRecord,
190    /// Actual lifecycle and restored acceptance for the exact selected set.
191    pub targets: Vec<TargetRestoreEvidence>,
192    /// Actual application safety lane and evidence for this exact source/selection/boundary.
193    pub safety: RestoreSafetyEvidence,
194    /// Opaque integration-qualified evidence identity; no signature or permission.
195    pub evidence: ArtifactChecksumRecord,
196    /// Actual remote observations, each needing separate prior spending authority.
197    pub remote_observations: u32,
198}
199/// Canonical immutable ephemeral observation; no Serde, expiry or persisted authority flag.
200#[derive(Clone, Debug)]
201pub struct RestoreSafetyObservation {
202    input: RestoreSafetyObservationInput,
203}
204impl RestoreSafetyObservation {
205    /// Normalize/sort nonempty bounded unique actual targets under the actual full inventory.
206    /// # Errors
207    /// Rejects excessive/empty rows, malformed principals, duplicates or unknown targets.
208    pub fn new(
209        mut input: RestoreSafetyObservationInput,
210    ) -> Result<Self, RestoreSafetyObservationError> {
211        if input.targets.is_empty() || input.targets.len() > MAX_INVENTORY_TARGETS {
212            return Err(RestoreSafetyObservationError::InvalidTargetCount);
213        }
214        for row in &mut input.targets {
215            row.target = super::principal::canonical_text(&row.target)
216                .ok_or(RestoreSafetyObservationError::InvalidPrincipal)?;
217            if input.inventory.canonical_target(&row.target).is_none() {
218                return Err(RestoreSafetyObservationError::TargetAbsentFromInventory);
219            }
220        }
221        input
222            .targets
223            .sort_by(|left, right| left.target.cmp(&right.target));
224        if input
225            .targets
226            .windows(2)
227            .any(|pair| pair[0].target == pair[1].target)
228        {
229            return Err(RestoreSafetyObservationError::DuplicateTarget);
230        }
231        Ok(Self { input })
232    }
233    /// Read current request identity.
234    #[must_use]
235    pub const fn request(&self) -> &ArtifactChecksumRecord {
236        &self.input.request
237    }
238    /// Read actual canonical current context.
239    #[must_use]
240    pub const fn context(&self) -> &PlanContextRecord {
241        &self.input.context
242    }
243    /// Read complete actual inventory.
244    #[must_use]
245    pub const fn inventory(&self) -> &InventoryRecord {
246        &self.input.inventory
247    }
248    /// Read actual qualified source plan identity.
249    #[must_use]
250    pub const fn source_plan_intent(&self) -> &ArtifactChecksumRecord {
251        &self.input.source_plan_intent
252    }
253    /// Read actual qualified source artifact binding.
254    #[must_use]
255    pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
256        &self.input.source_artifacts
257    }
258    /// Read canonical actual selected rows.
259    #[must_use]
260    pub fn targets(&self) -> &[TargetRestoreEvidence] {
261        &self.input.targets
262    }
263    /// Read current application-qualified safety lane.
264    #[must_use]
265    pub const fn safety(&self) -> &RestoreSafetyEvidence {
266        &self.input.safety
267    }
268    /// Read opaque qualified evidence.
269    #[must_use]
270    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
271        &self.input.evidence
272    }
273    /// Read actual descriptive calls, without new authority or accounting.
274    #[must_use]
275    pub const fn remote_observations(&self) -> u32 {
276        self.input.remote_observations
277    }
278}
279/// Typed owning-boundary current evidence admission denial.
280#[derive(Debug, Eq, Error, PartialEq)]
281pub enum RestoreSafetyObservationError {
282    /// Actual rows must be nonempty and within the inventory bound.
283    #[error("restore safety targets must contain 1..={MAX_INVENTORY_TARGETS} entries")]
284    InvalidTargetCount,
285    /// Target principal failed bounded normalization.
286    #[error("invalid restore safety target principal")]
287    InvalidPrincipal,
288    /// Canonical equivalent targets cannot appear twice.
289    #[error("duplicate restore safety target")]
290    DuplicateTarget,
291    /// Actual row is absent from the actual full inventory.
292    #[error("restore safety target absent from inventory")]
293    TargetAbsentFromInventory,
294}
295/// Typed original request denial; changes no source, fence, spending or journal.
296#[derive(Debug, Error)]
297pub enum RestoreSafetyRequestError {
298    /// Original source/safety/plan declarations differ.
299    #[error(transparent)]
300    Requirement(#[from] RestoreSafetyRequirementError),
301    /// Exact original operation cannot be derived.
302    #[error(transparent)]
303    Plan(#[from] OperationPlanError),
304    /// Exact original mutation bytes/target differ.
305    #[error(transparent)]
306    Payload(#[from] IcRequestError),
307    /// Only load/start are within this safety boundary.
308    #[error("restore safety requires exact load or start request")]
309    UnsupportedMethod,
310    /// Descriptive observation ceiling is excessive.
311    #[error("restore safety observation ceiling exceeds {MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS}")]
312    ObservationLimitTooLarge,
313}
314
315#[cfg(test)]
316mod tests;