ic_backup/model/fence_reconciliation/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord, MAX_OPERATION_ATTEMPTS},
6 consistency::ApplicationFenceEvidence,
7 evidence_digest::EvidenceDigest,
8 fence_obligation::{FenceObligationError, FenceObligationRecord},
9 inventory::{InventoryRecord, InventoryRecordError, MAX_INVENTORY_TARGETS},
10 operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
11 restore_safety::RestoreFenceEvidence,
12};
13use thiserror::Error;
14
15pub const MAX_FENCE_RECONCILIATION_REMOTE_OBSERVATIONS: u32 = 1;
20
21#[derive(Clone, Debug)]
27pub struct FenceReconciliationIntent<'a> {
28 plan: &'a OperationPlanRecord,
29 obligation: &'a FenceObligationRecord,
30 authority: AttemptAuthorityRecord,
31 mutation_attempt: u32,
32 challenge: ArtifactChecksumRecord,
33}
34impl<'a> FenceReconciliationIntent<'a> {
35 pub fn new(
42 plan: &'a OperationPlanRecord,
43 obligation: &'a FenceObligationRecord,
44 journal: &AttemptJournalRecord,
45 challenge: ArtifactChecksumRecord,
46 ) -> Result<Self, FenceReconciliationRequestError> {
47 obligation.validate_plan(plan)?;
48 let authority = plan.attempt_authority(obligation.acquisition_operation())?;
49 if journal.authority() != &authority {
50 return Err(FenceReconciliationRequestError::AuthorityMismatch);
51 }
52 let mutation_attempt = journal
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.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 framing = EvidenceDigest::new(b"ic-backup/fence-reconciliation/v1\0");
122 framing.checksum(obligation);
123 framing.checksum(authority);
124 framing.u32_be(mutation);
125 framing.checksum(challenge);
126 framing.finish()
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 .pending_observation()
150 .ok_or(FenceReconciliationRequestError::NoPendingObservation)?;
151 let request = Self {
152 intent,
153 observation_attempt,
154 };
155 request.validate_journal(journal)?;
156 Ok(request)
157 }
158 #[must_use]
160 pub const fn intent(&self) -> &FenceReconciliationIntent<'plan> {
161 self.intent
162 }
163 #[must_use]
165 pub const fn observation_attempt(&self) -> u32 {
166 self.observation_attempt
167 }
168 #[must_use]
170 pub fn digest(&self) -> ArtifactChecksumRecord {
171 self.intent.digest()
172 }
173 pub fn validate_journal(
179 &self,
180 journal: &AttemptJournalRecord,
181 ) -> Result<(), FenceReconciliationRequestError> {
182 crate::model::attempt_journal::ObservationReservation::validate_identity(
183 journal,
184 self.intent.authority(),
185 self.intent.mutation_attempt(),
186 self.observation_attempt,
187 self.digest().hash(),
188 )
189 .map_err(FenceReconciliationRequestError::from)
190 }
191}
192
193#[derive(Clone, Debug)]
195pub enum FenceReconciliationEvidence {
196 AcquiredCapture {
198 fence: Box<ApplicationFenceEvidence>,
200 attribution: ArtifactChecksumRecord,
202 },
203 AcquiredRestore {
205 fence: Box<RestoreFenceEvidence>,
207 attribution: ArtifactChecksumRecord,
209 },
210 NotAcquired {
212 exclusion: ArtifactChecksumRecord,
214 },
215 Unresolved {
217 uncertainty: ArtifactChecksumRecord,
219 },
220}
221#[derive(Clone, Debug)]
223pub struct FenceReconciliationObservationInput {
224 pub request: ArtifactChecksumRecord,
226 pub mutation_attempt: u32,
228 pub observation_attempt: u32,
230 pub context: PlanContextRecord,
232 pub inventory: InventoryRecord,
234 pub selected_targets: Vec<String>,
236 pub settlement: FenceReconciliationEvidence,
238 pub evidence: ArtifactChecksumRecord,
240 pub remote_observations: u32,
242}
243#[derive(Clone, Debug)]
245pub struct FenceReconciliationObservation {
246 input: FenceReconciliationObservationInput,
247}
248impl FenceReconciliationObservation {
249 pub fn new(
253 mut input: FenceReconciliationObservationInput,
254 ) -> Result<Self, FenceReconciliationObservationError> {
255 if input.mutation_attempt == 0
256 || input.observation_attempt <= input.mutation_attempt
257 || input.observation_attempt > MAX_OPERATION_ATTEMPTS
258 {
259 return Err(FenceReconciliationObservationError::InvalidAttempts);
260 }
261 if input.selected_targets.is_empty() || input.selected_targets.len() > MAX_INVENTORY_TARGETS
262 {
263 return Err(FenceReconciliationObservationError::InvalidTargetCount);
264 }
265 for target in &mut input.selected_targets {
266 *target = input.inventory.target(target)?.canister_id().into();
267 }
268 input.selected_targets.sort();
269 if input
270 .selected_targets
271 .windows(2)
272 .any(|pair| pair[0] == pair[1])
273 {
274 return Err(FenceReconciliationObservationError::DuplicateTarget);
275 }
276 Ok(Self { input })
277 }
278 #[must_use]
280 pub const fn input(&self) -> &FenceReconciliationObservationInput {
281 &self.input
282 }
283}
284
285#[derive(Debug, Error)]
287pub enum FenceReconciliationRequestError {
288 #[error("fence reconciliation original journal authority mismatch")]
290 AuthorityMismatch,
291 #[error("fence reconciliation requires a pending acquisition mutation")]
293 NoPendingMutation,
294 #[error("fence reconciliation pending acquisition mismatch")]
296 MutationMismatch,
297 #[error("fence reconciliation requires a pending observation reservation")]
299 NoPendingObservation,
300 #[error("fence reconciliation observation attempt mismatch")]
302 ObservationMismatch,
303 #[error("fence reconciliation reserved request mismatch")]
305 RequestMismatch,
306 #[error(transparent)]
308 Obligation(#[from] FenceObligationError),
309 #[error(transparent)]
311 Plan(#[from] OperationPlanError),
312}
313#[derive(Debug, Error)]
315pub enum FenceReconciliationObservationError {
316 #[error("invalid fence reconciliation attempt identities")]
318 InvalidAttempts,
319 #[error("invalid fence reconciliation target count")]
321 InvalidTargetCount,
322 #[error("duplicate fence reconciliation target")]
324 DuplicateTarget,
325 #[error(transparent)]
327 Inventory(#[from] InventoryRecordError),
328}
329
330#[cfg(test)]
331mod tests;
332
333impl From<crate::model::attempt_journal::ObservationReservationError>
334 for FenceReconciliationRequestError
335{
336 fn from(error: crate::model::attempt_journal::ObservationReservationError) -> Self {
337 use crate::model::attempt_journal::ObservationReservationError as Reservation;
338 match error {
339 Reservation::AuthorityMismatch => Self::AuthorityMismatch,
340 Reservation::NoPendingMutation => Self::NoPendingMutation,
341 Reservation::NoPendingObservation => Self::NoPendingObservation,
342 Reservation::MutationMismatch => Self::MutationMismatch,
343 Reservation::ObservationMismatch => Self::ObservationMismatch,
344 Reservation::RequestMismatch => Self::RequestMismatch,
345 }
346 }
347}