Skip to main content

ic_backup/model/fence_reconciliation/
mod.rs

1//! Original fence-acquisition observation identity and exact retained reservation binding.
2
3use 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
14/// Descriptive maximum underlying remote observations for this single reserved observation.
15///
16/// This is not spending authority. Every paid call requires prior durable accounting;
17/// this port cannot batch multiple remote calls under one observation reservation.
18pub const MAX_FENCE_RECONCILIATION_REMOTE_OBSERVATIONS: u32 = 1;
19
20/// Ephemeral original observation intent, derived before its durable reservation.
21///
22/// The original obligation must already be retained under its exact requirement
23/// and guarded plans. This pure binding authenticates no provider, request bytes
24/// or persisted declaration and grants no fresh dispatch authority.
25#[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    /// Bind the original pending acquisition, full obligation and caller-owned fresh challenge.
35    ///
36    /// Derivation never creates/resets a journal. Recovering the same intent may
37    /// associate a late reply; it never authorizes repeating a lost observation.
38    /// # Errors
39    /// Rejects another plan/journal/allowance or an acquisition without a pending mutation.
40    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    /// Read the exact original whole-unit obligation; never current Active custody.
64    #[must_use]
65    pub const fn obligation(&self) -> &FenceObligationRecord {
66        self.obligation
67    }
68    /// Read the original context, full inventory and selected unit.
69    #[must_use]
70    pub const fn plan(&self) -> &OperationPlanRecord {
71        self.plan
72    }
73    /// Read immutable acquisition identity and original attempt allowances.
74    #[must_use]
75    pub const fn authority(&self) -> &AttemptAuthorityRecord {
76        &self.authority
77    }
78    /// Read the exact unresolved original mutation attempt.
79    #[must_use]
80    pub const fn mutation_attempt(&self) -> u32 {
81        self.mutation_attempt
82    }
83    /// Read the integration-owned current challenge; equality does not prove freshness.
84    #[must_use]
85    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
86        &self.challenge
87    }
88    /// Hash NUL-terminated v1 domain, 64 ASCII obligation/authority hashes,
89    /// big-endian u32 mutation attempt and 64 ASCII challenge bytes.
90    ///
91    /// The digest is reserved before obtaining an observation attempt number.
92    /// Actual replies also bind that allocated number, preventing cross-attempt replay.
93    #[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/// Ephemeral observation request bound to the exact already-reserved original journal event.
130///
131/// Binding is structural, not a dispatch permit. Before invocation the integration
132/// admits durable custody, original dependencies, actual authority and no earlier
133/// dispatch of this reservation. Lost replies remain pending and cannot be resent.
134#[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    /// Bind only after the existing journal owner durably reserves this exact intent digest.
141    /// # Errors
142    /// Rejects absent/different pending mutation, observation, request or original authority.
143    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    /// Read the original challenge-bound intent retained by its caller.
160    #[must_use]
161    pub const fn intent(&self) -> &FenceReconciliationIntent<'plan> {
162        self.intent
163    }
164    /// Read the exact previously allocated observation attempt.
165    #[must_use]
166    pub const fn observation_attempt(&self) -> u32 {
167        self.observation_attempt
168    }
169    /// Read the same canonical observation digest reserved in the original journal.
170    #[must_use]
171    pub fn digest(&self) -> ArtifactChecksumRecord {
172        self.intent.digest()
173    }
174    /// Recheck exact current retained reservation before admitting any result.
175    ///
176    /// This pure check authenticates no receipt and never performs IO or scheduling.
177    /// # Errors
178    /// Rejects replaced/settled journal evidence, other attempts, request or authority.
179    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/// Passive application-qualified attribution; fence appearance alone is insufficient.
195#[derive(Clone, Debug)]
196pub enum FenceReconciliationEvidence {
197    /// Exact capture acquisition is exclusively attributed to the original request/attempt.
198    AcquiredCapture {
199        /// Actual Active whole-unit write/membership/timer/external-work/drain evidence.
200        fence: Box<ApplicationFenceEvidence>,
201        /// Original-request attribution, including exclusion of independent acquisitions.
202        attribution: ArtifactChecksumRecord,
203    },
204    /// Exact restore acquisition is attributed with outside-snapshot/replay-safe custody.
205    AcquiredRestore {
206        /// Actual retained fence, whole-selection, rewind-independent and replay-safety evidence.
207        fence: Box<RestoreFenceEvidence>,
208        /// Exact original-request attribution; not merely matching current fence identity.
209        attribution: ArtifactChecksumRecord,
210    },
211    /// Qualified exclusion proves this exact acquisition never applied.
212    NotAcquired {
213        /// Nonapplication proof includes exclusion of a transient acquire/release cycle.
214        exclusion: ArtifactChecksumRecord,
215    },
216    /// A settled authenticated observation cannot resolve the exact original acquisition.
217    Unresolved {
218        /// Retained uncertainty evidence; a lost reply cannot construct this variant.
219        uncertainty: ArtifactChecksumRecord,
220    },
221}
222/// Passive actual context/selection and attribution under one exact reserved observation.
223#[derive(Clone, Debug)]
224pub struct FenceReconciliationObservationInput {
225    /// Exact original challenge-bound observation request digest.
226    pub request: ArtifactChecksumRecord,
227    /// Actual associated original pending mutation attempt.
228    pub mutation_attempt: u32,
229    /// Actual associated already-allocated observation attempt.
230    pub observation_attempt: u32,
231    /// Actually authenticated network/caller/release, not copied declarations.
232    pub context: PlanContextRecord,
233    /// Full actual current inventory, including unselected parent metadata.
234    pub inventory: InventoryRecord,
235    /// Actual whole-unit coverage; model admits a bounded canonical unique set.
236    pub selected_targets: Vec<String>,
237    /// Actual application-qualified original-acquisition attribution or unresolved result.
238    pub settlement: FenceReconciliationEvidence,
239    /// Opaque evidence binding the exact request, attempts and all actual result fields.
240    pub evidence: ArtifactChecksumRecord,
241    /// Actual underlying remote observations; prior durable per-call accounting is mandatory.
242    pub remote_observations: u32,
243}
244/// Canonical immutable passive observation; no serialized authority or automatic receipt.
245#[derive(Clone, Debug)]
246pub struct FenceReconciliationObservation {
247    input: FenceReconciliationObservationInput,
248}
249impl FenceReconciliationObservation {
250    /// Admit bounded chronological attempt IDs and canonical exact actual selected targets.
251    /// # Errors
252    /// Rejects empty/excessive/duplicate/unknown targets or impossible attempt IDs.
253    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    /// Read immutable admitted passive fields; not authenticated by this accessor.
280    #[must_use]
281    pub const fn input(&self) -> &FenceReconciliationObservationInput {
282        &self.input
283    }
284}
285
286/// Typed original journal/reservation binding denial; never consumes or replenishes attempts.
287#[derive(Debug, Error)]
288pub enum FenceReconciliationRequestError {
289    /// Exact original journal context, operation, request or limits differ.
290    #[error("fence reconciliation original journal authority mismatch")]
291    AuthorityMismatch,
292    /// No original mutation is unresolved.
293    #[error("fence reconciliation requires a pending acquisition mutation")]
294    NoPendingMutation,
295    /// Original acquisition reservation changed or settled.
296    #[error("fence reconciliation pending acquisition mismatch")]
297    MutationMismatch,
298    /// The exact observation was not already reserved.
299    #[error("fence reconciliation requires a pending observation reservation")]
300    NoPendingObservation,
301    /// The retained observation changed or settled.
302    #[error("fence reconciliation observation attempt mismatch")]
303    ObservationMismatch,
304    /// The pending reservation binds another original observation digest.
305    #[error("fence reconciliation reserved request mismatch")]
306    RequestMismatch,
307    /// Full original fence obligation is not bound to the plan.
308    #[error(transparent)]
309    Obligation(#[from] FenceObligationError),
310    /// Original acquisition authority cannot be derived.
311    #[error(transparent)]
312    Plan(#[from] OperationPlanError),
313}
314/// Typed actual passive observation boundary denial.
315#[derive(Debug, Error)]
316pub enum FenceReconciliationObservationError {
317    /// Attempts must be chronological and within the existing total-attempt bound.
318    #[error("invalid fence reconciliation attempt identities")]
319    InvalidAttempts,
320    /// Actual whole-unit selection must contain 1..=1,024 members.
321    #[error("invalid fence reconciliation target count")]
322    InvalidTargetCount,
323    /// Equivalent physical IDs cannot appear twice.
324    #[error("duplicate fence reconciliation target")]
325    DuplicateTarget,
326    /// Physical selection cannot be admitted against actual inventory.
327    #[error(transparent)]
328    Inventory(#[from] InventoryRecordError),
329}
330
331#[cfg(test)]
332mod tests;