ic_backup/model/restore_safety/requirement/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4use crate::model::{
5 artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
6 operation_plan::OperationPlanRecord,
7};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11pub const MAX_RESTORE_SAFETY_REQUIREMENT_BYTES: u64 = 1024;
13#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum RestoreSafetyLaneRecord {
17 NoIrreversibleEffects,
19 ApplicationFenced,
21}
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct RestoreFenceBindingRecord {
26 pub identity: ArtifactChecksumRecord,
28 pub membership_revision: ArtifactChecksumRecord,
30 pub external_obligations_revision: ArtifactChecksumRecord,
32}
33#[derive(Clone, Debug)]
35pub struct RestoreSafetyRequirementRequest {
36 pub source_artifacts: ArtifactChecksumRecord,
38 pub safety: RestoreSafetyLaneRecord,
40 pub expected_fence: Option<RestoreFenceBindingRecord>,
42}
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
49#[serde(try_from = "RequirementFields")]
50pub struct RestoreSafetyRequirementRecord {
51 version: u16,
52 plan_intent: String,
53 source_plan_intent: String,
54 source_artifacts: ArtifactChecksumRecord,
55 safety: RestoreSafetyLaneRecord,
56 expected_fence: Option<RestoreFenceBindingRecord>,
57}
58#[derive(Deserialize)]
59#[serde(deny_unknown_fields)]
60struct RequirementFields {
61 version: u16,
62 plan_intent: String,
63 source_plan_intent: String,
64 source_artifacts: ArtifactChecksumRecord,
65 safety: RestoreSafetyLaneRecord,
66 #[serde(deserialize_with = "required_fence")]
67 expected_fence: Option<RestoreFenceBindingRecord>,
68}
69fn required_fence<'de, D: serde::Deserializer<'de>>(
70 decoder: D,
71) -> Result<Option<RestoreFenceBindingRecord>, D::Error> {
72 Option::deserialize(decoder)
73}
74impl TryFrom<RequirementFields> for RestoreSafetyRequirementRecord {
75 type Error = RestoreSafetyRequirementError;
76 fn try_from(fields: RequirementFields) -> Result<Self, Self::Error> {
77 if fields.version != 1 {
78 return Err(RestoreSafetyRequirementError::UnsupportedVersion(
79 fields.version,
80 ));
81 }
82 validate_lane(fields.safety, fields.expected_fence.as_ref())?;
83 Ok(Self {
84 version: 1,
85 plan_intent: canonical_hash(&fields.plan_intent)?,
86 source_plan_intent: canonical_hash(&fields.source_plan_intent)?,
87 source_artifacts: fields.source_artifacts,
88 safety: fields.safety,
89 expected_fence: fields.expected_fence,
90 })
91 }
92}
93fn validate_lane(
94 safety: RestoreSafetyLaneRecord,
95 fence: Option<&RestoreFenceBindingRecord>,
96) -> Result<(), RestoreSafetyRequirementError> {
97 match (safety, fence) {
98 (RestoreSafetyLaneRecord::ApplicationFenced, None) => {
99 Err(RestoreSafetyRequirementError::FenceRequired)
100 }
101 (RestoreSafetyLaneRecord::NoIrreversibleEffects, Some(_)) => {
102 Err(RestoreSafetyRequirementError::UnexpectedFence)
103 }
104 _ => Ok(()),
105 }
106}
107fn validate_source(
108 plan: &OperationPlanRecord,
109 source: &OperationPlanRecord,
110) -> Result<(), RestoreSafetyRequirementError> {
111 if plan.context().network() != source.context().network() {
112 return Err(RestoreSafetyRequirementError::SourceNetworkMismatch);
113 }
114 if plan.context().release() != source.context().release() {
115 return Err(RestoreSafetyRequirementError::SourceReleaseMismatch);
116 }
117 if plan
118 .selected_targets()
119 .iter()
120 .any(|target| source.selected_targets().binary_search(target).is_err())
121 {
122 return Err(RestoreSafetyRequirementError::SourceSelectionMismatch);
123 }
124 Ok(())
125}
126impl RestoreSafetyRequirementRecord {
127 pub fn new(
135 plan: &OperationPlanRecord,
136 source: &OperationPlanRecord,
137 input: RestoreSafetyRequirementRequest,
138 ) -> Result<Self, RestoreSafetyRequirementError> {
139 validate_source(plan, source)?;
140 validate_lane(input.safety, input.expected_fence.as_ref())?;
141 Ok(Self {
142 version: 1,
143 plan_intent: plan.digest().hash().into(),
144 source_plan_intent: source.digest().hash().into(),
145 source_artifacts: input.source_artifacts,
146 safety: input.safety,
147 expected_fence: input.expected_fence,
148 })
149 }
150 #[must_use]
152 pub fn plan_intent(&self) -> &str {
153 &self.plan_intent
154 }
155 #[must_use]
157 pub fn source_plan_intent(&self) -> &str {
158 &self.source_plan_intent
159 }
160 #[must_use]
162 pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
163 &self.source_artifacts
164 }
165 #[must_use]
167 pub const fn safety(&self) -> RestoreSafetyLaneRecord {
168 self.safety
169 }
170 #[must_use]
172 pub const fn expected_fence(&self) -> Option<&RestoreFenceBindingRecord> {
173 self.expected_fence.as_ref()
174 }
175 pub fn validate_plans(
179 &self,
180 plan: &OperationPlanRecord,
181 source: &OperationPlanRecord,
182 ) -> Result<(), RestoreSafetyRequirementError> {
183 if self.plan_intent != plan.digest().hash() {
184 return Err(RestoreSafetyRequirementError::PlanMismatch);
185 }
186 if self.source_plan_intent != source.digest().hash() {
187 return Err(RestoreSafetyRequirementError::SourcePlanMismatch);
188 }
189 validate_source(plan, source)
190 }
191 #[must_use]
197 pub fn digest(&self) -> ArtifactChecksumRecord {
198 let mut bytes = EvidenceDigest::new(b"ic-backup/restore-safety-requirement/v1\0");
199 bytes.append(self.plan_intent.as_bytes());
200 bytes.append(self.source_plan_intent.as_bytes());
201 bytes.append(self.source_artifacts.hash().as_bytes());
202 bytes.byte(match self.safety {
203 RestoreSafetyLaneRecord::NoIrreversibleEffects => 0,
204 RestoreSafetyLaneRecord::ApplicationFenced => 1,
205 });
206 if let Some(fence) = &self.expected_fence {
207 bytes.append(fence.identity.hash().as_bytes());
208 bytes.append(fence.membership_revision.hash().as_bytes());
209 bytes.append(fence.external_obligations_revision.hash().as_bytes());
210 }
211 bytes.finish()
212 }
213}
214#[derive(Debug, Error)]
216pub enum RestoreSafetyRequirementError {
217 #[error("unsupported restore safety requirement version {0}")]
219 UnsupportedVersion(u16),
220 #[error("restore safety original plan mismatch")]
222 PlanMismatch,
223 #[error("restore safety original source plan mismatch")]
225 SourcePlanMismatch,
226 #[error("restore source network mismatch")]
228 SourceNetworkMismatch,
229 #[error("restore source release mismatch")]
231 SourceReleaseMismatch,
232 #[error("restore source selection mismatch")]
234 SourceSelectionMismatch,
235 #[error("restore safety requires original fence binding")]
237 FenceRequired,
238 #[error("unexpected restore safety fence binding")]
240 UnexpectedFence,
241 #[error(transparent)]
243 Checksum(#[from] ChecksumError),
244}