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