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