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 current = journal.view();
58        let request = Self {
59            plan,
60            mutation,
61            payload,
62            authority,
63            mutation_attempt: current
64                .pending_mutation
65                .ok_or(IcObservationRequestError::NoPendingMutation)?,
66            observation_attempt: current
67                .pending_observation
68                .ok_or(IcObservationRequestError::NoPendingObservation)?,
69        };
70        request.validate_journal(journal)?;
71        Ok(request)
72    }
73    /// Read the full original context, inventory and selection.
74    #[must_use]
75    pub const fn plan(&self) -> &OperationPlanRecord {
76        self.plan
77    }
78    /// Read the exact original mutation, never an executable retry.
79    #[must_use]
80    pub const fn mutation(&self) -> &'a IcManagementRequestRecord {
81        self.mutation
82    }
83    /// Read exact observation receiver, routing target, method and Candid bytes.
84    #[must_use]
85    pub const fn payload(&self) -> &'a IcManagementRequestRecord {
86        self.payload
87    }
88    /// Read the original operation authority and immutable attempt limits.
89    #[must_use]
90    pub const fn authority(&self) -> &AttemptAuthorityRecord {
91        &self.authority
92    }
93    /// Read the original unresolved mutation attempt.
94    #[must_use]
95    pub const fn mutation_attempt(&self) -> u32 {
96        self.mutation_attempt
97    }
98    /// Read the original already consumed observation attempt.
99    #[must_use]
100    pub const fn observation_attempt(&self) -> u32 {
101        self.observation_attempt
102    }
103    /// Recheck current exact reservations and original observation bytes without IO.
104    /// # Errors
105    /// Rejects changed authority, settled/replaced attempts or different reserved bytes.
106    pub fn validate_journal(
107        &self,
108        journal: &AttemptJournalRecord,
109    ) -> Result<(), IcObservationRequestError> {
110        ObservationReservation {
111            authority: &self.authority,
112            mutation_attempt: self.mutation_attempt,
113            observation_attempt: self.observation_attempt,
114            payload: self.payload,
115        }
116        .validate(journal)
117    }
118}
119
120pub(crate) struct ObservationReservation<'a> {
121    pub authority: &'a AttemptAuthorityRecord,
122    pub mutation_attempt: u32,
123    pub observation_attempt: u32,
124    pub payload: &'a IcManagementRequestRecord,
125}
126
127impl ObservationReservation<'_> {
128    pub(crate) fn validate(
129        &self,
130        journal: &AttemptJournalRecord,
131    ) -> Result<(), IcObservationRequestError> {
132        if journal.authority() != self.authority {
133            return Err(IcObservationRequestError::AuthorityMismatch);
134        }
135        let current = journal.view();
136        if current.pending_mutation != Some(self.mutation_attempt) {
137            return Err(IcObservationRequestError::MutationMismatch);
138        }
139        if current.pending_observation != Some(self.observation_attempt) {
140            return Err(IcObservationRequestError::ObservationMismatch);
141        }
142        if journal.pending_observation_request() != Some(self.payload.digest().hash()) {
143            return Err(IcObservationRequestError::RequestMismatch);
144        }
145        Ok(())
146    }
147}
148
149/// Passive provider fields; actual authentication and observation timing remain external.
150#[derive(Clone)]
151pub struct IcObservationResponseInput {
152    /// Exact full original authority digest including immutable allowances.
153    pub authority: ArtifactChecksumRecord,
154    /// Exact original unresolved mutation attempt.
155    pub mutation_attempt: u32,
156    /// Exact already reserved observation attempt, not another call number.
157    pub observation_attempt: u32,
158    /// Exact existing observation payload digest retained in its reservation.
159    pub request: ArtifactChecksumRecord,
160    /// Actual network/caller/release association claimed by the provider.
161    pub context: PlanContextRecord,
162    /// Actual response target claimed by the provider; canonicalized on admission.
163    pub target: String,
164    /// Exact bounded raw Candid reply.
165    pub reply: Vec<u8>,
166    /// Opaque retained association/authentication/timing evidence; not self-authenticating.
167    pub evidence: ArtifactChecksumRecord,
168}
169
170/// Immutable bounded passive observation, with no outcome, Default or Serde admission.
171#[derive(Clone)]
172pub struct IcObservationResponse {
173    input: IcObservationResponseInput,
174}
175impl IcObservationResponse {
176    /// Admit canonical target, chronological finite attempt IDs and bounded raw bytes.
177    /// # Errors
178    /// Rejects zero/excessive/reversed attempts, oversized replies and invalid principals.
179    pub fn new(mut input: IcObservationResponseInput) -> Result<Self, IcObservationResponseError> {
180        if input.mutation_attempt == 0
181            || input.observation_attempt <= input.mutation_attempt
182            || input.observation_attempt > MAX_OPERATION_ATTEMPTS
183        {
184            return Err(IcObservationResponseError::InvalidAttempts);
185        }
186        if input.reply.len() > MAX_IC_OBSERVATION_REPLY_BYTES {
187            return Err(IcObservationResponseError::ReplyTooLarge);
188        }
189        input.target = crate::model::principal::canonical_text(&input.target)
190            .ok_or(IcObservationResponseError::InvalidTarget)?;
191        Ok(Self { input })
192    }
193    /// Read immutable canonical claims and exact raw bytes; no mutation access.
194    #[must_use]
195    pub const fn input(&self) -> &IcObservationResponseInput {
196        &self.input
197    }
198}
199impl fmt::Debug for IcObservationResponse {
200    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201        formatter
202            .debug_struct("IcObservationResponse")
203            .field("authority", &self.input.authority)
204            .field("mutation_attempt", &self.input.mutation_attempt)
205            .field("observation_attempt", &self.input.observation_attempt)
206            .field("request", &self.input.request)
207            .field("target", &self.input.target)
208            .field("reply_bytes", &self.input.reply.len())
209            .finish_non_exhaustive()
210    }
211}
212
213/// Structural original observation denial; no spending or effect outcome changes.
214#[derive(Debug, Error)]
215pub enum IcObservationRequestError {
216    /// Journal differs from original plan/operation/allowances.
217    #[error("IC observation original authority mismatch")]
218    AuthorityMismatch,
219    /// Original mutation has no pending reservation.
220    #[error("IC observation requires a pending original mutation")]
221    NoPendingMutation,
222    /// Original recovery observation has no pending reservation.
223    #[error("IC observation requires a pending original observation")]
224    NoPendingObservation,
225    /// Current original mutation changed or settled.
226    #[error("IC observation original mutation mismatch")]
227    MutationMismatch,
228    /// Current original observation changed or settled.
229    #[error("IC observation original attempt mismatch")]
230    ObservationMismatch,
231    /// Reserved observation digest differs from exact canonical payload bytes.
232    #[error("IC observation reserved request mismatch")]
233    RequestMismatch,
234    /// Original mutation or observation target/digest/class differs.
235    #[error(transparent)]
236    Payload(#[from] IcRequestError),
237    /// Original plan cannot derive the requested operation authority.
238    #[error(transparent)]
239    Plan(#[from] OperationPlanError),
240}
241
242/// Bounded passive response rejection with no raw bytes in diagnostics.
243#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
244pub enum IcObservationResponseError {
245    /// IDs must be chronological within the existing 1,024 total-attempt bound.
246    #[error("invalid IC observation response attempts")]
247    InvalidAttempts,
248    /// Raw bytes exceed the existing finite codec ceilings.
249    #[error("IC observation response reply too large")]
250    ReplyTooLarge,
251    /// Actual target is not a principal.
252    #[error("invalid IC observation response target")]
253    InvalidTarget,
254}
255
256#[cfg(test)]
257mod tests;