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