Skip to main content

ic_backup/model/ic_lifecycle_reply/
mod.rs

1//! Bounded lifecycle acknowledgements and status/controller projections.
2
3mod wire;
4
5use super::{
6    artifacts::ArtifactChecksumRecord,
7    control_authority::{ControlObservationError, ControllerSet},
8    ic_request::{IcManagementMethodRecord, IcManagementRequestRecord},
9};
10use ic_management_canister_types::CanisterStatusType;
11use thiserror::Error;
12
13/// Maximum raw reply bytes, checked before any status decoding.
14pub const MAX_IC_LIFECYCLE_REPLY_BYTES: usize = 1024 * 1024;
15
16/// Read-only required status and complete declared controller set.
17///
18/// Other upstream status fields are skipped under finite work limits; their
19/// presence, values and semantics are not qualified by this projection. A decoded
20/// Stopped value establishes no drained work, continuous fence or load success.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct IcCanisterStatusInfo {
23    status: CanisterStatusType,
24    controllers: ControllerSet,
25}
26
27impl IcCanisterStatusInfo {
28    /// Read the upstream running, stopping or stopped variant exactly.
29    #[must_use]
30    pub const fn status(&self) -> CanisterStatusType {
31        self.status
32    }
33
34    /// Read the canonical bounded set through its existing owning boundary.
35    ///
36    /// An explicitly empty set stays empty; missing settings/controllers reject.
37    #[must_use]
38    pub const fn controllers(&self) -> &ControllerSet {
39        &self.controllers
40    }
41}
42
43/// Decoded wire shape, with no receipt or current authority admission.
44#[derive(Debug, Eq, PartialEq)]
45pub enum IcLifecycleReplyKind {
46    /// Required status/controller projection of a canister-status result.
47    Status(IcCanisterStatusInfo),
48    /// Exact empty Candid argument tuple for stop, start or snapshot load.
49    ///
50    /// This is local wire admission, not authentication or ongoing lifecycle state.
51    Acknowledgement,
52}
53
54/// Local reply evidence associated with the caller's immutable exact request.
55///
56/// The wire carries no network, caller, target or challenge. Integrations must
57/// authenticate association, qualify observation timing and consume original
58/// per-call authority. No decoded kind settles pending attempts, replenishes limits,
59/// admits restart/load/fence release or proves complete same-release restoration.
60#[derive(Debug)]
61pub struct IcLifecycleReply<'request> {
62    request: &'request IcManagementRequestRecord,
63    kind: IcLifecycleReplyKind,
64    payload_checksum: ArtifactChecksumRecord,
65}
66
67impl<'request> IcLifecycleReply<'request> {
68    /// Admit the existing status, stop, start or load reply under finite local bounds.
69    ///
70    /// Status requires exactly one value with required status/settings/controllers.
71    /// Unprojected fields are skipped, not validated. Acknowledgements require the
72    /// canonical six-byte empty tuple, with no extra type table, argument or bytes.
73    ///
74    /// # Errors
75    /// Rejects unsupported methods, excessive raw input, invalid/bounded wire shapes
76    /// and duplicate controller identity, with no raw payload in diagnostics.
77    pub fn decode(
78        request: &'request IcManagementRequestRecord,
79        bytes: &[u8],
80    ) -> Result<Self, IcLifecycleReplyError> {
81        let method = request.method();
82        if !matches!(
83            method,
84            IcManagementMethodRecord::CanisterStatus
85                | IcManagementMethodRecord::StopCanister
86                | IcManagementMethodRecord::StartCanister
87                | IcManagementMethodRecord::LoadCanisterSnapshot
88        ) {
89            return Err(IcLifecycleReplyError::UnsupportedMethod { method });
90        }
91        if bytes.len() > MAX_IC_LIFECYCLE_REPLY_BYTES {
92            return Err(IcLifecycleReplyError::ReplyTooLarge);
93        }
94        let kind = if method == IcManagementMethodRecord::CanisterStatus {
95            IcLifecycleReplyKind::Status(wire::status(bytes)?)
96        } else {
97            if bytes != b"DIDL\0\0" {
98                return Err(IcLifecycleReplyError::InvalidReply);
99            }
100            IcLifecycleReplyKind::Acknowledgement
101        };
102        Ok(Self {
103            request,
104            kind,
105            payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
106        })
107    }
108
109    /// Read the original method, canonical target and exact wire identity owner.
110    #[must_use]
111    pub const fn request(&self) -> &'request IcManagementRequestRecord {
112        self.request
113    }
114
115    /// Read the method-specific local projection or acknowledgement.
116    #[must_use]
117    pub const fn kind(&self) -> &IcLifecycleReplyKind {
118        &self.kind
119    }
120
121    /// Read SHA-256 of exact raw bytes, including all unprojected fields/order.
122    #[must_use]
123    pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
124        &self.payload_checksum
125    }
126
127    /// Hash existing request digest and exact raw-reply checksum in a v1 domain.
128    ///
129    /// Two fixed 64-byte lowercase SHA-256 strings follow the NUL-terminated domain.
130    /// This binds declared association and never authenticates a transport receipt.
131    #[must_use]
132    pub fn digest(&self) -> ArtifactChecksumRecord {
133        let mut bytes = b"ic-backup/ic-lifecycle-reply/v1\0".to_vec();
134        bytes.extend_from_slice(self.request.digest().hash().as_bytes());
135        bytes.extend_from_slice(self.payload_checksum.hash().as_bytes());
136        ArtifactChecksumRecord::from_bytes(&bytes)
137    }
138}
139
140/// Typed local wire admission failure, preserving caller-owned evidence and journals.
141#[derive(Debug, Error, Eq, PartialEq)]
142pub enum IcLifecycleReplyError {
143    /// The existing capture/inventory shapes have a separate reply owner.
144    #[error("lifecycle reply codec does not support {method:?}")]
145    UnsupportedMethod {
146        /// Actual request method.
147        method: IcManagementMethodRecord,
148    },
149    /// Raw input exceeded the bound before parsing.
150    #[error("lifecycle reply exceeds {MAX_IC_LIFECYCLE_REPLY_BYTES} bytes")]
151    ReplyTooLarge,
152    /// Required shape/fields/counts, decoder work or exact consumption failed.
153    #[error("invalid or unbounded lifecycle Candid reply")]
154    InvalidReply,
155    /// Canonical controller admission failed in its existing owner.
156    #[error(transparent)]
157    Controllers(#[from] ControlObservationError),
158}
159
160#[cfg(test)]
161mod tests;