ic_backup/model/restore_safety/
mod.rs1mod 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ApplicationFenceState {
23 Active,
25 Inactive,
27}
28
29pub const MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS: u32 = 1024;
31#[derive(Clone, Debug)]
33pub struct RestoreSafetyRequestInput {
34 pub operation_sequence: u64,
36 pub challenge: ArtifactChecksumRecord,
38 pub max_remote_observations: u32,
40}
41#[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 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 #[must_use]
88 pub const fn binding(&self) -> &OperationBindingRecord {
89 &self.binding
90 }
91 #[must_use]
93 pub const fn inventory(&self) -> &InventoryRecord {
94 self.plan.inventory()
95 }
96 #[must_use]
98 pub fn selected_targets(&self) -> &[String] {
99 self.plan.selected_targets()
100 }
101 #[must_use]
103 pub const fn requirement(&self) -> &RestoreSafetyRequirementRecord {
104 self.requirement
105 }
106 #[must_use]
108 pub const fn wire(&self) -> &IcManagementRequestRecord {
109 self.wire
110 }
111 #[must_use]
113 pub const fn challenge(&self) -> &ArtifactChecksumRecord {
114 &self.input.challenge
115 }
116 #[must_use]
118 pub const fn max_remote_observations(&self) -> u32 {
119 self.input.max_remote_observations
120 }
121 #[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#[derive(Clone, Debug)]
137pub struct TargetRestoreEvidence {
138 pub target: String,
140 pub state: CanisterStatusType,
142 pub lifecycle_evidence: ArtifactChecksumRecord,
144 pub restored_acceptance: Option<ArtifactChecksumRecord>,
147}
148#[derive(Clone, Debug)]
150pub struct RestoreFenceEvidence {
151 pub state: ApplicationFenceState,
153 pub binding: RestoreFenceBindingRecord,
155 pub whole_selection: ArtifactChecksumRecord,
157 pub rewind_independent_custody: ArtifactChecksumRecord,
159 pub external_obligations_and_replay: ArtifactChecksumRecord,
162 pub controlled_execution: Option<ArtifactChecksumRecord>,
165}
166#[derive(Clone, Debug)]
168pub enum RestoreSafetyEvidence {
169 NoIrreversibleEffects(ArtifactChecksumRecord),
171 ApplicationFenced(Box<RestoreFenceEvidence>),
173 Unresolved(ArtifactChecksumRecord),
175}
176#[derive(Clone, Debug)]
178pub struct RestoreSafetyObservationInput {
179 pub request: ArtifactChecksumRecord,
181 pub context: PlanContextRecord,
183 pub inventory: InventoryRecord,
185 pub source_plan_intent: ArtifactChecksumRecord,
187 pub source_artifacts: ArtifactChecksumRecord,
190 pub targets: Vec<TargetRestoreEvidence>,
192 pub safety: RestoreSafetyEvidence,
194 pub evidence: ArtifactChecksumRecord,
196 pub remote_observations: u32,
198}
199#[derive(Clone, Debug)]
201pub struct RestoreSafetyObservation {
202 input: RestoreSafetyObservationInput,
203}
204impl RestoreSafetyObservation {
205 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 #[must_use]
235 pub const fn request(&self) -> &ArtifactChecksumRecord {
236 &self.input.request
237 }
238 #[must_use]
240 pub const fn context(&self) -> &PlanContextRecord {
241 &self.input.context
242 }
243 #[must_use]
245 pub const fn inventory(&self) -> &InventoryRecord {
246 &self.input.inventory
247 }
248 #[must_use]
250 pub const fn source_plan_intent(&self) -> &ArtifactChecksumRecord {
251 &self.input.source_plan_intent
252 }
253 #[must_use]
255 pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
256 &self.input.source_artifacts
257 }
258 #[must_use]
260 pub fn targets(&self) -> &[TargetRestoreEvidence] {
261 &self.input.targets
262 }
263 #[must_use]
265 pub const fn safety(&self) -> &RestoreSafetyEvidence {
266 &self.input.safety
267 }
268 #[must_use]
270 pub const fn evidence(&self) -> &ArtifactChecksumRecord {
271 &self.input.evidence
272 }
273 #[must_use]
275 pub const fn remote_observations(&self) -> u32 {
276 self.input.remote_observations
277 }
278}
279#[derive(Debug, Eq, Error, PartialEq)]
281pub enum RestoreSafetyObservationError {
282 #[error("restore safety targets must contain 1..={MAX_INVENTORY_TARGETS} entries")]
284 InvalidTargetCount,
285 #[error("invalid restore safety target principal")]
287 InvalidPrincipal,
288 #[error("duplicate restore safety target")]
290 DuplicateTarget,
291 #[error("restore safety target absent from inventory")]
293 TargetAbsentFromInventory,
294}
295#[derive(Debug, Error)]
297pub enum RestoreSafetyRequestError {
298 #[error(transparent)]
300 Requirement(#[from] RestoreSafetyRequirementError),
301 #[error(transparent)]
303 Plan(#[from] OperationPlanError),
304 #[error(transparent)]
306 Payload(#[from] IcRequestError),
307 #[error("restore safety requires exact load or start request")]
309 UnsupportedMethod,
310 #[error("restore safety observation ceiling exceeds {MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS}")]
312 ObservationLimitTooLarge,
313}
314
315#[cfg(test)]
316mod tests;