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 .view()
54 .pending_mutation
55 .ok_or(FenceReconciliationRequestError::NoPendingMutation)?;
56 Ok(Self {
57 plan,
58 obligation,
59 authority,
60 mutation_attempt,
61 challenge,
62 })
63 }
64 #[must_use]
66 pub const fn obligation(&self) -> &FenceObligationRecord {
67 self.obligation
68 }
69 #[must_use]
71 pub const fn plan(&self) -> &OperationPlanRecord {
72 self.plan
73 }
74 #[must_use]
76 pub const fn authority(&self) -> &AttemptAuthorityRecord {
77 &self.authority
78 }
79 #[must_use]
81 pub const fn mutation_attempt(&self) -> u32 {
82 self.mutation_attempt
83 }
84 #[must_use]
86 pub const fn challenge(&self) -> &ArtifactChecksumRecord {
87 &self.challenge
88 }
89 #[must_use]
95 pub fn digest(&self) -> ArtifactChecksumRecord {
96 observation_digest(
97 &self.obligation.digest(),
98 &self.authority.digest(),
99 self.mutation_attempt,
100 &self.challenge,
101 )
102 }
103 fn validate_mutation(
104 &self,
105 journal: &AttemptJournalRecord,
106 ) -> Result<(), FenceReconciliationRequestError> {
107 if journal.authority() != &self.authority {
108 return Err(FenceReconciliationRequestError::AuthorityMismatch);
109 }
110 if journal.view().pending_mutation != Some(self.mutation_attempt) {
111 return Err(FenceReconciliationRequestError::MutationMismatch);
112 }
113 Ok(())
114 }
115}
116fn observation_digest(
117 obligation: &ArtifactChecksumRecord,
118 authority: &ArtifactChecksumRecord,
119 mutation: u32,
120 challenge: &ArtifactChecksumRecord,
121) -> ArtifactChecksumRecord {
122 let mut framing = EvidenceDigest::new(b"ic-backup/fence-reconciliation/v1\0");
123 framing.checksum(obligation);
124 framing.checksum(authority);
125 framing.u32_be(mutation);
126 framing.checksum(challenge);
127 framing.finish()
128}
129
130#[derive(Clone, Debug)]
136pub struct FenceReconciliationRequest<'a, 'plan> {
137 intent: &'a FenceReconciliationIntent<'plan>,
138 observation_attempt: u32,
139}
140impl<'a, 'plan> FenceReconciliationRequest<'a, 'plan> {
141 pub fn new(
145 intent: &'a FenceReconciliationIntent<'plan>,
146 journal: &AttemptJournalRecord,
147 ) -> Result<Self, FenceReconciliationRequestError> {
148 intent.validate_mutation(journal)?;
149 let observation_attempt = journal
150 .view()
151 .pending_observation
152 .ok_or(FenceReconciliationRequestError::NoPendingObservation)?;
153 let request = Self {
154 intent,
155 observation_attempt,
156 };
157 request.validate_journal(journal)?;
158 Ok(request)
159 }
160 #[must_use]
162 pub const fn intent(&self) -> &FenceReconciliationIntent<'plan> {
163 self.intent
164 }
165 #[must_use]
167 pub const fn observation_attempt(&self) -> u32 {
168 self.observation_attempt
169 }
170 #[must_use]
172 pub fn digest(&self) -> ArtifactChecksumRecord {
173 self.intent.digest()
174 }
175 pub fn validate_journal(
181 &self,
182 journal: &AttemptJournalRecord,
183 ) -> Result<(), FenceReconciliationRequestError> {
184 self.intent.validate_mutation(journal)?;
185 if journal.view().pending_observation != Some(self.observation_attempt) {
186 return Err(FenceReconciliationRequestError::ObservationMismatch);
187 }
188 if journal.pending_observation_request() != Some(self.digest().hash()) {
189 return Err(FenceReconciliationRequestError::RequestMismatch);
190 }
191 Ok(())
192 }
193}
194
195#[derive(Clone, Debug)]
197pub enum FenceReconciliationEvidence {
198 AcquiredCapture {
200 fence: Box<ApplicationFenceEvidence>,
202 attribution: ArtifactChecksumRecord,
204 },
205 AcquiredRestore {
207 fence: Box<RestoreFenceEvidence>,
209 attribution: ArtifactChecksumRecord,
211 },
212 NotAcquired {
214 exclusion: ArtifactChecksumRecord,
216 },
217 Unresolved {
219 uncertainty: ArtifactChecksumRecord,
221 },
222}
223#[derive(Clone, Debug)]
225pub struct FenceReconciliationObservationInput {
226 pub request: ArtifactChecksumRecord,
228 pub mutation_attempt: u32,
230 pub observation_attempt: u32,
232 pub context: PlanContextRecord,
234 pub inventory: InventoryRecord,
236 pub selected_targets: Vec<String>,
238 pub settlement: FenceReconciliationEvidence,
240 pub evidence: ArtifactChecksumRecord,
242 pub remote_observations: u32,
244}
245#[derive(Clone, Debug)]
247pub struct FenceReconciliationObservation {
248 input: FenceReconciliationObservationInput,
249}
250impl FenceReconciliationObservation {
251 pub fn new(
255 mut input: FenceReconciliationObservationInput,
256 ) -> Result<Self, FenceReconciliationObservationError> {
257 if input.mutation_attempt == 0
258 || input.observation_attempt <= input.mutation_attempt
259 || input.observation_attempt > MAX_OPERATION_ATTEMPTS
260 {
261 return Err(FenceReconciliationObservationError::InvalidAttempts);
262 }
263 if input.selected_targets.is_empty() || input.selected_targets.len() > MAX_INVENTORY_TARGETS
264 {
265 return Err(FenceReconciliationObservationError::InvalidTargetCount);
266 }
267 for target in &mut input.selected_targets {
268 *target = input.inventory.target(target)?.canister_id().into();
269 }
270 input.selected_targets.sort();
271 if input
272 .selected_targets
273 .windows(2)
274 .any(|pair| pair[0] == pair[1])
275 {
276 return Err(FenceReconciliationObservationError::DuplicateTarget);
277 }
278 Ok(Self { input })
279 }
280 #[must_use]
282 pub const fn input(&self) -> &FenceReconciliationObservationInput {
283 &self.input
284 }
285}
286
287#[derive(Debug, Error)]
289pub enum FenceReconciliationRequestError {
290 #[error("fence reconciliation original journal authority mismatch")]
292 AuthorityMismatch,
293 #[error("fence reconciliation requires a pending acquisition mutation")]
295 NoPendingMutation,
296 #[error("fence reconciliation pending acquisition mismatch")]
298 MutationMismatch,
299 #[error("fence reconciliation requires a pending observation reservation")]
301 NoPendingObservation,
302 #[error("fence reconciliation observation attempt mismatch")]
304 ObservationMismatch,
305 #[error("fence reconciliation reserved request mismatch")]
307 RequestMismatch,
308 #[error(transparent)]
310 Obligation(#[from] FenceObligationError),
311 #[error(transparent)]
313 Plan(#[from] OperationPlanError),
314}
315#[derive(Debug, Error)]
317pub enum FenceReconciliationObservationError {
318 #[error("invalid fence reconciliation attempt identities")]
320 InvalidAttempts,
321 #[error("invalid fence reconciliation target count")]
323 InvalidTargetCount,
324 #[error("duplicate fence reconciliation target")]
326 DuplicateTarget,
327 #[error(transparent)]
329 Inventory(#[from] InventoryRecordError),
330}
331
332#[cfg(test)]
333mod tests;