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