Skip to main content

ic_backup/model/consistency/
mod.rs

1//! Original declared consistency and ephemeral current capture/fence evidence.
2
3mod requirement;
4pub use requirement::{
5    ConsistencyGuaranteeRecord, ConsistencyRequirementError, ConsistencyRequirementRecord,
6    MAX_CONSISTENCY_REQUIREMENT_BYTES,
7};
8
9use crate::model::{
10    artifacts::ArtifactChecksumRecord,
11    attempt_journal::OperationBindingRecord,
12    inventory::{InventoryRecord, MAX_INVENTORY_TARGETS},
13    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
14};
15use thiserror::Error;
16
17/// Maximum descriptive remote observations per invocation; no paid-call allowance.
18pub const MAX_CONSISTENCY_REMOTE_OBSERVATIONS: u32 = 1024;
19/// Explicit capture boundary; before/after equality alone establishes no continuity.
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum ConsistencyBoundary {
22    /// Check current evidence before capture under independently qualified effect admission.
23    BeforeCapture,
24    /// Check current evidence after capture; not a completion receipt.
25    AfterCapture,
26}
27/// Exact integration-retained application fence and original membership revision.
28///
29/// Integrations recover this binding from durable obligation evidence; constructing
30/// it proves neither retention nor current active custody and grants no release authority.
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct ApplicationFenceBinding {
33    /// Exact retained fence identity, never a release token.
34    pub identity: ArtifactChecksumRecord,
35    /// Original membership revision bound when the retained fence was established.
36    pub membership_revision: ArtifactChecksumRecord,
37}
38/// Passive request parameters; no default capture or fence authority.
39#[derive(Clone, Debug)]
40pub struct ConsistencyRequestInput {
41    /// Exact original operation sequence; request payload remains bound through full intent.
42    pub operation_sequence: u64,
43    /// Fresh integration-owned challenge; digest binding alone proves no freshness.
44    pub challenge: ArtifactChecksumRecord,
45    /// Explicit capture boundary.
46    pub boundary: ConsistencyBoundary,
47    /// Retained exact fence/revision binding required only for coordinated capture.
48    pub expected_fence: Option<ApplicationFenceBinding>,
49    /// Descriptive call ceiling, independent of any original or preflight spending allowance.
50    pub max_remote_observations: u32,
51}
52/// Ephemeral current request under the exact original consistency declaration and plan.
53#[derive(Clone, Debug)]
54pub struct ConsistencyRequest<'a> {
55    plan: &'a OperationPlanRecord,
56    requirement: &'a ConsistencyRequirementRecord,
57    binding: OperationBindingRecord,
58    input: ConsistencyRequestInput,
59}
60impl<'a> ConsistencyRequest<'a> {
61    /// Bind original requirement/operation to an explicit challenge/boundary/fence identity.
62    ///
63    /// The integration recovers the expected fence from its durable obligation owner.
64    /// This constructor cannot acquire/recover/release a fence or inspect that evidence.
65    /// # Errors
66    /// Rejects plan mismatch, unknown operation, inappropriate/missing fence or excess ceiling.
67    pub fn new(
68        plan: &'a OperationPlanRecord,
69        requirement: &'a ConsistencyRequirementRecord,
70        input: ConsistencyRequestInput,
71    ) -> Result<Self, ConsistencyRequestError> {
72        requirement.validate_plan(plan)?;
73        if input.max_remote_observations > MAX_CONSISTENCY_REMOTE_OBSERVATIONS {
74            return Err(ConsistencyRequestError::ObservationLimitTooLarge);
75        }
76        match (requirement.guarantee(), input.expected_fence.as_ref()) {
77            (ConsistencyGuaranteeRecord::ApplicationCoordinated, None) => {
78                return Err(ConsistencyRequestError::FenceRequired);
79            }
80            (ConsistencyGuaranteeRecord::PerCanister, Some(_)) => {
81                return Err(ConsistencyRequestError::UnexpectedFence);
82            }
83            _ => {}
84        }
85        let binding = plan
86            .attempt_authority(input.operation_sequence)?
87            .binding()
88            .clone();
89        Ok(Self {
90            plan,
91            requirement,
92            binding,
93            input,
94        })
95    }
96    /// Read exact original mutation identity; no codec, dispatch or current permission admission.
97    #[must_use]
98    pub const fn binding(&self) -> &OperationBindingRecord {
99        &self.binding
100    }
101    /// Read full original inventory, including unselected parents/metadata.
102    #[must_use]
103    pub const fn inventory(&self) -> &InventoryRecord {
104        self.plan.inventory()
105    }
106    /// Read exact original selected principals in canonical order, not dispatch order.
107    #[must_use]
108    pub fn selected_targets(&self) -> &[String] {
109        self.plan.selected_targets()
110    }
111    /// Read immutable original requested guarantee.
112    #[must_use]
113    pub const fn requirement(&self) -> &ConsistencyRequirementRecord {
114        self.requirement
115    }
116    /// Read caller-owned challenge; matching digests alone are not freshness proof.
117    #[must_use]
118    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
119        &self.input.challenge
120    }
121    /// Read explicit capture boundary.
122    #[must_use]
123    pub const fn boundary(&self) -> ConsistencyBoundary {
124        self.input.boundary
125    }
126    /// Read retained exact expected fence/revision binding; not proof of current active custody.
127    #[must_use]
128    pub const fn expected_fence(&self) -> Option<&ApplicationFenceBinding> {
129        self.input.expected_fence.as_ref()
130    }
131    /// Read descriptive call ceiling; grants no spending/retry admission.
132    #[must_use]
133    pub const fn max_remote_observations(&self) -> u32 {
134        self.input.max_remote_observations
135    }
136    /// Hash original requirement, operation, challenge, boundary, expected fence and ceiling.
137    ///
138    /// Domain is NUL-terminated ASCII v1, then 64 ASCII requirement-digest bytes,
139    /// u64 BE sequence, 64 ASCII challenge bytes, boundary byte (before=0/after=1),
140    /// fence presence byte (0/1), optional 64 ASCII identity and 64 ASCII original
141    /// membership-revision bytes, then u32 BE ceiling.
142    #[must_use]
143    pub fn digest(&self) -> ArtifactChecksumRecord {
144        let mut bytes = b"ic-backup/consistency-request/v1\0".to_vec();
145        bytes.extend_from_slice(self.requirement.digest().hash().as_bytes());
146        bytes.extend_from_slice(&self.binding.operation_sequence().to_be_bytes());
147        bytes.extend_from_slice(self.challenge().hash().as_bytes());
148        bytes.push(match self.boundary() {
149            ConsistencyBoundary::BeforeCapture => 0,
150            ConsistencyBoundary::AfterCapture => 1,
151        });
152        match self.expected_fence() {
153            None => bytes.push(0),
154            Some(fence) => {
155                bytes.push(1);
156                bytes.extend_from_slice(fence.identity.hash().as_bytes());
157                bytes.extend_from_slice(fence.membership_revision.hash().as_bytes());
158            }
159        }
160        bytes.extend_from_slice(&self.max_remote_observations().to_be_bytes());
161        ArtifactChecksumRecord::from_bytes(&bytes)
162    }
163}
164/// Actual observed lifecycle at the capture boundary; not a management reply codec.
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
166pub enum CaptureState {
167    /// Target is currently running; deny capture consistency admission.
168    Running,
169    /// Target has not completed stopping; deny capture consistency admission.
170    Stopping,
171    /// Target is observed stopped; integration must also qualify drained-work evidence.
172    Stopped,
173}
174/// Passive exact target state and opaque stopped/drained evidence qualified by its owner.
175#[derive(Clone, Debug)]
176pub struct TargetCaptureEvidence {
177    /// Actually observed physical target, canonicalized at observation admission.
178    pub target: String,
179    /// Actually observed lifecycle; process success cannot substitute.
180    pub state: CaptureState,
181    /// Required integration-qualified stopped/drained-work evidence; digest alone is no proof.
182    pub stopped_and_drained: ArtifactChecksumRecord,
183}
184/// Actually observed application fence state; not a serialized accepted/proven flag.
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
186pub enum ApplicationFenceState {
187    /// Exact retained fence is qualified currently active with continuous custody.
188    Active,
189    /// Exact retained fence is known inactive; deny coordinated consistency.
190    Inactive,
191}
192/// Passive whole-selection fence evidence qualified by the application integration.
193///
194/// All evidence applies to the exact current request/context/full inventory/selection.
195/// The integration proves continuous retained custody across the capture window and
196/// interruption. Matching identities/revisions alone cannot prove that property.
197#[derive(Clone, Debug)]
198pub struct ApplicationFenceEvidence {
199    /// Actually observed active/inactive state; unknown custody must fail at the provider.
200    pub state: ApplicationFenceState,
201    /// Exact active retained fence identity; never a release token.
202    pub identity: ArtifactChecksumRecord,
203    /// Qualified current membership authority revision bound to this active fence.
204    pub membership_revision: ArtifactChecksumRecord,
205    /// Evidence application writes are fenced for the whole selected unit.
206    pub writes: ArtifactChecksumRecord,
207    /// Evidence membership changes are fenced for the whole selected unit.
208    pub membership: ArtifactChecksumRecord,
209    /// Evidence relevant application timers are fenced.
210    pub timers: ArtifactChecksumRecord,
211    /// Evidence relevant external work is fenced; not restore/payment settlement.
212    pub external_work: ArtifactChecksumRecord,
213    /// Evidence application work has drained under retained fence custody.
214    pub drained_work: ArtifactChecksumRecord,
215}
216/// Current declared evidence lane; neither variant is self-authenticating.
217#[derive(Clone, Debug)]
218pub enum ConsistencyEvidence {
219    /// Only each exact stopped target is qualified; no atomic application claim.
220    PerCanister,
221    /// Application integration qualifies this exact currently active whole-selection fence.
222    ApplicationCoordinated(Box<ApplicationFenceEvidence>),
223}
224/// Passive provider data; accepted flags, expiry/defaults and JSON authority admission are absent.
225#[derive(Clone, Debug)]
226pub struct ConsistencyObservationInput {
227    /// Exact current request digest.
228    pub request: ArtifactChecksumRecord,
229    /// Actually observed canonical network/caller/release.
230    pub context: PlanContextRecord,
231    /// Complete actually observed current inventory, including unselected parent metadata.
232    pub inventory: InventoryRecord,
233    /// Exact actual selected target states/evidence; admitted canonically by the model.
234    pub targets: Vec<TargetCaptureEvidence>,
235    /// Actually observed optional membership revision; coordinated evidence requires a match.
236    pub membership_revision: Option<ArtifactChecksumRecord>,
237    /// Actual current evidence lane; policy requires the original declared guarantee.
238    pub consistency: ConsistencyEvidence,
239    /// Required opaque integration-qualified observation evidence.
240    pub evidence: ArtifactChecksumRecord,
241    /// Actual remote observations, each requiring separate prior approved accounting.
242    pub remote_observations: u32,
243}
244/// Immutable canonical current evidence; not persisted proof or a fence-release capability.
245#[derive(Clone, Debug)]
246pub struct ConsistencyObservation {
247    input: ConsistencyObservationInput,
248}
249impl ConsistencyObservation {
250    /// Normalize/sort exact targets, require a nonempty bounded unique inventory-backed set.
251    /// # Errors
252    /// Rejects target count, malformed identities, canonical duplicates or unknown targets.
253    pub fn new(
254        mut input: ConsistencyObservationInput,
255    ) -> Result<Self, ConsistencyObservationError> {
256        if input.targets.is_empty() || input.targets.len() > MAX_INVENTORY_TARGETS {
257            return Err(ConsistencyObservationError::InvalidTargetCount);
258        }
259        for target in &mut input.targets {
260            target.target = super::principal::canonical_text(&target.target)
261                .ok_or(ConsistencyObservationError::InvalidPrincipal)?;
262            if input.inventory.target(&target.target).is_err() {
263                return Err(ConsistencyObservationError::TargetAbsentFromInventory);
264            }
265        }
266        input.targets.sort_by(|a, b| a.target.cmp(&b.target));
267        if input
268            .targets
269            .windows(2)
270            .any(|pair| pair[0].target == pair[1].target)
271        {
272            return Err(ConsistencyObservationError::DuplicateTarget);
273        }
274        Ok(Self { input })
275    }
276    /// Read current request identity.
277    #[must_use]
278    pub const fn request(&self) -> &ArtifactChecksumRecord {
279        &self.input.request
280    }
281    /// Read actually observed canonical context.
282    #[must_use]
283    pub const fn context(&self) -> &PlanContextRecord {
284        &self.input.context
285    }
286    /// Read complete actual current inventory.
287    #[must_use]
288    pub const fn inventory(&self) -> &InventoryRecord {
289        &self.input.inventory
290    }
291    /// Read exact canonical actual target state/evidence rows.
292    #[must_use]
293    pub fn targets(&self) -> &[TargetCaptureEvidence] {
294        &self.input.targets
295    }
296    /// Read actual membership revision; equality alone grants no continuity.
297    #[must_use]
298    pub const fn membership_revision(&self) -> Option<&ArtifactChecksumRecord> {
299        self.input.membership_revision.as_ref()
300    }
301    /// Read current evidence lane.
302    #[must_use]
303    pub const fn consistency(&self) -> &ConsistencyEvidence {
304        &self.input.consistency
305    }
306    /// Read opaque qualified observation evidence.
307    #[must_use]
308    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
309        &self.input.evidence
310    }
311    /// Read actual call reporting without spending/replenishment.
312    #[must_use]
313    pub const fn remote_observations(&self) -> u32 {
314        self.input.remote_observations
315    }
316}
317/// Typed current canonical evidence admission failure.
318#[derive(Debug, Eq, Error, PartialEq)]
319pub enum ConsistencyObservationError {
320    /// Exact actual targets must fit the inventory bound and be nonempty.
321    #[error("consistency targets must contain 1..={MAX_INVENTORY_TARGETS} entries")]
322    InvalidTargetCount,
323    /// Target principal is invalid or exceeds its own bound.
324    #[error("invalid consistency target principal")]
325    InvalidPrincipal,
326    /// Equivalent target identities cannot appear twice.
327    #[error("duplicate consistency target")]
328    DuplicateTarget,
329    /// Actual target is not in the actually observed inventory.
330    #[error("consistency target absent from actual inventory")]
331    TargetAbsentFromInventory,
332}
333/// Typed original requirement/request denial; no failure acquires/releases a fence.
334#[derive(Debug, Error)]
335pub enum ConsistencyRequestError {
336    /// Original requirement does not match the original plan.
337    #[error(transparent)]
338    Requirement(#[from] ConsistencyRequirementError),
339    /// Original plan rejects exact operation derivation.
340    #[error(transparent)]
341    Plan(#[from] OperationPlanError),
342    /// Coordinated capture must name the retained exact fence obligation.
343    #[error("coordinated consistency requires retained fence identity")]
344    FenceRequired,
345    /// Per-canister guarantee cannot silently acquire/upgrade an application fence.
346    #[error("per-canister consistency does not admit expected fence identity")]
347    UnexpectedFence,
348    /// Descriptive call ceiling exceeds its maintained bound.
349    #[error("consistency observation ceiling exceeds {MAX_CONSISTENCY_REMOTE_OBSERVATIONS}")]
350    ObservationLimitTooLarge,
351}
352
353#[cfg(test)]
354mod tests;