ic_backup/model/fence_reconciliation/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord, MAX_OPERATION_ATTEMPTS},
6 consistency::ApplicationFenceEvidence,
7 fence_obligation::{FenceObligationError, FenceObligationRecord},
8 inventory::{InventoryRecord, InventoryRecordError, MAX_INVENTORY_TARGETS},
9 operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
10 restore_safety::RestoreFenceEvidence,
11};
12use thiserror::Error;
13
14pub const MAX_FENCE_RECONCILIATION_REMOTE_OBSERVATIONS: u32 = 1;
19
20#[derive(Clone, Debug)]
26pub struct FenceReconciliationIntent<'a> {
27 plan: &'a OperationPlanRecord,
28 obligation: &'a FenceObligationRecord,
29 authority: AttemptAuthorityRecord,
30 mutation_attempt: u32,
31 challenge: ArtifactChecksumRecord,
32}
33impl<'a> FenceReconciliationIntent<'a> {
34 pub fn new(
41 plan: &'a OperationPlanRecord,
42 obligation: &'a FenceObligationRecord,
43 journal: &AttemptJournalRecord,
44 challenge: ArtifactChecksumRecord,
45 ) -> Result<Self, FenceReconciliationRequestError> {
46 obligation.validate_plan(plan)?;
47 let authority = plan.attempt_authority(obligation.acquisition_operation())?;
48 if journal.authority() != &authority {
49 return Err(FenceReconciliationRequestError::AuthorityMismatch);
50 }
51 let mutation_attempt = journal
52 .view()
53 .pending_mutation
54 .ok_or(FenceReconciliationRequestError::NoPendingMutation)?;
55 Ok(Self {
56 plan,
57 obligation,
58 authority,
59 mutation_attempt,
60 challenge,
61 })
62 }
63 #[must_use]
65 pub const fn obligation(&self) -> &FenceObligationRecord {
66 self.obligation
67 }
68 #[must_use]
70 pub const fn plan(&self) -> &OperationPlanRecord {
71 self.plan
72 }
73 #[must_use]
75 pub const fn authority(&self) -> &AttemptAuthorityRecord {
76 &self.authority
77 }
78 #[must_use]
80 pub const fn mutation_attempt(&self) -> u32 {
81 self.mutation_attempt
82 }
83 #[must_use]
85 pub const fn challenge(&self) -> &ArtifactChecksumRecord {
86 &self.challenge
87 }
88 #[must_use]
94 pub fn digest(&self) -> ArtifactChecksumRecord {
95 observation_digest(
96 &self.obligation.digest(),
97 &self.authority.digest(),
98 self.mutation_attempt,
99 &self.challenge,
100 )
101 }
102 fn validate_mutation(
103 &self,
104 journal: &AttemptJournalRecord,
105 ) -> Result<(), FenceReconciliationRequestError> {
106 if journal.authority() != &self.authority {
107 return Err(FenceReconciliationRequestError::AuthorityMismatch);
108 }
109 if journal.view().pending_mutation != Some(self.mutation_attempt) {
110 return Err(FenceReconciliationRequestError::MutationMismatch);
111 }
112 Ok(())
113 }
114}
115fn observation_digest(
116 obligation: &ArtifactChecksumRecord,
117 authority: &ArtifactChecksumRecord,
118 mutation: u32,
119 challenge: &ArtifactChecksumRecord,
120) -> ArtifactChecksumRecord {
121 let mut bytes = b"ic-backup/fence-reconciliation/v1\0".to_vec();
122 bytes.extend_from_slice(obligation.hash().as_bytes());
123 bytes.extend_from_slice(authority.hash().as_bytes());
124 bytes.extend_from_slice(&mutation.to_be_bytes());
125 bytes.extend_from_slice(challenge.hash().as_bytes());
126 ArtifactChecksumRecord::from_bytes(&bytes)
127}
128
129#[derive(Clone, Debug)]
135pub struct FenceReconciliationRequest<'a, 'plan> {
136 intent: &'a FenceReconciliationIntent<'plan>,
137 observation_attempt: u32,
138}
139impl<'a, 'plan> FenceReconciliationRequest<'a, 'plan> {
140 pub fn new(
144 intent: &'a FenceReconciliationIntent<'plan>,
145 journal: &AttemptJournalRecord,
146 ) -> Result<Self, FenceReconciliationRequestError> {
147 intent.validate_mutation(journal)?;
148 let observation_attempt = journal
149 .view()
150 .pending_observation
151 .ok_or(FenceReconciliationRequestError::NoPendingObservation)?;
152 let request = Self {
153 intent,
154 observation_attempt,
155 };
156 request.validate_journal(journal)?;
157 Ok(request)
158 }
159 #[must_use]
161 pub const fn intent(&self) -> &FenceReconciliationIntent<'plan> {
162 self.intent
163 }
164 #[must_use]
166 pub const fn observation_attempt(&self) -> u32 {
167 self.observation_attempt
168 }
169 #[must_use]
171 pub fn digest(&self) -> ArtifactChecksumRecord {
172 self.intent.digest()
173 }
174 pub fn validate_journal(
180 &self,
181 journal: &AttemptJournalRecord,
182 ) -> Result<(), FenceReconciliationRequestError> {
183 self.intent.validate_mutation(journal)?;
184 if journal.view().pending_observation != Some(self.observation_attempt) {
185 return Err(FenceReconciliationRequestError::ObservationMismatch);
186 }
187 if journal.pending_observation_request() != Some(self.digest().hash()) {
188 return Err(FenceReconciliationRequestError::RequestMismatch);
189 }
190 Ok(())
191 }
192}
193
194#[derive(Clone, Debug)]
196pub enum FenceReconciliationEvidence {
197 AcquiredCapture {
199 fence: Box<ApplicationFenceEvidence>,
201 attribution: ArtifactChecksumRecord,
203 },
204 AcquiredRestore {
206 fence: Box<RestoreFenceEvidence>,
208 attribution: ArtifactChecksumRecord,
210 },
211 NotAcquired {
213 exclusion: ArtifactChecksumRecord,
215 },
216 Unresolved {
218 uncertainty: ArtifactChecksumRecord,
220 },
221}
222#[derive(Clone, Debug)]
224pub struct FenceReconciliationObservationInput {
225 pub request: ArtifactChecksumRecord,
227 pub mutation_attempt: u32,
229 pub observation_attempt: u32,
231 pub context: PlanContextRecord,
233 pub inventory: InventoryRecord,
235 pub selected_targets: Vec<String>,
237 pub settlement: FenceReconciliationEvidence,
239 pub evidence: ArtifactChecksumRecord,
241 pub remote_observations: u32,
243}
244#[derive(Clone, Debug)]
246pub struct FenceReconciliationObservation {
247 input: FenceReconciliationObservationInput,
248}
249impl FenceReconciliationObservation {
250 pub fn new(
254 mut input: FenceReconciliationObservationInput,
255 ) -> Result<Self, FenceReconciliationObservationError> {
256 if input.mutation_attempt == 0
257 || input.observation_attempt <= input.mutation_attempt
258 || input.observation_attempt > MAX_OPERATION_ATTEMPTS
259 {
260 return Err(FenceReconciliationObservationError::InvalidAttempts);
261 }
262 if input.selected_targets.is_empty() || input.selected_targets.len() > MAX_INVENTORY_TARGETS
263 {
264 return Err(FenceReconciliationObservationError::InvalidTargetCount);
265 }
266 for target in &mut input.selected_targets {
267 *target = input.inventory.target(target)?.canister_id().into();
268 }
269 input.selected_targets.sort();
270 if input
271 .selected_targets
272 .windows(2)
273 .any(|pair| pair[0] == pair[1])
274 {
275 return Err(FenceReconciliationObservationError::DuplicateTarget);
276 }
277 Ok(Self { input })
278 }
279 #[must_use]
281 pub const fn input(&self) -> &FenceReconciliationObservationInput {
282 &self.input
283 }
284}
285
286#[derive(Debug, Error)]
288pub enum FenceReconciliationRequestError {
289 #[error("fence reconciliation original journal authority mismatch")]
291 AuthorityMismatch,
292 #[error("fence reconciliation requires a pending acquisition mutation")]
294 NoPendingMutation,
295 #[error("fence reconciliation pending acquisition mismatch")]
297 MutationMismatch,
298 #[error("fence reconciliation requires a pending observation reservation")]
300 NoPendingObservation,
301 #[error("fence reconciliation observation attempt mismatch")]
303 ObservationMismatch,
304 #[error("fence reconciliation reserved request mismatch")]
306 RequestMismatch,
307 #[error(transparent)]
309 Obligation(#[from] FenceObligationError),
310 #[error(transparent)]
312 Plan(#[from] OperationPlanError),
313}
314#[derive(Debug, Error)]
316pub enum FenceReconciliationObservationError {
317 #[error("invalid fence reconciliation attempt identities")]
319 InvalidAttempts,
320 #[error("invalid fence reconciliation target count")]
322 InvalidTargetCount,
323 #[error("duplicate fence reconciliation target")]
325 DuplicateTarget,
326 #[error(transparent)]
328 Inventory(#[from] InventoryRecordError),
329}
330
331#[cfg(test)]
332mod tests;