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 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
20pub const MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS: u32 = 1024;
22#[derive(Clone, Debug)]
24pub struct RestoreSafetyRequestInput {
25 pub operation_sequence: u64,
27 pub challenge: ArtifactChecksumRecord,
29 pub max_remote_observations: u32,
31}
32#[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 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 #[must_use]
82 pub const fn binding(&self) -> &OperationBindingRecord {
83 &self.binding
84 }
85 #[must_use]
87 pub const fn inventory(&self) -> &InventoryRecord {
88 self.plan.inventory()
89 }
90 #[must_use]
92 pub fn selected_targets(&self) -> &[String] {
93 self.plan.selected_targets()
94 }
95 #[must_use]
97 pub const fn requirement(&self) -> &RestoreSafetyRequirementRecord {
98 self.requirement
99 }
100 #[must_use]
102 pub const fn wire(&self) -> &IcManagementRequestRecord {
103 self.wire
104 }
105 #[must_use]
107 pub const fn challenge(&self) -> &ArtifactChecksumRecord {
108 &self.input.challenge
109 }
110 #[must_use]
112 pub const fn max_remote_observations(&self) -> u32 {
113 self.input.max_remote_observations
114 }
115 #[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#[derive(Clone, Debug)]
131pub struct TargetRestoreEvidence {
132 pub target: String,
134 pub state: CanisterStatusType,
136 pub lifecycle_evidence: ArtifactChecksumRecord,
138 pub restored_acceptance: Option<ArtifactChecksumRecord>,
141}
142#[derive(Clone, Debug)]
144pub struct RestoreFenceEvidence {
145 pub state: ApplicationFenceState,
147 pub binding: RestoreFenceBindingRecord,
149 pub whole_selection: ArtifactChecksumRecord,
151 pub rewind_independent_custody: ArtifactChecksumRecord,
153 pub external_obligations_and_replay: ArtifactChecksumRecord,
156 pub controlled_execution: Option<ArtifactChecksumRecord>,
159}
160#[derive(Clone, Debug)]
162pub enum RestoreSafetyEvidence {
163 NoIrreversibleEffects(ArtifactChecksumRecord),
165 ApplicationFenced(Box<RestoreFenceEvidence>),
167 Unresolved(ArtifactChecksumRecord),
169}
170#[derive(Clone, Debug)]
172pub struct RestoreSafetyObservationInput {
173 pub request: ArtifactChecksumRecord,
175 pub context: PlanContextRecord,
177 pub inventory: InventoryRecord,
179 pub source_plan_intent: ArtifactChecksumRecord,
181 pub source_artifacts: ArtifactChecksumRecord,
184 pub targets: Vec<TargetRestoreEvidence>,
186 pub safety: RestoreSafetyEvidence,
188 pub evidence: ArtifactChecksumRecord,
190 pub remote_observations: u32,
192}
193#[derive(Clone, Debug)]
195pub struct RestoreSafetyObservation {
196 input: RestoreSafetyObservationInput,
197}
198impl RestoreSafetyObservation {
199 pub fn new(
203 mut input: RestoreSafetyObservationInput,
204 ) -> Result<Self, RestoreSafetyObservationError> {
205 if input.targets.is_empty() || input.targets.len() > MAX_INVENTORY_TARGETS {
206 return Err(RestoreSafetyObservationError::InvalidTargetCount);
207 }
208 for target in &mut input.targets {
209 target.target = super::principal::canonical_text(&target.target)
210 .ok_or(RestoreSafetyObservationError::InvalidPrincipal)?;
211 if input.inventory.target(&target.target).is_err() {
212 return Err(RestoreSafetyObservationError::TargetAbsentFromInventory);
213 }
214 }
215 input.targets.sort_by(|a, b| a.target.cmp(&b.target));
216 if input
217 .targets
218 .windows(2)
219 .any(|pair| pair[0].target == pair[1].target)
220 {
221 return Err(RestoreSafetyObservationError::DuplicateTarget);
222 }
223 Ok(Self { input })
224 }
225 #[must_use]
227 pub const fn request(&self) -> &ArtifactChecksumRecord {
228 &self.input.request
229 }
230 #[must_use]
232 pub const fn context(&self) -> &PlanContextRecord {
233 &self.input.context
234 }
235 #[must_use]
237 pub const fn inventory(&self) -> &InventoryRecord {
238 &self.input.inventory
239 }
240 #[must_use]
242 pub const fn source_plan_intent(&self) -> &ArtifactChecksumRecord {
243 &self.input.source_plan_intent
244 }
245 #[must_use]
247 pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
248 &self.input.source_artifacts
249 }
250 #[must_use]
252 pub fn targets(&self) -> &[TargetRestoreEvidence] {
253 &self.input.targets
254 }
255 #[must_use]
257 pub const fn safety(&self) -> &RestoreSafetyEvidence {
258 &self.input.safety
259 }
260 #[must_use]
262 pub const fn evidence(&self) -> &ArtifactChecksumRecord {
263 &self.input.evidence
264 }
265 #[must_use]
267 pub const fn remote_observations(&self) -> u32 {
268 self.input.remote_observations
269 }
270}
271#[derive(Debug, Eq, Error, PartialEq)]
273pub enum RestoreSafetyObservationError {
274 #[error("restore safety targets must contain 1..={MAX_INVENTORY_TARGETS} entries")]
276 InvalidTargetCount,
277 #[error("invalid restore safety target principal")]
279 InvalidPrincipal,
280 #[error("duplicate restore safety target")]
282 DuplicateTarget,
283 #[error("restore safety target absent from inventory")]
285 TargetAbsentFromInventory,
286}
287#[derive(Debug, Error)]
289pub enum RestoreSafetyRequestError {
290 #[error(transparent)]
292 Requirement(#[from] RestoreSafetyRequirementError),
293 #[error(transparent)]
295 Plan(#[from] OperationPlanError),
296 #[error(transparent)]
298 Payload(#[from] IcRequestError),
299 #[error("restore safety requires exact load or start request")]
301 UnsupportedMethod,
302 #[error("restore safety observation ceiling exceeds {MAX_RESTORE_SAFETY_REMOTE_OBSERVATIONS}")]
304 ObservationLimitTooLarge,
305}
306
307#[cfg(test)]
308mod tests;