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(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    /// 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.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/// 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            .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    /// Read the original challenge-bound intent retained by its caller.
159    #[must_use]
160    pub const fn intent(&self) -> &FenceReconciliationIntent<'plan> {
161        self.intent
162    }
163    /// Read the exact previously allocated observation attempt.
164    #[must_use]
165    pub const fn observation_attempt(&self) -> u32 {
166        self.observation_attempt
167    }
168    /// Read the same canonical observation digest reserved in the original journal.
169    #[must_use]
170    pub fn digest(&self) -> ArtifactChecksumRecord {
171        self.intent.digest()
172    }
173    /// Recheck exact current retained reservation before admitting any result.
174    ///
175    /// This pure check authenticates no receipt and never performs IO or scheduling.
176    /// # Errors
177    /// Rejects replaced/settled journal evidence, other attempts, request or authority.
178    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/// Passive application-qualified attribution; fence appearance alone is insufficient.
194#[derive(Clone, Debug)]
195pub enum FenceReconciliationEvidence {
196    /// Exact capture acquisition is exclusively attributed to the original request/attempt.
197    AcquiredCapture {
198        /// Actual Active whole-unit write/membership/timer/external-work/drain evidence.
199        fence: Box<ApplicationFenceEvidence>,
200        /// Original-request attribution, including exclusion of independent acquisitions.
201        attribution: ArtifactChecksumRecord,
202    },
203    /// Exact restore acquisition is attributed with outside-snapshot/replay-safe custody.
204    AcquiredRestore {
205        /// Actual retained fence, whole-selection, rewind-independent and replay-safety evidence.
206        fence: Box<RestoreFenceEvidence>,
207        /// Exact original-request attribution; not merely matching current fence identity.
208        attribution: ArtifactChecksumRecord,
209    },
210    /// Qualified exclusion proves this exact acquisition never applied.
211    NotAcquired {
212        /// Nonapplication proof includes exclusion of a transient acquire/release cycle.
213        exclusion: ArtifactChecksumRecord,
214    },
215    /// A settled authenticated observation cannot resolve the exact original acquisition.
216    Unresolved {
217        /// Retained uncertainty evidence; a lost reply cannot construct this variant.
218        uncertainty: ArtifactChecksumRecord,
219    },
220}
221/// Passive actual context/selection and attribution under one exact reserved observation.
222#[derive(Clone, Debug)]
223pub struct FenceReconciliationObservationInput {
224    /// Exact original challenge-bound observation request digest.
225    pub request: ArtifactChecksumRecord,
226    /// Actual associated original pending mutation attempt.
227    pub mutation_attempt: u32,
228    /// Actual associated already-allocated observation attempt.
229    pub observation_attempt: u32,
230    /// Actually authenticated network/caller/release, not copied declarations.
231    pub context: PlanContextRecord,
232    /// Full actual current inventory, including unselected parent metadata.
233    pub inventory: InventoryRecord,
234    /// Actual whole-unit coverage; model admits a bounded canonical unique set.
235    pub selected_targets: Vec<String>,
236    /// Actual application-qualified original-acquisition attribution or unresolved result.
237    pub settlement: FenceReconciliationEvidence,
238    /// Opaque evidence binding the exact request, attempts and all actual result fields.
239    pub evidence: ArtifactChecksumRecord,
240    /// Actual underlying remote observations; prior durable per-call accounting is mandatory.
241    pub remote_observations: u32,
242}
243/// Canonical immutable passive observation; no serialized authority or automatic receipt.
244#[derive(Clone, Debug)]
245pub struct FenceReconciliationObservation {
246    input: FenceReconciliationObservationInput,
247}
248impl FenceReconciliationObservation {
249    /// Admit bounded chronological attempt IDs and canonical exact actual selected targets.
250    /// # Errors
251    /// Rejects empty/excessive/duplicate/unknown targets or impossible attempt IDs.
252    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    /// Read immutable admitted passive fields; not authenticated by this accessor.
279    #[must_use]
280    pub const fn input(&self) -> &FenceReconciliationObservationInput {
281        &self.input
282    }
283}
284
285/// Typed original journal/reservation binding denial; never consumes or replenishes attempts.
286#[derive(Debug, Error)]
287pub enum FenceReconciliationRequestError {
288    /// Exact original journal context, operation, request or limits differ.
289    #[error("fence reconciliation original journal authority mismatch")]
290    AuthorityMismatch,
291    /// No original mutation is unresolved.
292    #[error("fence reconciliation requires a pending acquisition mutation")]
293    NoPendingMutation,
294    /// Original acquisition reservation changed or settled.
295    #[error("fence reconciliation pending acquisition mismatch")]
296    MutationMismatch,
297    /// The exact observation was not already reserved.
298    #[error("fence reconciliation requires a pending observation reservation")]
299    NoPendingObservation,
300    /// The retained observation changed or settled.
301    #[error("fence reconciliation observation attempt mismatch")]
302    ObservationMismatch,
303    /// The pending reservation binds another original observation digest.
304    #[error("fence reconciliation reserved request mismatch")]
305    RequestMismatch,
306    /// Full original fence obligation is not bound to the plan.
307    #[error(transparent)]
308    Obligation(#[from] FenceObligationError),
309    /// Original acquisition authority cannot be derived.
310    #[error(transparent)]
311    Plan(#[from] OperationPlanError),
312}
313/// Typed actual passive observation boundary denial.
314#[derive(Debug, Error)]
315pub enum FenceReconciliationObservationError {
316    /// Attempts must be chronological and within the existing total-attempt bound.
317    #[error("invalid fence reconciliation attempt identities")]
318    InvalidAttempts,
319    /// Actual whole-unit selection must contain 1..=1,024 members.
320    #[error("invalid fence reconciliation target count")]
321    InvalidTargetCount,
322    /// Equivalent physical IDs cannot appear twice.
323    #[error("duplicate fence reconciliation target")]
324    DuplicateTarget,
325    /// Physical selection cannot be admitted against actual inventory.
326    #[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}