Skip to main content

ic_backup/model/ic_observation/
mod.rs

1//! Exact already reserved IC recovery observations and bounded passive replies.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord, MAX_OPERATION_ATTEMPTS},
6    ic_lifecycle_reply::MAX_IC_LIFECYCLE_REPLY_BYTES,
7    ic_request::{IcManagementRequestRecord, IcRequestError},
8    ic_snapshot_reply::MAX_IC_SNAPSHOT_REPLY_BYTES,
9    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
10};
11use std::fmt;
12use thiserror::Error;
13
14/// Raw observation reply bound derived from both existing method-specific codecs.
15pub const MAX_IC_OBSERVATION_REPLY_BYTES: usize =
16    if MAX_IC_SNAPSHOT_REPLY_BYTES < MAX_IC_LIFECYCLE_REPLY_BYTES {
17        MAX_IC_SNAPSHOT_REPLY_BYTES
18    } else {
19        MAX_IC_LIFECYCLE_REPLY_BYTES
20    };
21
22/// Structural binding to an original pending mutation and its reserved IC observation.
23///
24/// Retain original observation bytes before reserving their existing payload digest.
25/// This request neither spends nor proves dispatch custody, fresh read permission,
26/// authentication or effect attribution. Recovery never permits repeating a lost call.
27#[derive(Debug)]
28pub struct IcObservationRequest<'a> {
29    plan: &'a OperationPlanRecord,
30    mutation: &'a IcManagementRequestRecord,
31    payload: &'a IcManagementRequestRecord,
32    authority: AttemptAuthorityRecord,
33    mutation_attempt: u32,
34    observation_attempt: u32,
35}
36
37impl<'a> IcObservationRequest<'a> {
38    /// Bind original mutation bytes and exact already reserved status/list bytes.
39    ///
40    /// Both payloads must name the same original target. Observation identity reuses
41    /// the existing IC payload digest; no second hash or journal owner is introduced.
42    /// # Errors
43    /// Rejects changed authority/payloads or missing original pending reservations.
44    pub fn new(
45        plan: &'a OperationPlanRecord,
46        operation_sequence: u64,
47        journal: &AttemptJournalRecord,
48        mutation: &'a IcManagementRequestRecord,
49        payload: &'a IcManagementRequestRecord,
50    ) -> Result<Self, IcObservationRequestError> {
51        let authority = plan.attempt_authority(operation_sequence)?;
52        if journal.authority() != &authority {
53            return Err(IcObservationRequestError::AuthorityMismatch);
54        }
55        mutation.validate_mutation_binding(authority.binding())?;
56        payload.validate_observation_binding(authority.binding(), &payload.digest())?;
57        let (mutation_attempt, observation_attempt) = pending_attempts(journal)?;
58        let request = Self {
59            plan,
60            mutation,
61            payload,
62            authority,
63            mutation_attempt,
64            observation_attempt,
65        };
66        request.validate_journal(journal)?;
67        Ok(request)
68    }
69    /// Read the full original context, inventory and selection.
70    #[must_use]
71    pub const fn plan(&self) -> &OperationPlanRecord {
72        self.plan
73    }
74    /// Read the exact original mutation, never an executable retry.
75    #[must_use]
76    pub const fn mutation(&self) -> &'a IcManagementRequestRecord {
77        self.mutation
78    }
79    /// Read exact observation receiver, routing target, method and Candid bytes.
80    #[must_use]
81    pub const fn payload(&self) -> &'a IcManagementRequestRecord {
82        self.payload
83    }
84    /// Read the original operation authority and immutable attempt limits.
85    #[must_use]
86    pub const fn authority(&self) -> &AttemptAuthorityRecord {
87        &self.authority
88    }
89    /// Read the original unresolved mutation attempt.
90    #[must_use]
91    pub const fn mutation_attempt(&self) -> u32 {
92        self.mutation_attempt
93    }
94    /// Read the original already consumed observation attempt.
95    #[must_use]
96    pub const fn observation_attempt(&self) -> u32 {
97        self.observation_attempt
98    }
99    /// Recheck current exact reservations and original observation bytes without IO.
100    /// # Errors
101    /// Rejects changed authority, settled/replaced attempts or different reserved bytes.
102    pub fn validate_journal(
103        &self,
104        journal: &AttemptJournalRecord,
105    ) -> Result<(), IcObservationRequestError> {
106        ObservationReservation {
107            authority: &self.authority,
108            mutation_attempt: self.mutation_attempt,
109            observation_attempt: self.observation_attempt,
110            request: self.payload.digest(),
111        }
112        .validate(journal)
113    }
114}
115
116pub(crate) struct ObservationReservation<'a> {
117    pub authority: &'a AttemptAuthorityRecord,
118    pub mutation_attempt: u32,
119    pub observation_attempt: u32,
120    pub request: ArtifactChecksumRecord,
121}
122
123/// Read only the already consumed original attempt IDs, in mutation-first order.
124pub(crate) fn pending_attempts(
125    journal: &AttemptJournalRecord,
126) -> Result<(u32, u32), IcObservationRequestError> {
127    let current = journal.view();
128    Ok((
129        current
130            .pending_mutation
131            .ok_or(IcObservationRequestError::NoPendingMutation)?,
132        current
133            .pending_observation
134            .ok_or(IcObservationRequestError::NoPendingObservation)?,
135    ))
136}
137
138impl ObservationReservation<'_> {
139    pub(crate) fn validate(
140        &self,
141        journal: &AttemptJournalRecord,
142    ) -> Result<(), IcObservationRequestError> {
143        if journal.authority() != self.authority {
144            return Err(IcObservationRequestError::AuthorityMismatch);
145        }
146        let current = journal.view();
147        if current.pending_mutation != Some(self.mutation_attempt) {
148            return Err(IcObservationRequestError::MutationMismatch);
149        }
150        if current.pending_observation != Some(self.observation_attempt) {
151            return Err(IcObservationRequestError::ObservationMismatch);
152        }
153        if journal.pending_observation_request() != Some(self.request.hash()) {
154            return Err(IcObservationRequestError::RequestMismatch);
155        }
156        Ok(())
157    }
158}
159
160/// Passive provider fields; actual authentication and observation timing remain external.
161#[derive(Clone)]
162pub struct IcObservationResponseInput {
163    /// Exact full original authority digest including immutable allowances.
164    pub authority: ArtifactChecksumRecord,
165    /// Exact original unresolved mutation attempt.
166    pub mutation_attempt: u32,
167    /// Exact already reserved observation attempt, not another call number.
168    pub observation_attempt: u32,
169    /// Exact existing observation payload digest retained in its reservation.
170    pub request: ArtifactChecksumRecord,
171    /// Actual network/caller/release association claimed by the provider.
172    pub context: PlanContextRecord,
173    /// Actual response target claimed by the provider; canonicalized on admission.
174    pub target: String,
175    /// Exact bounded raw Candid reply.
176    pub reply: Vec<u8>,
177    /// Opaque retained association/authentication/timing evidence; not self-authenticating.
178    pub evidence: ArtifactChecksumRecord,
179}
180
181/// Immutable bounded passive observation, with no outcome, Default or Serde admission.
182#[derive(Clone)]
183pub struct IcObservationResponse {
184    input: IcObservationResponseInput,
185}
186impl IcObservationResponse {
187    /// Admit canonical target, chronological finite attempt IDs and bounded raw bytes.
188    /// # Errors
189    /// Rejects zero/excessive/reversed attempts, oversized replies and invalid principals.
190    pub fn new(mut input: IcObservationResponseInput) -> Result<Self, IcObservationResponseError> {
191        validate_response_input(&mut input, MAX_IC_OBSERVATION_REPLY_BYTES)?;
192        Ok(Self { input })
193    }
194    /// Read immutable canonical claims and exact raw bytes; no mutation access.
195    #[must_use]
196    pub const fn input(&self) -> &IcObservationResponseInput {
197        &self.input
198    }
199}
200impl fmt::Debug for IcObservationResponse {
201    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202        fmt_response_input(&self.input, formatter, "IcObservationResponse")
203    }
204}
205
206pub(crate) fn validate_response_input(
207    input: &mut IcObservationResponseInput,
208    max_reply_bytes: usize,
209) -> Result<(), IcObservationResponseError> {
210    if input.mutation_attempt == 0
211        || input.observation_attempt <= input.mutation_attempt
212        || input.observation_attempt > MAX_OPERATION_ATTEMPTS
213    {
214        return Err(IcObservationResponseError::InvalidAttempts);
215    }
216    if input.reply.len() > max_reply_bytes {
217        return Err(IcObservationResponseError::ReplyTooLarge);
218    }
219    input.target = crate::model::principal::canonical_text(&input.target)
220        .ok_or(IcObservationResponseError::InvalidTarget)?;
221    Ok(())
222}
223
224pub(crate) fn fmt_response_input(
225    input: &IcObservationResponseInput,
226    formatter: &mut fmt::Formatter<'_>,
227    name: &str,
228) -> fmt::Result {
229    formatter
230        .debug_struct(name)
231        .field("authority", &input.authority)
232        .field("mutation_attempt", &input.mutation_attempt)
233        .field("observation_attempt", &input.observation_attempt)
234        .field("request", &input.request)
235        .field("target", &input.target)
236        .field("reply_bytes", &input.reply.len())
237        .finish_non_exhaustive()
238}
239
240/// Structural original observation denial; no spending or effect outcome changes.
241#[derive(Debug, Error)]
242pub enum IcObservationRequestError {
243    /// Journal differs from original plan/operation/allowances.
244    #[error("IC observation original authority mismatch")]
245    AuthorityMismatch,
246    /// Original mutation has no pending reservation.
247    #[error("IC observation requires a pending original mutation")]
248    NoPendingMutation,
249    /// Original recovery observation has no pending reservation.
250    #[error("IC observation requires a pending original observation")]
251    NoPendingObservation,
252    /// Current original mutation changed or settled.
253    #[error("IC observation original mutation mismatch")]
254    MutationMismatch,
255    /// Current original observation changed or settled.
256    #[error("IC observation original attempt mismatch")]
257    ObservationMismatch,
258    /// Reserved observation digest differs from exact canonical payload bytes.
259    #[error("IC observation reserved request mismatch")]
260    RequestMismatch,
261    /// Original mutation or observation target/digest/class differs.
262    #[error(transparent)]
263    Payload(#[from] IcRequestError),
264    /// Original plan cannot derive the requested operation authority.
265    #[error(transparent)]
266    Plan(#[from] OperationPlanError),
267}
268
269/// Bounded passive response rejection with no raw bytes in diagnostics.
270#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
271pub enum IcObservationResponseError {
272    /// IDs must be chronological within the existing 1,024 total-attempt bound.
273    #[error("invalid IC observation response attempts")]
274    InvalidAttempts,
275    /// Raw bytes exceed the existing finite codec ceilings.
276    #[error("IC observation response reply too large")]
277    ReplyTooLarge,
278    /// Actual target is not a principal.
279    #[error("invalid IC observation response target")]
280    InvalidTarget,
281}
282
283mod capture_settlement;
284pub use capture_settlement::{IcCaptureAttribution, IcCaptureSettlement};
285
286mod settlement;
287pub use settlement::{IcLifecycleAttribution, IcLifecycleSettlement};
288
289#[cfg(test)]
290mod tests;