ic_backup/model/fence_obligation/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4use crate::model::{
5 artifacts::ArtifactChecksumRecord,
6 consistency::{
7 ApplicationFenceBinding, ConsistencyGuaranteeRecord, ConsistencyRequirementError,
8 ConsistencyRequirementRecord,
9 },
10 operation_plan::{OperationPlanError, OperationPlanRecord},
11 restore_safety::{
12 RestoreFenceBindingRecord, RestoreSafetyLaneRecord, RestoreSafetyRequirementError,
13 RestoreSafetyRequirementRecord,
14 },
15};
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19pub const MAX_FENCE_OBLIGATION_BYTES: u64 = 1024;
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(tag = "purpose", rename_all = "snake_case", deny_unknown_fields)]
25pub enum FenceObligationScopeRecord {
26 Capture {
28 requirement: ArtifactChecksumRecord,
30 identity: ArtifactChecksumRecord,
32 membership_revision: ArtifactChecksumRecord,
34 },
35 Restore {
37 requirement: ArtifactChecksumRecord,
39 fence: RestoreFenceBindingRecord,
41 },
42}
43
44#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
52#[serde(try_from = "ObligationFields")]
53pub struct FenceObligationRecord {
54 version: u16,
55 plan_intent: ArtifactChecksumRecord,
56 acquisition_operation: u64,
57 scope: FenceObligationScopeRecord,
58}
59#[derive(Deserialize)]
60#[serde(deny_unknown_fields)]
61struct ObligationFields {
62 version: u16,
63 plan_intent: ArtifactChecksumRecord,
64 acquisition_operation: u64,
65 scope: FenceObligationScopeRecord,
66}
67impl TryFrom<ObligationFields> for FenceObligationRecord {
68 type Error = FenceObligationError;
69 fn try_from(fields: ObligationFields) -> Result<Self, Self::Error> {
70 if fields.version != 1 {
71 return Err(FenceObligationError::UnsupportedVersion(fields.version));
72 }
73 Ok(Self {
74 version: 1,
75 plan_intent: fields.plan_intent,
76 acquisition_operation: fields.acquisition_operation,
77 scope: fields.scope,
78 })
79 }
80}
81impl FenceObligationRecord {
82 pub fn for_capture(
86 plan: &OperationPlanRecord,
87 requirement: &ConsistencyRequirementRecord,
88 acquisition_operation: u64,
89 fence: &ApplicationFenceBinding,
90 ) -> Result<Self, FenceObligationError> {
91 require_capture(plan, requirement)?;
92 Self::new(
93 plan,
94 acquisition_operation,
95 FenceObligationScopeRecord::Capture {
96 requirement: requirement.digest(),
97 identity: fence.identity.clone(),
98 membership_revision: fence.membership_revision.clone(),
99 },
100 )
101 }
102 pub fn for_restore(
106 plan: &OperationPlanRecord,
107 source: &OperationPlanRecord,
108 requirement: &RestoreSafetyRequirementRecord,
109 acquisition_operation: u64,
110 ) -> Result<Self, FenceObligationError> {
111 let fence = require_restore(plan, source, requirement)?;
112 Self::new(
113 plan,
114 acquisition_operation,
115 FenceObligationScopeRecord::Restore {
116 requirement: requirement.digest(),
117 fence: fence.clone(),
118 },
119 )
120 }
121 fn new(
122 plan: &OperationPlanRecord,
123 acquisition_operation: u64,
124 scope: FenceObligationScopeRecord,
125 ) -> Result<Self, FenceObligationError> {
126 let record = Self {
127 version: 1,
128 plan_intent: plan.digest(),
129 acquisition_operation,
130 scope,
131 };
132 record.validate_plan(plan)?;
133 Ok(record)
134 }
135 #[must_use]
137 pub const fn plan_intent(&self) -> &ArtifactChecksumRecord {
138 &self.plan_intent
139 }
140 #[must_use]
142 pub const fn acquisition_operation(&self) -> u64 {
143 self.acquisition_operation
144 }
145 #[must_use]
147 pub const fn scope(&self) -> &FenceObligationScopeRecord {
148 &self.scope
149 }
150 pub fn validate_plan(&self, plan: &OperationPlanRecord) -> Result<(), FenceObligationError> {
154 if self.plan_intent != plan.digest() {
155 return Err(FenceObligationError::PlanMismatch);
156 }
157 require_acquisition(plan, self.acquisition_operation)
158 }
159 pub fn validate_capture(
163 &self,
164 plan: &OperationPlanRecord,
165 requirement: &ConsistencyRequirementRecord,
166 fence: &ApplicationFenceBinding,
167 ) -> Result<(), FenceObligationError> {
168 require_capture(plan, requirement)?;
169 require_acquisition(plan, self.acquisition_operation)?;
170 let scope_matches = matches!(
171 &self.scope,
172 FenceObligationScopeRecord::Capture { requirement: original, identity, membership_revision }
173 if original == &requirement.digest()
174 && identity == &fence.identity
175 && membership_revision == &fence.membership_revision
176 );
177 self.validate_binding(plan, scope_matches)
178 }
179 pub fn validate_restore(
183 &self,
184 plan: &OperationPlanRecord,
185 source: &OperationPlanRecord,
186 requirement: &RestoreSafetyRequirementRecord,
187 ) -> Result<(), FenceObligationError> {
188 let fence = require_restore(plan, source, requirement)?;
189 require_acquisition(plan, self.acquisition_operation)?;
190 let scope_matches = matches!(
191 &self.scope,
192 FenceObligationScopeRecord::Restore { requirement: original, fence: original_fence }
193 if original == &requirement.digest() && original_fence == fence
194 );
195 self.validate_binding(plan, scope_matches)
196 }
197 fn validate_binding(
200 &self,
201 plan: &OperationPlanRecord,
202 scope_matches: bool,
203 ) -> Result<(), FenceObligationError> {
204 if self.plan_intent != plan.digest() || !scope_matches {
205 return Err(FenceObligationError::BindingMismatch);
206 }
207 Ok(())
208 }
209 #[must_use]
215 pub fn digest(&self) -> ArtifactChecksumRecord {
216 let mut bytes = EvidenceDigest::new(b"ic-backup/fence-obligation/v1\0");
217 bytes.append(self.plan_intent.hash().as_bytes());
218 bytes.append(&self.acquisition_operation.to_be_bytes());
219 let (tag, hashes) = match &self.scope {
220 FenceObligationScopeRecord::Capture {
221 requirement,
222 identity,
223 membership_revision,
224 } => (0, vec![requirement, identity, membership_revision]),
225 FenceObligationScopeRecord::Restore { requirement, fence } => (
226 1,
227 vec![
228 requirement,
229 &fence.identity,
230 &fence.membership_revision,
231 &fence.external_obligations_revision,
232 ],
233 ),
234 };
235 bytes.byte(tag);
236 for hash in hashes {
237 bytes.append(hash.hash().as_bytes());
238 }
239 bytes.finish()
240 }
241}
242
243fn require_capture(
244 plan: &OperationPlanRecord,
245 requirement: &ConsistencyRequirementRecord,
246) -> Result<(), FenceObligationError> {
247 requirement.validate_plan(plan)?;
248 if requirement.guarantee() != ConsistencyGuaranteeRecord::ApplicationCoordinated {
249 return Err(FenceObligationError::FenceNotRequired);
250 }
251 Ok(())
252}
253
254fn require_restore<'a>(
255 plan: &OperationPlanRecord,
256 source: &OperationPlanRecord,
257 requirement: &'a RestoreSafetyRequirementRecord,
258) -> Result<&'a RestoreFenceBindingRecord, FenceObligationError> {
259 requirement.validate_plans(plan, source)?;
260 if requirement.safety() != RestoreSafetyLaneRecord::ApplicationFenced {
261 return Err(FenceObligationError::FenceNotRequired);
262 }
263 requirement
264 .expected_fence()
265 .ok_or(FenceObligationError::FenceNotRequired)
266}
267
268fn require_acquisition(
269 plan: &OperationPlanRecord,
270 sequence: u64,
271) -> Result<(), FenceObligationError> {
272 if plan.operation(sequence)?.budget().mutations() == 0 {
275 return Err(FenceObligationError::NoAcquisitionAllowance);
276 }
277 Ok(())
278}
279#[derive(Debug, Error)]
281pub enum FenceObligationError {
282 #[error("unsupported fence obligation version {0}")]
284 UnsupportedVersion(u16),
285 #[error("fence obligation original plan mismatch")]
287 PlanMismatch,
288 #[error("fence obligation original binding mismatch")]
290 BindingMismatch,
291 #[error("original requirement does not require an application fence")]
293 FenceNotRequired,
294 #[error("original fence acquisition operation has no mutation allowance")]
296 NoAcquisitionAllowance,
297 #[error(transparent)]
299 Plan(#[from] OperationPlanError),
300 #[error(transparent)]
302 Consistency(#[from] ConsistencyRequirementError),
303 #[error(transparent)]
305 Restore(#[from] RestoreSafetyRequirementError),
306}
307
308#[cfg(test)]
309mod tests;