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