Skip to main content

ic_backup/model/ic_mutation/
mod.rs

1//! Exact reserved IC mutation identity and bounded passive reply association.
2
3use crate::model::attempt_journal::MutationReservation;
4use crate::model::attempt_journal::MutationReservationError as Reservation;
5use crate::model::ic_update_response::IcUpdateResponseInput;
6use crate::model::{
7    attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord},
8    ic_lifecycle_reply::MAX_IC_LIFECYCLE_REPLY_BYTES,
9    ic_request::{IcManagementRequestRecord, IcRequestError},
10    ic_snapshot_reply::MAX_IC_SNAPSHOT_REPLY_BYTES,
11    operation_plan::{OperationPlanError, OperationPlanRecord},
12};
13use std::fmt;
14use thiserror::Error;
15
16/// Maximum owned raw reply bytes, derived from both existing finite codec owners.
17pub const MAX_IC_MUTATION_REPLY_BYTES: usize =
18    if MAX_IC_SNAPSHOT_REPLY_BYTES < MAX_IC_LIFECYCLE_REPLY_BYTES {
19        MAX_IC_SNAPSHOT_REPLY_BYTES
20    } else {
21        MAX_IC_LIFECYCLE_REPLY_BYTES
22    };
23
24/// Structural request for one already reserved original host-ingress mutation.
25///
26/// Retained identity does not prove fresh permissions, prerequisites, application
27/// safety or exclusive custody. A pending reservation does not prove it was never
28/// dispatched. This request is not a dispatch permit or an interruption retry.
29#[derive(Debug)]
30pub struct IcMutationRequest<'a> {
31    plan: &'a OperationPlanRecord,
32    payload: &'a IcManagementRequestRecord,
33    reservation: MutationReservation,
34}
35
36impl<'a> IcMutationRequest<'a> {
37    /// Bind exact original plan, allowances and IC bytes after mutation reservation.
38    ///
39    /// Accepts only capture, load, start and stop; observations have separate
40    /// reservations. Derivation never creates a journal, spends or resets allowance.
41    /// # Errors
42    /// Rejects unknown operations, changed authority/bytes, absent pending mutation
43    /// or an observation already reserved for recovery.
44    pub fn new(
45        plan: &'a OperationPlanRecord,
46        operation_sequence: u64,
47        journal: &AttemptJournalRecord,
48        payload: &'a IcManagementRequestRecord,
49    ) -> Result<Self, IcMutationRequestError> {
50        let authority = plan.attempt_authority_for_journal(
51            operation_sequence,
52            journal,
53            IcMutationRequestError::AuthorityMismatch,
54        )?;
55        payload.validate_mutation_binding(authority.binding())?;
56        let reservation = MutationReservation::new(authority, journal)?;
57        Ok(Self {
58            plan,
59            payload,
60            reservation,
61        })
62    }
63
64    /// Read original full context, inventory, selection and dependency declarations.
65    #[must_use]
66    pub const fn plan(&self) -> &OperationPlanRecord {
67        self.plan
68    }
69    /// Read exact management receiver, routing target, method and Candid argument bytes.
70    #[must_use]
71    pub const fn payload(&self) -> &'a IcManagementRequestRecord {
72        self.payload
73    }
74    /// Read canonical original identity and immutable mutation/observation limits.
75    #[must_use]
76    pub const fn authority(&self) -> &AttemptAuthorityRecord {
77        &self.reservation.authority
78    }
79    /// Read the original already consumed mutation attempt number.
80    #[must_use]
81    pub const fn mutation_attempt(&self) -> u32 {
82        self.reservation.mutation_attempt
83    }
84    /// Recheck the exact current pending reservation without IO or effect admission.
85    /// # Errors
86    /// Rejects different authority, replaced/settled mutation or observation recovery.
87    pub fn validate_journal(
88        &self,
89        journal: &AttemptJournalRecord,
90    ) -> Result<(), IcMutationRequestError> {
91        self.reservation.validate(journal).map_err(Into::into)
92    }
93}
94
95/// Immutable bounded passive acknowledgement; not a receipt or authenticated outcome.
96///
97/// No Serde or default admission exists. Raw bytes are hidden from Debug; explicit
98/// access retains exact evidence. Decoding happens in the existing method-specific
99/// reply owners during pure association, never in a second codec.
100#[derive(Clone)]
101pub struct IcMutationAcknowledgement {
102    input: IcUpdateResponseInput,
103}
104
105impl IcMutationAcknowledgement {
106    /// Admit finite attempt/raw-byte bounds and normalize the actual target identity.
107    /// # Errors
108    /// Rejects zero/excessive attempts, oversized raw replies and invalid principals.
109    pub fn new(mut input: IcUpdateResponseInput) -> Result<Self, IcMutationAcknowledgementError> {
110        input.admit(MAX_IC_MUTATION_REPLY_BYTES)?;
111        Ok(Self { input })
112    }
113    /// Read canonical passive association fields and exact raw reply, without mutation access.
114    #[must_use]
115    pub const fn input(&self) -> &IcUpdateResponseInput {
116        &self.input
117    }
118}
119
120impl fmt::Debug for IcMutationAcknowledgement {
121    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122        self.input
123            .fmt_redacted(formatter, "IcMutationAcknowledgement")
124    }
125}
126
127/// Original reservation denial; no variant consumes, refunds or grants call authority.
128#[derive(Debug, Error)]
129pub enum IcMutationRequestError {
130    /// Journal differs from full original plan/context/operation/request/allowance identity.
131    #[error("IC mutation original authority mismatch")]
132    AuthorityMismatch,
133    /// There is no unresolved original mutation reservation.
134    #[error("IC mutation requires a pending original mutation")]
135    NoPendingMutation,
136    /// Current pending mutation changed or was settled after binding.
137    #[error("IC mutation original attempt mismatch")]
138    MutationMismatch,
139    /// Recovery observation is pending; association cannot bypass its original owner.
140    #[error("IC mutation has a pending recovery observation")]
141    ObservationPending,
142    /// Exact closed method, target or original Candid payload digest differs.
143    #[error(transparent)]
144    Payload(#[from] IcRequestError),
145    /// Original plan cannot derive the requested operation authority.
146    #[error(transparent)]
147    Plan(#[from] OperationPlanError),
148}
149
150/// Passive reply boundary denial with no raw payload in diagnostics.
151#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
152pub enum IcMutationAcknowledgementError {
153    /// Original attempt must be within the existing 1..=1,024 total-attempt bound.
154    #[error("invalid IC mutation acknowledgement attempt")]
155    InvalidAttempt,
156    /// Raw reply exceeded the existing finite codec byte bounds.
157    #[error("IC mutation acknowledgement reply too large")]
158    ReplyTooLarge,
159    /// Actual provider target is not a principal.
160    #[error("invalid IC mutation acknowledgement target")]
161    InvalidTarget,
162}
163
164#[cfg(test)]
165mod tests;
166
167impl From<crate::model::attempt_journal::MutationReservationError> for IcMutationRequestError {
168    fn from(error: crate::model::attempt_journal::MutationReservationError) -> Self {
169        match error {
170            Reservation::NoPendingMutation => Self::NoPendingMutation,
171            Reservation::AuthorityMismatch => Self::AuthorityMismatch,
172            Reservation::MutationMismatch => Self::MutationMismatch,
173            Reservation::ObservationPending => Self::ObservationPending,
174        }
175    }
176}
177
178impl From<crate::model::ic_update_response::UpdateResponseAdmissionError>
179    for IcMutationAcknowledgementError
180{
181    fn from(error: crate::model::ic_update_response::UpdateResponseAdmissionError) -> Self {
182        match error {
183            crate::model::ic_update_response::UpdateResponseAdmissionError::InvalidAttempt => {
184                Self::InvalidAttempt
185            }
186            crate::model::ic_update_response::UpdateResponseAdmissionError::ReplyTooLarge => {
187                Self::ReplyTooLarge
188            }
189            crate::model::ic_update_response::UpdateResponseAdmissionError::InvalidTarget => {
190                Self::InvalidTarget
191            }
192        }
193    }
194}