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    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
15/// Descriptive maximum underlying remote observations for this single reserved observation.
16///
17/// This is not spending authority. Every paid call requires prior durable accounting;
18/// this port cannot batch multiple remote calls under one observation reservation.
19pub const MAX_FENCE_RECONCILIATION_REMOTE_OBSERVATIONS: u32 = 1;
20
21/// Ephemeral original observation intent, derived before its durable reservation.
22///
23/// The original obligation must already be retained under its exact requirement
24/// and guarded plans. This pure binding authenticates no provider, request bytes
25/// or persisted declaration and grants no fresh dispatch authority.
26#[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    /// Bind the original pending acquisition, full obligation and caller-owned fresh challenge.
36    ///
37    /// Derivation never creates/resets a journal. Recovering the same intent may
38    /// associate a late reply; it never authorizes repeating a lost observation.
39    /// # Errors
40    /// Rejects another plan/journal/allowance or an acquisition without a pending mutation.
41    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_for_journal(
49            obligation.acquisition_operation(),
50            journal,
51            FenceReconciliationRequestError::AuthorityMismatch,
52        )?;
53        let mutation_attempt = journal
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    /// Read the exact original whole-unit obligation; never current Active custody.
65    #[must_use]
66    pub const fn obligation(&self) -> &FenceObligationRecord {
67        self.obligation
68    }
69    /// Read the original context, full inventory and selected unit.
70    #[must_use]
71    pub const fn plan(&self) -> &OperationPlanRecord {
72        self.plan
73    }
74    /// Read immutable acquisition identity and original attempt allowances.
75    #[must_use]
76    pub const fn authority(&self) -> &AttemptAuthorityRecord {
77        &self.authority
78    }
79    /// Read the exact unresolved original mutation attempt.
80    #[must_use]
81    pub const fn mutation_attempt(&self) -> u32 {
82        self.mutation_attempt
83    }
84    /// Read the integration-owned current challenge; equality does not prove freshness.
85    #[must_use]
86    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
87        &self.challenge
88    }
89    /// Hash NUL-terminated v1 domain, 64 ASCII obligation/authority hashes,
90    /// big-endian u32 mutation attempt and 64 ASCII challenge bytes.
91    ///
92    /// The digest is reserved before obtaining an observation attempt number.
93    /// Actual replies also bind that allocated number, preventing cross-attempt replay.
94    #[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.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/// Ephemeral observation request bound to the exact already-reserved original journal event.
131///
132/// Binding is structural, not a dispatch permit. Before invocation the integration
133/// admits durable custody, original dependencies, actual authority and no earlier
134/// dispatch of this reservation. Lost replies remain pending and cannot be resent.
135#[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    /// Bind only after the existing journal owner durably reserves this exact intent digest.
142    /// # Errors
143    /// Rejects absent/different pending mutation, observation, request or original authority.
144    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            .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        crate::model::attempt_journal::ObservationReservation::validate_identity(
184            journal,
185            self.intent.authority(),
186            self.intent.mutation_attempt(),
187            self.observation_attempt,
188            self.digest().hash(),
189        )
190        .map_err(FenceReconciliationRequestError::from)
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;
333
334impl From<crate::model::attempt_journal::ObservationReservationError>
335    for FenceReconciliationRequestError
336{
337    fn from(error: crate::model::attempt_journal::ObservationReservationError) -> Self {
338        use crate::model::attempt_journal::ObservationReservationError as Reservation;
339        match error {
340            Reservation::AuthorityMismatch => Self::AuthorityMismatch,
341            Reservation::NoPendingMutation => Self::NoPendingMutation,
342            Reservation::NoPendingObservation => Self::NoPendingObservation,
343            Reservation::MutationMismatch => Self::MutationMismatch,
344            Reservation::ObservationMismatch => Self::ObservationMismatch,
345            Reservation::RequestMismatch => Self::RequestMismatch,
346        }
347    }
348}