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    evidence_digest::EvidenceDigest,
13    inventory::{InventoryRecord, MAX_INVENTORY_TARGETS},
14    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
15};
16use thiserror::Error;
17
18/// Maximum descriptive remote observations per invocation; no paid-call allowance.
19pub const MAX_CONSISTENCY_REMOTE_OBSERVATIONS: u32 = 1024;
20/// Explicit capture boundary; before/after equality alone establishes no continuity.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ConsistencyBoundary {
23    /// Check current evidence before capture under independently qualified effect admission.
24    BeforeCapture,
25    /// Check current evidence after capture; not a completion receipt.
26    AfterCapture,
27}
28/// Exact integration-retained application fence and original membership revision.
29///
30/// Integrations recover this binding from durable obligation evidence; constructing
31/// it proves neither retention nor current active custody and grants no release authority.
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct ApplicationFenceBinding {
34    /// Exact retained fence identity, never a release token.
35    pub identity: ArtifactChecksumRecord,
36    /// Original membership revision bound when the retained fence was established.
37    pub membership_revision: ArtifactChecksumRecord,
38}
39/// Passive request parameters; no default capture or fence authority.
40#[derive(Clone, Debug)]
41pub struct ConsistencyRequestInput {
42    /// Exact original operation sequence; request payload remains bound through full intent.
43    pub operation_sequence: u64,
44    /// Fresh integration-owned challenge; digest binding alone proves no freshness.
45    pub challenge: ArtifactChecksumRecord,
46    /// Explicit capture boundary.
47    pub boundary: ConsistencyBoundary,
48    /// Retained exact fence/revision binding required only for coordinated capture.
49    pub expected_fence: Option<ApplicationFenceBinding>,
50    /// Descriptive call ceiling, independent of any original or preflight spending allowance.
51    pub max_remote_observations: u32,
52}
53/// Ephemeral current request under the exact original consistency declaration and plan.
54#[derive(Clone, Debug)]
55pub struct ConsistencyRequest<'a> {
56    plan: &'a OperationPlanRecord,
57    requirement: &'a ConsistencyRequirementRecord,
58    binding: OperationBindingRecord,
59    input: ConsistencyRequestInput,
60}
61impl<'a> ConsistencyRequest<'a> {
62    /// Bind original requirement/operation to an explicit challenge/boundary/fence identity.
63    ///
64    /// The integration recovers the expected fence from its durable obligation owner.
65    /// This constructor cannot acquire/recover/release a fence or inspect that evidence.
66    /// # Errors
67    /// Rejects plan mismatch, unknown operation, inappropriate/missing fence or excess ceiling.
68    pub fn new(
69        plan: &'a OperationPlanRecord,
70        requirement: &'a ConsistencyRequirementRecord,
71        input: ConsistencyRequestInput,
72    ) -> Result<Self, ConsistencyRequestError> {
73        requirement.validate_plan(plan)?;
74        if input.max_remote_observations > MAX_CONSISTENCY_REMOTE_OBSERVATIONS {
75            return Err(ConsistencyRequestError::ObservationLimitTooLarge);
76        }
77        match (requirement.guarantee(), input.expected_fence.as_ref()) {
78            (ConsistencyGuaranteeRecord::ApplicationCoordinated, None) => {
79                return Err(ConsistencyRequestError::FenceRequired);
80            }
81            (ConsistencyGuaranteeRecord::PerCanister, Some(_)) => {
82                return Err(ConsistencyRequestError::UnexpectedFence);
83            }
84            _ => {}
85        }
86        let binding = plan
87            .attempt_authority(input.operation_sequence)?
88            .binding()
89            .clone();
90        Ok(Self {
91            plan,
92            requirement,
93            binding,
94            input,
95        })
96    }
97    /// Read exact original mutation identity; no codec, dispatch or current permission admission.
98    #[must_use]
99    pub const fn binding(&self) -> &OperationBindingRecord {
100        &self.binding
101    }
102    /// Read full original inventory, including unselected parents/metadata.
103    #[must_use]
104    pub const fn inventory(&self) -> &InventoryRecord {
105        self.plan.inventory()
106    }
107    /// Read exact original selected principals in canonical order, not dispatch order.
108    #[must_use]
109    pub fn selected_targets(&self) -> &[String] {
110        self.plan.selected_targets()
111    }
112    /// Read immutable original requested guarantee.
113    #[must_use]
114    pub const fn requirement(&self) -> &ConsistencyRequirementRecord {
115        self.requirement
116    }
117    /// Read caller-owned challenge; matching digests alone are not freshness proof.
118    #[must_use]
119    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
120        &self.input.challenge
121    }
122    /// Read explicit capture boundary.
123    #[must_use]
124    pub const fn boundary(&self) -> ConsistencyBoundary {
125        self.input.boundary
126    }
127    /// Read retained exact expected fence/revision binding; not proof of current active custody.
128    #[must_use]
129    pub const fn expected_fence(&self) -> Option<&ApplicationFenceBinding> {
130        self.input.expected_fence.as_ref()
131    }
132    /// Read descriptive call ceiling; grants no spending/retry admission.
133    #[must_use]
134    pub const fn max_remote_observations(&self) -> u32 {
135        self.input.max_remote_observations
136    }
137    /// Hash original requirement, operation, challenge, boundary, expected fence and ceiling.
138    ///
139    /// Domain is NUL-terminated ASCII v1, then 64 ASCII requirement-digest bytes,
140    /// u64 BE sequence, 64 ASCII challenge bytes, boundary byte (before=0/after=1),
141    /// fence presence byte (0/1), optional 64 ASCII identity and 64 ASCII original
142    /// membership-revision bytes, then u32 BE ceiling.
143    #[must_use]
144    pub fn digest(&self) -> ArtifactChecksumRecord {
145        let mut framing = EvidenceDigest::new(b"ic-backup/consistency-request/v1\0");
146        framing.checksum(&self.requirement.digest());
147        framing.u64_be(self.binding.operation_sequence());
148        framing.checksum(self.challenge());
149        framing.byte(match self.boundary() {
150            ConsistencyBoundary::BeforeCapture => 0,
151            ConsistencyBoundary::AfterCapture => 1,
152        });
153        match self.expected_fence() {
154            None => framing.byte(0),
155            Some(fence) => {
156                framing.byte(1);
157                framing.checksum(&fence.identity);
158                framing.checksum(&fence.membership_revision);
159            }
160        }
161        framing.u32_be(self.max_remote_observations());
162        framing.finish()
163    }
164}
165/// Actual observed lifecycle at the capture boundary; not a management reply codec.
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub enum CaptureState {
168    /// Target is currently running; deny capture consistency admission.
169    Running,
170    /// Target has not completed stopping; deny capture consistency admission.
171    Stopping,
172    /// Target is observed stopped; integration must also qualify drained-work evidence.
173    Stopped,
174}
175/// Passive exact target state and opaque stopped/drained evidence qualified by its owner.
176#[derive(Clone, Debug)]
177pub struct TargetCaptureEvidence {
178    /// Actually observed physical target, canonicalized at observation admission.
179    pub target: String,
180    /// Actually observed lifecycle; process success cannot substitute.
181    pub state: CaptureState,
182    /// Required integration-qualified stopped/drained-work evidence; digest alone is no proof.
183    pub stopped_and_drained: ArtifactChecksumRecord,
184}
185impl super::observed_targets::ObservedTargetRow for TargetCaptureEvidence {
186    fn target(&self) -> &str {
187        &self.target
188    }
189
190    fn target_mut(&mut self) -> &mut String {
191        &mut self.target
192    }
193}
194/// Actually observed application fence state; not a serialized accepted/proven flag.
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub enum ApplicationFenceState {
197    /// Exact retained fence is qualified currently active with continuous custody.
198    Active,
199    /// Exact retained fence is known inactive; deny coordinated consistency.
200    Inactive,
201}
202/// Passive whole-selection fence evidence qualified by the application integration.
203///
204/// All evidence applies to the exact current request/context/full inventory/selection.
205/// The integration proves continuous retained custody across the capture window and
206/// interruption. Matching identities/revisions alone cannot prove that property.
207#[derive(Clone, Debug)]
208pub struct ApplicationFenceEvidence {
209    /// Actually observed active/inactive state; unknown custody must fail at the provider.
210    pub state: ApplicationFenceState,
211    /// Exact active retained fence identity; never a release token.
212    pub identity: ArtifactChecksumRecord,
213    /// Qualified current membership authority revision bound to this active fence.
214    pub membership_revision: ArtifactChecksumRecord,
215    /// Evidence application writes are fenced for the whole selected unit.
216    pub writes: ArtifactChecksumRecord,
217    /// Evidence membership changes are fenced for the whole selected unit.
218    pub membership: ArtifactChecksumRecord,
219    /// Evidence relevant application timers are fenced.
220    pub timers: ArtifactChecksumRecord,
221    /// Evidence relevant external work is fenced; not restore/payment settlement.
222    pub external_work: ArtifactChecksumRecord,
223    /// Evidence application work has drained under retained fence custody.
224    pub drained_work: ArtifactChecksumRecord,
225}
226/// Current declared evidence lane; neither variant is self-authenticating.
227#[derive(Clone, Debug)]
228pub enum ConsistencyEvidence {
229    /// Only each exact stopped target is qualified; no atomic application claim.
230    PerCanister,
231    /// Application integration qualifies this exact currently active whole-selection fence.
232    ApplicationCoordinated(Box<ApplicationFenceEvidence>),
233}
234/// Passive provider data; accepted flags, expiry/defaults and JSON authority admission are absent.
235#[derive(Clone, Debug)]
236pub struct ConsistencyObservationInput {
237    /// Exact current request digest.
238    pub request: ArtifactChecksumRecord,
239    /// Actually observed canonical network/caller/release.
240    pub context: PlanContextRecord,
241    /// Complete actually observed current inventory, including unselected parent metadata.
242    pub inventory: InventoryRecord,
243    /// Exact actual selected target states/evidence; admitted canonically by the model.
244    pub targets: Vec<TargetCaptureEvidence>,
245    /// Actually observed optional membership revision; coordinated evidence requires a match.
246    pub membership_revision: Option<ArtifactChecksumRecord>,
247    /// Actual current evidence lane; policy requires the original declared guarantee.
248    pub consistency: ConsistencyEvidence,
249    /// Required opaque integration-qualified observation evidence.
250    pub evidence: ArtifactChecksumRecord,
251    /// Actual remote observations, each requiring separate prior approved accounting.
252    pub remote_observations: u32,
253}
254/// Immutable canonical current evidence; not persisted proof or a fence-release capability.
255#[derive(Clone, Debug)]
256pub struct ConsistencyObservation {
257    input: ConsistencyObservationInput,
258}
259impl ConsistencyObservation {
260    /// Normalize/sort exact targets, require a nonempty bounded unique inventory-backed set.
261    /// # Errors
262    /// Rejects target count, malformed identities, canonical duplicates or unknown targets.
263    pub fn new(
264        mut input: ConsistencyObservationInput,
265    ) -> Result<Self, ConsistencyObservationError> {
266        input.targets = super::observed_targets::canonicalize(
267            input.targets,
268            &input.inventory,
269            || ConsistencyObservationError::InvalidTargetCount,
270            || ConsistencyObservationError::InvalidPrincipal,
271            || ConsistencyObservationError::TargetAbsentFromInventory,
272            || 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;