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