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;