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}
184impl super::observed_targets::ObservedTargetRow for TargetCaptureEvidence {
185    fn target(&self) -> &str {
186        &self.target
187    }
188
189    fn target_mut(&mut self) -> &mut String {
190        &mut self.target
191    }
192}
193/// Actually observed application fence state; not a serialized accepted/proven flag.
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195pub enum ApplicationFenceState {
196    /// Exact retained fence is qualified currently active with continuous custody.
197    Active,
198    /// Exact retained fence is known inactive; deny coordinated consistency.
199    Inactive,
200}
201/// Passive whole-selection fence evidence qualified by the application integration.
202///
203/// All evidence applies to the exact current request/context/full inventory/selection.
204/// The integration proves continuous retained custody across the capture window and
205/// interruption. Matching identities/revisions alone cannot prove that property.
206#[derive(Clone, Debug)]
207pub struct ApplicationFenceEvidence {
208    /// Actually observed active/inactive state; unknown custody must fail at the provider.
209    pub state: ApplicationFenceState,
210    /// Exact active retained fence identity; never a release token.
211    pub identity: ArtifactChecksumRecord,
212    /// Qualified current membership authority revision bound to this active fence.
213    pub membership_revision: ArtifactChecksumRecord,
214    /// Evidence application writes are fenced for the whole selected unit.
215    pub writes: ArtifactChecksumRecord,
216    /// Evidence membership changes are fenced for the whole selected unit.
217    pub membership: ArtifactChecksumRecord,
218    /// Evidence relevant application timers are fenced.
219    pub timers: ArtifactChecksumRecord,
220    /// Evidence relevant external work is fenced; not restore/payment settlement.
221    pub external_work: ArtifactChecksumRecord,
222    /// Evidence application work has drained under retained fence custody.
223    pub drained_work: ArtifactChecksumRecord,
224}
225/// Current declared evidence lane; neither variant is self-authenticating.
226#[derive(Clone, Debug)]
227pub enum ConsistencyEvidence {
228    /// Only each exact stopped target is qualified; no atomic application claim.
229    PerCanister,
230    /// Application integration qualifies this exact currently active whole-selection fence.
231    ApplicationCoordinated(Box<ApplicationFenceEvidence>),
232}
233/// Passive provider data; accepted flags, expiry/defaults and JSON authority admission are absent.
234#[derive(Clone, Debug)]
235pub struct ConsistencyObservationInput {
236    /// Exact current request digest.
237    pub request: ArtifactChecksumRecord,
238    /// Actually observed canonical network/caller/release.
239    pub context: PlanContextRecord,
240    /// Complete actually observed current inventory, including unselected parent metadata.
241    pub inventory: InventoryRecord,
242    /// Exact actual selected target states/evidence; admitted canonically by the model.
243    pub targets: Vec<TargetCaptureEvidence>,
244    /// Actually observed optional membership revision; coordinated evidence requires a match.
245    pub membership_revision: Option<ArtifactChecksumRecord>,
246    /// Actual current evidence lane; policy requires the original declared guarantee.
247    pub consistency: ConsistencyEvidence,
248    /// Required opaque integration-qualified observation evidence.
249    pub evidence: ArtifactChecksumRecord,
250    /// Actual remote observations, each requiring separate prior approved accounting.
251    pub remote_observations: u32,
252}
253/// Immutable canonical current evidence; not persisted proof or a fence-release capability.
254#[derive(Clone, Debug)]
255pub struct ConsistencyObservation {
256    input: ConsistencyObservationInput,
257}
258impl ConsistencyObservation {
259    /// Normalize/sort exact targets, require a nonempty bounded unique inventory-backed set.
260    /// # Errors
261    /// Rejects target count, malformed identities, canonical duplicates or unknown targets.
262    pub fn new(
263        mut input: ConsistencyObservationInput,
264    ) -> Result<Self, ConsistencyObservationError> {
265        input.targets = super::observed_targets::canonicalize(
266            input.targets,
267            &input.inventory,
268            || ConsistencyObservationError::InvalidTargetCount,
269            || ConsistencyObservationError::InvalidPrincipal,
270            || ConsistencyObservationError::TargetAbsentFromInventory,
271            || ConsistencyObservationError::DuplicateTarget,
272        )?;
273        Ok(Self { input })
274    }
275    /// Read current request identity.
276    #[must_use]
277    pub const fn request(&self) -> &ArtifactChecksumRecord {
278        &self.input.request
279    }
280    /// Read actually observed canonical context.
281    #[must_use]
282    pub const fn context(&self) -> &PlanContextRecord {
283        &self.input.context
284    }
285    /// Read complete actual current inventory.
286    #[must_use]
287    pub const fn inventory(&self) -> &InventoryRecord {
288        &self.input.inventory
289    }
290    /// Read exact canonical actual target state/evidence rows.
291    #[must_use]
292    pub fn targets(&self) -> &[TargetCaptureEvidence] {
293        &self.input.targets
294    }
295    /// Read actual membership revision; equality alone grants no continuity.
296    #[must_use]
297    pub const fn membership_revision(&self) -> Option<&ArtifactChecksumRecord> {
298        self.input.membership_revision.as_ref()
299    }
300    /// Read current evidence lane.
301    #[must_use]
302    pub const fn consistency(&self) -> &ConsistencyEvidence {
303        &self.input.consistency
304    }
305    /// Read opaque qualified observation evidence.
306    #[must_use]
307    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
308        &self.input.evidence
309    }
310    /// Read actual call reporting without spending/replenishment.
311    #[must_use]
312    pub const fn remote_observations(&self) -> u32 {
313        self.input.remote_observations
314    }
315}
316/// Typed current canonical evidence admission failure.
317#[derive(Debug, Eq, Error, PartialEq)]
318pub enum ConsistencyObservationError {
319    /// Exact actual targets must fit the inventory bound and be nonempty.
320    #[error("consistency targets must contain 1..={MAX_INVENTORY_TARGETS} entries")]
321    InvalidTargetCount,
322    /// Target principal is invalid or exceeds its own bound.
323    #[error("invalid consistency target principal")]
324    InvalidPrincipal,
325    /// Equivalent target identities cannot appear twice.
326    #[error("duplicate consistency target")]
327    DuplicateTarget,
328    /// Actual target is not in the actually observed inventory.
329    #[error("consistency target absent from actual inventory")]
330    TargetAbsentFromInventory,
331}
332/// Typed original requirement/request denial; no failure acquires/releases a fence.
333#[derive(Debug, Error)]
334pub enum ConsistencyRequestError {
335    /// Original requirement does not match the original plan.
336    #[error(transparent)]
337    Requirement(#[from] ConsistencyRequirementError),
338    /// Original plan rejects exact operation derivation.
339    #[error(transparent)]
340    Plan(#[from] OperationPlanError),
341    /// Coordinated capture must name the retained exact fence obligation.
342    #[error("coordinated consistency requires retained fence identity")]
343    FenceRequired,
344    /// Per-canister guarantee cannot silently acquire/upgrade an application fence.
345    #[error("per-canister consistency does not admit expected fence identity")]
346    UnexpectedFence,
347    /// Descriptive call ceiling exceeds its maintained bound.
348    #[error("consistency observation ceiling exceeds {MAX_CONSISTENCY_REMOTE_OBSERVATIONS}")]
349    ObservationLimitTooLarge,
350}
351
352#[cfg(test)]
353mod tests;