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