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.checksum(&self.plan_intent);
218 bytes.u64_be(self.acquisition_operation);
219 match &self.scope {
220 FenceObligationScopeRecord::Capture {
221 requirement,
222 identity,
223 membership_revision,
224 } => {
225 bytes.byte(0);
226 bytes.checksum(requirement);
227 bytes.checksum(identity);
228 bytes.checksum(membership_revision);
229 }
230 FenceObligationScopeRecord::Restore { requirement, fence } => {
231 bytes.byte(1);
232 bytes.checksum(requirement);
233 bytes.checksum(&fence.identity);
234 bytes.checksum(&fence.membership_revision);
235 bytes.checksum(&fence.external_obligations_revision);
236 }
237 }
238 bytes.finish()
239 }
240}
241
242fn require_capture(
243 plan: &OperationPlanRecord,
244 requirement: &ConsistencyRequirementRecord,
245) -> Result<(), FenceObligationError> {
246 requirement.validate_plan(plan)?;
247 if requirement.guarantee() != ConsistencyGuaranteeRecord::ApplicationCoordinated {
248 return Err(FenceObligationError::FenceNotRequired);
249 }
250 Ok(())
251}
252
253fn require_restore<'a>(
254 plan: &OperationPlanRecord,
255 source: &OperationPlanRecord,
256 requirement: &'a RestoreSafetyRequirementRecord,
257) -> Result<&'a RestoreFenceBindingRecord, FenceObligationError> {
258 requirement.validate_plans(plan, source)?;
259 if requirement.safety() != RestoreSafetyLaneRecord::ApplicationFenced {
260 return Err(FenceObligationError::FenceNotRequired);
261 }
262 requirement
263 .expected_fence()
264 .ok_or(FenceObligationError::FenceNotRequired)
265}
266
267fn require_acquisition(
268 plan: &OperationPlanRecord,
269 sequence: u64,
270) -> Result<(), FenceObligationError> {
271 if plan.operation(sequence)?.budget().mutations() == 0 {
274 return Err(FenceObligationError::NoAcquisitionAllowance);
275 }
276 Ok(())
277}
278#[derive(Debug, Error)]
280pub enum FenceObligationError {
281 #[error("unsupported fence obligation version {0}")]
283 UnsupportedVersion(u16),
284 #[error("fence obligation original plan mismatch")]
286 PlanMismatch,
287 #[error("fence obligation original binding mismatch")]
289 BindingMismatch,
290 #[error("original requirement does not require an application fence")]
292 FenceNotRequired,
293 #[error("original fence acquisition operation has no mutation allowance")]
295 NoAcquisitionAllowance,
296 #[error(transparent)]
298 Plan(#[from] OperationPlanError),
299 #[error(transparent)]
301 Consistency(#[from] ConsistencyRequirementError),
302 #[error(transparent)]
304 Restore(#[from] RestoreSafetyRequirementError),
305}
306
307#[cfg(test)]
308mod tests;