1use super::{
2 BackupExecutionPreflightReceipts, BackupPlan, BackupPlanError, BackupTarget,
3 ControlAuthorityPreflightRequest, ControlAuthorityPreflightTarget, ControlAuthorityReceipt,
4 QuiescencePreflightReceipt, QuiescencePreflightRequest, QuiescencePreflightTarget,
5 SnapshotReadAuthorityPreflightRequest, SnapshotReadAuthorityPreflightTarget,
6 SnapshotReadAuthorityReceipt, TopologyPreflightReceipt, TopologyPreflightRequest,
7 TopologyPreflightTarget,
8 validation::{
9 validate_control_authority, validate_nonempty, validate_optional_nonempty,
10 validate_preflight_id, validate_preflight_timestamp, validate_preflight_window,
11 validate_principal, validate_required_hash,
12 },
13};
14use std::collections::{BTreeMap, BTreeSet};
15
16impl BackupPlan {
17 pub fn validate_execution_preflight_receipts(
19 &self,
20 topology_receipt: &TopologyPreflightReceipt,
21 quiescence_receipt: &QuiescencePreflightReceipt,
22 preflight_id: &str,
23 as_of: &str,
24 ) -> Result<(), BackupPlanError> {
25 self.validate_for_execution()?;
26 validate_preflight_id(preflight_id)?;
27 validate_preflight_timestamp("preflight.as_of", as_of)?;
28 validate_topology_preflight_receipt(self, topology_receipt, preflight_id, as_of)?;
29 validate_quiescence_preflight_receipt(self, quiescence_receipt, preflight_id, as_of)
30 }
31
32 pub fn apply_execution_preflight_receipts(
34 &mut self,
35 receipts: &BackupExecutionPreflightReceipts,
36 as_of: &str,
37 ) -> Result<(), BackupPlanError> {
38 validate_execution_preflight_bundle(self, receipts, as_of)?;
39 self.apply_authority_preflight_receipts(
40 &receipts.preflight_id,
41 &receipts.control_authority,
42 &receipts.snapshot_read_authority,
43 as_of,
44 )?;
45 self.validate_execution_preflight_receipts(
46 &receipts.topology,
47 &receipts.quiescence,
48 &receipts.preflight_id,
49 as_of,
50 )
51 }
52
53 pub fn apply_authority_preflight_receipts(
55 &mut self,
56 preflight_id: &str,
57 control_receipts: &[ControlAuthorityReceipt],
58 snapshot_read_receipts: &[SnapshotReadAuthorityReceipt],
59 as_of: &str,
60 ) -> Result<(), BackupPlanError> {
61 self.apply_control_authority_receipts(preflight_id, control_receipts, as_of)?;
62 self.apply_snapshot_read_authority_receipts(preflight_id, snapshot_read_receipts, as_of)
63 }
64
65 pub fn apply_control_authority_receipts(
67 &mut self,
68 preflight_id: &str,
69 receipts: &[ControlAuthorityReceipt],
70 as_of: &str,
71 ) -> Result<(), BackupPlanError> {
72 let mut receipts =
73 control_receipt_map(&self.plan_id, preflight_id, as_of, &self.targets, receipts)?;
74 let mut updates = Vec::with_capacity(self.targets.len());
75 for target in &self.targets {
76 let receipt = receipts.remove(&target.canister_id).ok_or_else(|| {
77 BackupPlanError::MissingControlAuthorityReceipt(target.canister_id.clone())
78 })?;
79 if !receipt.authority.is_proven() {
80 return Err(BackupPlanError::UnprovenControlAuthority(
81 target.canister_id.clone(),
82 ));
83 }
84 if self.requires_root_controller
85 && target.canister_id != self.root_canister_id
86 && !receipt.authority.is_proven_root_controller()
87 {
88 return Err(BackupPlanError::MissingRootController(
89 target.canister_id.clone(),
90 ));
91 }
92 updates.push((target.canister_id.clone(), receipt.authority));
93 }
94
95 for (target_id, authority) in updates {
96 let target = self
97 .targets
98 .iter_mut()
99 .find(|target| target.canister_id == target_id)
100 .expect("validated update target should exist");
101 target.control_authority = authority;
102 }
103 Ok(())
104 }
105
106 pub fn apply_snapshot_read_authority_receipts(
108 &mut self,
109 preflight_id: &str,
110 receipts: &[SnapshotReadAuthorityReceipt],
111 as_of: &str,
112 ) -> Result<(), BackupPlanError> {
113 let mut receipts =
114 snapshot_read_receipt_map(&self.plan_id, preflight_id, as_of, &self.targets, receipts)?;
115 let mut updates = Vec::with_capacity(self.targets.len());
116 for target in &self.targets {
117 let receipt = receipts.remove(&target.canister_id).ok_or_else(|| {
118 BackupPlanError::MissingSnapshotReadAuthorityReceipt(target.canister_id.clone())
119 })?;
120 if !receipt.authority.is_proven() {
121 return Err(BackupPlanError::UnprovenTargetSnapshotReadAuthority(
122 target.canister_id.clone(),
123 ));
124 }
125 updates.push((target.canister_id.clone(), receipt.authority));
126 }
127
128 for (target_id, authority) in updates {
129 let target = self
130 .targets
131 .iter_mut()
132 .find(|target| target.canister_id == target_id)
133 .expect("validated update target should exist");
134 target.snapshot_read_authority = authority;
135 }
136 Ok(())
137 }
138
139 #[must_use]
141 pub fn control_authority_preflight_request(&self) -> ControlAuthorityPreflightRequest {
142 ControlAuthorityPreflightRequest {
143 plan_id: self.plan_id.clone(),
144 run_id: self.run_id.clone(),
145 fleet: self.fleet.clone(),
146 network: self.network.clone(),
147 root_canister_id: self.root_canister_id.clone(),
148 requires_root_controller: self.requires_root_controller,
149 targets: self
150 .targets
151 .iter()
152 .map(ControlAuthorityPreflightTarget::from)
153 .collect(),
154 }
155 }
156
157 #[must_use]
159 pub fn snapshot_read_authority_preflight_request(
160 &self,
161 ) -> SnapshotReadAuthorityPreflightRequest {
162 SnapshotReadAuthorityPreflightRequest {
163 plan_id: self.plan_id.clone(),
164 run_id: self.run_id.clone(),
165 fleet: self.fleet.clone(),
166 network: self.network.clone(),
167 root_canister_id: self.root_canister_id.clone(),
168 targets: self
169 .targets
170 .iter()
171 .map(SnapshotReadAuthorityPreflightTarget::from)
172 .collect(),
173 }
174 }
175
176 #[must_use]
178 pub fn topology_preflight_request(&self) -> TopologyPreflightRequest {
179 TopologyPreflightRequest {
180 plan_id: self.plan_id.clone(),
181 run_id: self.run_id.clone(),
182 fleet: self.fleet.clone(),
183 network: self.network.clone(),
184 root_canister_id: self.root_canister_id.clone(),
185 selected_subtree_root: self.selected_subtree_root.clone(),
186 selected_scope_kind: self.selected_scope_kind.clone(),
187 topology_hash_before_quiesce: self.topology_hash_before_quiesce.clone(),
188 targets: self
189 .targets
190 .iter()
191 .map(TopologyPreflightTarget::from)
192 .collect(),
193 }
194 }
195
196 #[must_use]
198 pub fn quiescence_preflight_request(&self) -> QuiescencePreflightRequest {
199 QuiescencePreflightRequest {
200 plan_id: self.plan_id.clone(),
201 run_id: self.run_id.clone(),
202 fleet: self.fleet.clone(),
203 network: self.network.clone(),
204 root_canister_id: self.root_canister_id.clone(),
205 selected_subtree_root: self.selected_subtree_root.clone(),
206 quiescence_policy: self.quiescence_policy.clone(),
207 targets: self
208 .targets
209 .iter()
210 .map(QuiescencePreflightTarget::from)
211 .collect(),
212 }
213 }
214}
215
216fn control_receipt_map(
217 plan_id: &str,
218 preflight_id: &str,
219 as_of: &str,
220 targets: &[BackupTarget],
221 receipts: &[ControlAuthorityReceipt],
222) -> Result<BTreeMap<String, ControlAuthorityReceipt>, BackupPlanError> {
223 let target_ids = targets
224 .iter()
225 .map(|target| target.canister_id.as_str())
226 .collect::<BTreeSet<_>>();
227 let mut receipt_map = BTreeMap::new();
228
229 for receipt in receipts {
230 validate_authority_receipt_header(AuthorityReceiptHeaderInput {
231 expected_plan_id: plan_id,
232 expected_preflight_id: preflight_id,
233 as_of,
234 target_ids: &target_ids,
235 actual_plan_id: &receipt.plan_id,
236 actual_preflight_id: &receipt.preflight_id,
237 target_canister_id: &receipt.target_canister_id,
238 validated_at: &receipt.validated_at,
239 expires_at: &receipt.expires_at,
240 message: receipt.message.as_deref(),
241 })?;
242 validate_control_authority(&receipt.authority)?;
243 if receipt_map
244 .insert(receipt.target_canister_id.clone(), receipt.clone())
245 .is_some()
246 {
247 return Err(BackupPlanError::DuplicateAuthorityReceipt(
248 receipt.target_canister_id.clone(),
249 ));
250 }
251 }
252
253 Ok(receipt_map)
254}
255
256fn snapshot_read_receipt_map(
257 plan_id: &str,
258 preflight_id: &str,
259 as_of: &str,
260 targets: &[BackupTarget],
261 receipts: &[SnapshotReadAuthorityReceipt],
262) -> Result<BTreeMap<String, SnapshotReadAuthorityReceipt>, BackupPlanError> {
263 let target_ids = targets
264 .iter()
265 .map(|target| target.canister_id.as_str())
266 .collect::<BTreeSet<_>>();
267 let mut receipt_map = BTreeMap::new();
268
269 for receipt in receipts {
270 validate_authority_receipt_header(AuthorityReceiptHeaderInput {
271 expected_plan_id: plan_id,
272 expected_preflight_id: preflight_id,
273 as_of,
274 target_ids: &target_ids,
275 actual_plan_id: &receipt.plan_id,
276 actual_preflight_id: &receipt.preflight_id,
277 target_canister_id: &receipt.target_canister_id,
278 validated_at: &receipt.validated_at,
279 expires_at: &receipt.expires_at,
280 message: receipt.message.as_deref(),
281 })?;
282 if receipt_map
283 .insert(receipt.target_canister_id.clone(), receipt.clone())
284 .is_some()
285 {
286 return Err(BackupPlanError::DuplicateAuthorityReceipt(
287 receipt.target_canister_id.clone(),
288 ));
289 }
290 }
291
292 Ok(receipt_map)
293}
294
295struct AuthorityReceiptHeaderInput<'a> {
296 expected_plan_id: &'a str,
297 expected_preflight_id: &'a str,
298 as_of: &'a str,
299 target_ids: &'a BTreeSet<&'a str>,
300 actual_plan_id: &'a str,
301 actual_preflight_id: &'a str,
302 target_canister_id: &'a str,
303 validated_at: &'a str,
304 expires_at: &'a str,
305 message: Option<&'a str>,
306}
307
308fn validate_authority_receipt_header(
309 input: AuthorityReceiptHeaderInput<'_>,
310) -> Result<(), BackupPlanError> {
311 validate_nonempty("authority_receipts[].plan_id", input.actual_plan_id)?;
312 validate_preflight_id(input.actual_preflight_id)?;
313 validate_principal(
314 "authority_receipts[].target_canister_id",
315 input.target_canister_id,
316 )?;
317 validate_optional_nonempty("authority_receipts[].message", input.message)?;
318 validate_preflight_window(
319 input.actual_preflight_id,
320 input.validated_at,
321 input.expires_at,
322 input.as_of,
323 )?;
324
325 if input.actual_plan_id != input.expected_plan_id {
326 return Err(BackupPlanError::AuthorityReceiptPlanMismatch {
327 expected: input.expected_plan_id.to_string(),
328 actual: input.actual_plan_id.to_string(),
329 });
330 }
331 if input.actual_preflight_id != input.expected_preflight_id {
332 return Err(BackupPlanError::AuthorityReceiptPreflightMismatch {
333 expected: input.expected_preflight_id.to_string(),
334 actual: input.actual_preflight_id.to_string(),
335 });
336 }
337 if !input.target_ids.contains(input.target_canister_id) {
338 return Err(BackupPlanError::UnknownAuthorityReceiptTarget(
339 input.target_canister_id.to_string(),
340 ));
341 }
342
343 Ok(())
344}
345
346fn validate_execution_preflight_bundle(
347 plan: &BackupPlan,
348 receipts: &BackupExecutionPreflightReceipts,
349 as_of: &str,
350) -> Result<(), BackupPlanError> {
351 validate_nonempty("preflight_receipts.plan_id", &receipts.plan_id)?;
352 validate_preflight_id(&receipts.preflight_id)?;
353 validate_preflight_timestamp("preflight_receipts.as_of", as_of)?;
354 validate_preflight_window(
355 &receipts.preflight_id,
356 &receipts.validated_at,
357 &receipts.expires_at,
358 as_of,
359 )?;
360
361 if receipts.plan_id != plan.plan_id {
362 return Err(BackupPlanError::PreflightReceiptPlanMismatch {
363 expected: plan.plan_id.clone(),
364 actual: receipts.plan_id.clone(),
365 });
366 }
367
368 Ok(())
369}
370
371fn validate_topology_preflight_receipt(
372 plan: &BackupPlan,
373 receipt: &TopologyPreflightReceipt,
374 preflight_id: &str,
375 as_of: &str,
376) -> Result<(), BackupPlanError> {
377 validate_nonempty("topology_receipt.plan_id", &receipt.plan_id)?;
378 validate_preflight_id(&receipt.preflight_id)?;
379 validate_required_hash(
380 "topology_receipt.topology_hash_before_quiesce",
381 &receipt.topology_hash_before_quiesce,
382 )?;
383 validate_required_hash(
384 "topology_receipt.topology_hash_at_preflight",
385 &receipt.topology_hash_at_preflight,
386 )?;
387 validate_optional_nonempty("topology_receipt.message", receipt.message.as_deref())?;
388 validate_preflight_window(
389 &receipt.preflight_id,
390 &receipt.validated_at,
391 &receipt.expires_at,
392 as_of,
393 )?;
394
395 if receipt.plan_id != plan.plan_id {
396 return Err(BackupPlanError::PreflightReceiptPlanMismatch {
397 expected: plan.plan_id.clone(),
398 actual: receipt.plan_id.clone(),
399 });
400 }
401 if receipt.preflight_id != preflight_id {
402 return Err(BackupPlanError::PreflightReceiptIdMismatch {
403 expected: preflight_id.to_string(),
404 actual: receipt.preflight_id.clone(),
405 });
406 }
407 if receipt.topology_hash_before_quiesce != plan.topology_hash_before_quiesce {
408 return Err(BackupPlanError::TopologyPreflightHashMismatch {
409 expected: plan.topology_hash_before_quiesce.clone(),
410 actual: receipt.topology_hash_before_quiesce.clone(),
411 });
412 }
413 if receipt.topology_hash_at_preflight != plan.topology_hash_before_quiesce {
414 return Err(BackupPlanError::TopologyPreflightHashMismatch {
415 expected: plan.topology_hash_before_quiesce.clone(),
416 actual: receipt.topology_hash_at_preflight.clone(),
417 });
418 }
419 if receipt.targets != plan.topology_preflight_request().targets {
420 return Err(BackupPlanError::TopologyPreflightTargetsMismatch);
421 }
422
423 Ok(())
424}
425
426fn validate_quiescence_preflight_receipt(
427 plan: &BackupPlan,
428 receipt: &QuiescencePreflightReceipt,
429 preflight_id: &str,
430 as_of: &str,
431) -> Result<(), BackupPlanError> {
432 validate_nonempty("quiescence_receipt.plan_id", &receipt.plan_id)?;
433 validate_preflight_id(&receipt.preflight_id)?;
434 validate_optional_nonempty("quiescence_receipt.message", receipt.message.as_deref())?;
435 validate_preflight_window(
436 &receipt.preflight_id,
437 &receipt.validated_at,
438 &receipt.expires_at,
439 as_of,
440 )?;
441
442 if receipt.plan_id != plan.plan_id {
443 return Err(BackupPlanError::PreflightReceiptPlanMismatch {
444 expected: plan.plan_id.clone(),
445 actual: receipt.plan_id.clone(),
446 });
447 }
448 if receipt.preflight_id != preflight_id {
449 return Err(BackupPlanError::PreflightReceiptIdMismatch {
450 expected: preflight_id.to_string(),
451 actual: receipt.preflight_id.clone(),
452 });
453 }
454 if receipt.quiescence_policy != plan.quiescence_policy {
455 return Err(BackupPlanError::QuiescencePolicyMismatch);
456 }
457 if !receipt.accepted {
458 return Err(BackupPlanError::QuiescencePreflightRejected);
459 }
460 if receipt.targets != plan.quiescence_preflight_request().targets {
461 return Err(BackupPlanError::QuiescencePreflightTargetsMismatch);
462 }
463
464 Ok(())
465}