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;