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;