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(operation_sequence)?;
51        if journal.authority() != &authority {
52            return Err(IcMutationRequestError::AuthorityMismatch);
53        }
54        payload.validate_mutation_binding(authority.binding())?;
55        let reservation = MutationReservation::new(authority, journal)?;
56        Ok(Self {
57            plan,
58            payload,
59            reservation,
60        })
61    }
62
63    /// Read original full context, inventory, selection and dependency declarations.
64    #[must_use]
65    pub const fn plan(&self) -> &OperationPlanRecord {
66        self.plan
67    }
68    /// Read exact management receiver, routing target, method and Candid argument bytes.
69    #[must_use]
70    pub const fn payload(&self) -> &'a IcManagementRequestRecord {
71        self.payload
72    }
73    /// Read canonical original identity and immutable mutation/observation limits.
74    #[must_use]
75    pub const fn authority(&self) -> &AttemptAuthorityRecord {
76        &self.reservation.authority
77    }
78    /// Read the original already consumed mutation attempt number.
79    #[must_use]
80    pub const fn mutation_attempt(&self) -> u32 {
81        self.reservation.mutation_attempt
82    }
83    /// Recheck the exact current pending reservation without IO or effect admission.
84    /// # Errors
85    /// Rejects different authority, replaced/settled mutation or observation recovery.
86    pub fn validate_journal(
87        &self,
88        journal: &AttemptJournalRecord,
89    ) -> Result<(), IcMutationRequestError> {
90        self.reservation.validate(journal).map_err(Into::into)
91    }
92}
93
94/// Immutable bounded passive acknowledgement; not a receipt or authenticated outcome.
95///
96/// No Serde or default admission exists. Raw bytes are hidden from Debug; explicit
97/// access retains exact evidence. Decoding happens in the existing method-specific
98/// reply owners during pure association, never in a second codec.
99#[derive(Clone)]
100pub struct IcMutationAcknowledgement {
101    input: IcUpdateResponseInput,
102}
103
104impl IcMutationAcknowledgement {
105    /// Admit finite attempt/raw-byte bounds and normalize the actual target identity.
106    /// # Errors
107    /// Rejects zero/excessive attempts, oversized raw replies and invalid principals.
108    pub fn new(mut input: IcUpdateResponseInput) -> Result<Self, IcMutationAcknowledgementError> {
109        input.admit(MAX_IC_MUTATION_REPLY_BYTES)?;
110        Ok(Self { input })
111    }
112    /// Read canonical passive association fields and exact raw reply, without mutation access.
113    #[must_use]
114    pub const fn input(&self) -> &IcUpdateResponseInput {
115        &self.input
116    }
117}
118
119impl fmt::Debug for IcMutationAcknowledgement {
120    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121        self.input
122            .fmt_redacted(formatter, "IcMutationAcknowledgement")
123    }
124}
125
126/// Original reservation denial; no variant consumes, refunds or grants call authority.
127#[derive(Debug, Error)]
128pub enum IcMutationRequestError {
129    /// Journal differs from full original plan/context/operation/request/allowance identity.
130    #[error("IC mutation original authority mismatch")]
131    AuthorityMismatch,
132    /// There is no unresolved original mutation reservation.
133    #[error("IC mutation requires a pending original mutation")]
134    NoPendingMutation,
135    /// Current pending mutation changed or was settled after binding.
136    #[error("IC mutation original attempt mismatch")]
137    MutationMismatch,
138    /// Recovery observation is pending; association cannot bypass its original owner.
139    #[error("IC mutation has a pending recovery observation")]
140    ObservationPending,
141    /// Exact closed method, target or original Candid payload digest differs.
142    #[error(transparent)]
143    Payload(#[from] IcRequestError),
144    /// Original plan cannot derive the requested operation authority.
145    #[error(transparent)]
146    Plan(#[from] OperationPlanError),
147}
148
149/// Passive reply boundary denial with no raw payload in diagnostics.
150#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
151pub enum IcMutationAcknowledgementError {
152    /// Original attempt must be within the existing 1..=1,024 total-attempt bound.
153    #[error("invalid IC mutation acknowledgement attempt")]
154    InvalidAttempt,
155    /// Raw reply exceeded the existing finite codec byte bounds.
156    #[error("IC mutation acknowledgement reply too large")]
157    ReplyTooLarge,
158    /// Actual provider target is not a principal.
159    #[error("invalid IC mutation acknowledgement target")]
160    InvalidTarget,
161}
162
163#[cfg(test)]
164mod tests;
165
166impl From<crate::model::attempt_journal::MutationReservationError> for IcMutationRequestError {
167    fn from(error: crate::model::attempt_journal::MutationReservationError) -> Self {
168        match error {
169            Reservation::NoPendingMutation => Self::NoPendingMutation,
170            Reservation::AuthorityMismatch => Self::AuthorityMismatch,
171            Reservation::MutationMismatch => Self::MutationMismatch,
172            Reservation::ObservationPending => Self::ObservationPending,
173        }
174    }
175}
176
177impl From<crate::model::ic_update_response::UpdateResponseAdmissionError>
178    for IcMutationAcknowledgementError
179{
180    fn from(error: crate::model::ic_update_response::UpdateResponseAdmissionError) -> Self {
181        match error {
182            crate::model::ic_update_response::UpdateResponseAdmissionError::InvalidAttempt => {
183                Self::InvalidAttempt
184            }
185            crate::model::ic_update_response::UpdateResponseAdmissionError::ReplyTooLarge => {
186                Self::ReplyTooLarge
187            }
188            crate::model::ic_update_response::UpdateResponseAdmissionError::InvalidTarget => {
189                Self::InvalidTarget
190            }
191        }
192    }
193}