ic_backup/model/ic_request/
mod.rs1mod method;
4pub use method::{IcManagementMethodRecord, IcRequestEffect};
5
6use crate::model::{
7 artifacts::ArtifactChecksumRecord, attempt_journal::OperationBindingRecord, bounded_sequence,
8};
9use ic_management_canister_types::{
10 CanisterIdRecord, LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs,
11};
12use serde::{Deserialize, Deserializer, Serialize};
13use thiserror::Error;
14
15pub const MAX_IC_SNAPSHOT_ID_BYTES: usize = 256;
17pub const MAX_IC_ARGUMENT_BYTES: usize = 4096;
19pub const MAX_IC_REQUEST_RECORD_BYTES: u64 = 8192;
21
22#[derive(Clone, Debug)]
24pub struct IcManagementRequest {
25 pub method: IcManagementMethodRecord,
27 pub target: String,
29 pub snapshot_id: Option<Vec<u8>>,
31}
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
39#[serde(try_from = "RequestFields")]
40pub struct IcManagementRequestRecord {
41 version: u16,
42 method: IcManagementMethodRecord,
43 target: String,
44 snapshot_id: Option<Vec<u8>>,
45 #[serde(skip)]
46 target_bytes: Vec<u8>,
47 #[serde(skip)]
48 arguments: Vec<u8>,
49}
50#[derive(Deserialize)]
51#[serde(deny_unknown_fields)]
52struct RequestFields {
53 version: u16,
54 method: IcManagementMethodRecord,
55 target: String,
56 #[serde(deserialize_with = "required_snapshot")]
57 snapshot_id: Option<Vec<u8>>,
58}
59impl TryFrom<RequestFields> for IcManagementRequestRecord {
60 type Error = IcRequestError;
61 fn try_from(fields: RequestFields) -> Result<Self, Self::Error> {
62 if fields.version != 1 {
63 return Err(IcRequestError::UnsupportedVersion(fields.version));
64 }
65 Self::new(IcManagementRequest {
66 method: fields.method,
67 target: fields.target,
68 snapshot_id: fields.snapshot_id,
69 })
70 }
71}
72impl IcManagementRequestRecord {
73 pub fn new(request: IcManagementRequest) -> Result<Self, IcRequestError> {
82 let (target, principal) = crate::model::principal::canonical_parts(&request.target)
83 .ok_or(IcRequestError::InvalidTarget)?;
84 match (request.method, request.snapshot_id.as_deref()) {
85 (IcManagementMethodRecord::LoadCanisterSnapshot, None) => {
86 return Err(IcRequestError::SnapshotRequired);
87 }
88 (IcManagementMethodRecord::LoadCanisterSnapshot, Some(bytes)) => {
89 if bytes.is_empty() || bytes.len() > MAX_IC_SNAPSHOT_ID_BYTES {
90 return Err(IcRequestError::InvalidSnapshotId);
91 }
92 }
93 (_, Some(_)) => return Err(IcRequestError::UnexpectedSnapshot),
94 (_, None) => {}
95 }
96 let arguments = match request.method {
97 IcManagementMethodRecord::TakeCanisterSnapshot => {
98 candid::encode_one(TakeCanisterSnapshotArgs {
99 canister_id: principal,
100 replace_snapshot: None,
101 uninstall_code: Some(false),
102 sender_canister_version: None,
103 })
104 }
105 IcManagementMethodRecord::LoadCanisterSnapshot => {
106 candid::encode_one(LoadCanisterSnapshotArgs {
107 canister_id: principal,
108 snapshot_id: request
109 .snapshot_id
110 .clone()
111 .ok_or(IcRequestError::SnapshotRequired)?,
112 sender_canister_version: None,
113 })
114 }
115 IcManagementMethodRecord::CanisterStatus
116 | IcManagementMethodRecord::ListCanisterSnapshots
117 | IcManagementMethodRecord::StartCanister
118 | IcManagementMethodRecord::StopCanister => candid::encode_one(CanisterIdRecord {
119 canister_id: principal,
120 }),
121 }
122 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
123 if arguments.len() > MAX_IC_ARGUMENT_BYTES {
124 return Err(IcRequestError::ArgumentsTooLarge);
125 }
126 Ok(Self {
127 version: 1,
128 method: request.method,
129 target,
130 snapshot_id: request.snapshot_id,
131 target_bytes: principal.as_slice().to_vec(),
132 arguments,
133 })
134 }
135 #[must_use]
137 pub const fn method(&self) -> IcManagementMethodRecord {
138 self.method
139 }
140 #[must_use]
142 pub fn target(&self) -> &str {
143 &self.target
144 }
145 #[must_use]
147 pub fn snapshot_id(&self) -> Option<&[u8]> {
148 self.snapshot_id.as_deref()
149 }
150 #[must_use]
152 pub fn arguments(&self) -> &[u8] {
153 &self.arguments
154 }
155 #[must_use]
157 pub const fn receiver(&self) -> &'static str {
158 "aaaaa-aa"
159 }
160 #[must_use]
165 pub fn digest(&self) -> ArtifactChecksumRecord {
166 management_request_digest(&self.target_bytes, self.method.name(), &self.arguments)
167 }
168 pub fn validate_mutation_binding(
174 &self,
175 binding: &OperationBindingRecord,
176 ) -> Result<(), IcRequestError> {
177 self.require_effect(IcRequestEffect::Mutation)?;
178 self.validate_identity(binding, binding.request())
179 }
180 pub fn validate_observation_binding(
187 &self,
188 binding: &OperationBindingRecord,
189 request: &ArtifactChecksumRecord,
190 ) -> Result<(), IcRequestError> {
191 self.require_effect(IcRequestEffect::Observation)?;
192 self.validate_identity(binding, request.hash())
193 }
194 fn require_effect(&self, expected: IcRequestEffect) -> Result<(), IcRequestError> {
195 if self.method.effect() != expected {
196 return Err(IcRequestError::EffectMismatch { expected });
197 }
198 Ok(())
199 }
200 fn validate_identity(
201 &self,
202 binding: &OperationBindingRecord,
203 expected: &str,
204 ) -> Result<(), IcRequestError> {
205 if self.target != binding.target() {
206 return Err(IcRequestError::TargetMismatch);
207 }
208 if self.digest().hash() != expected {
209 return Err(IcRequestError::DigestMismatch);
210 }
211 Ok(())
212 }
213}
214
215pub(super) fn management_request_digest(
217 target_bytes: &[u8],
218 method: &str,
219 arguments: &[u8],
220) -> ArtifactChecksumRecord {
221 let mut bytes = b"ic-backup/ic-management-request/v1\0".to_vec();
222 bytes.push(0); bytes.push(target_bytes.len().to_le_bytes()[0]); bytes.extend_from_slice(target_bytes);
225 bytes.push(1); bytes.push(method.len().to_le_bytes()[0]); bytes.extend_from_slice(method.as_bytes());
228 append_argument_length(&mut bytes, arguments.len());
229 bytes.extend_from_slice(arguments);
230 ArtifactChecksumRecord::from_bytes(&bytes)
231}
232
233#[expect(
234 clippy::cast_possible_truncation,
235 reason = "admitted argument byte length is at most 4096"
236)]
237fn append_argument_length(bytes: &mut Vec<u8>, length: usize) {
238 bytes.extend_from_slice(&(length as u32).to_be_bytes());
239}
240
241fn required_snapshot<'de, D: Deserializer<'de>>(
242 deserializer: D,
243) -> Result<Option<Vec<u8>>, D::Error> {
244 Ok(Option::<SnapshotBytes>::deserialize(deserializer)?.map(|bytes| bytes.0))
245}
246struct SnapshotBytes(Vec<u8>);
247impl<'de> Deserialize<'de> for SnapshotBytes {
248 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
249 bounded_sequence::deserialize::<_, u8, MAX_IC_SNAPSHOT_ID_BYTES>(
250 deserializer,
251 "at most 256 exact snapshot bytes",
252 IcRequestError::InvalidSnapshotId,
253 )
254 .map(Self)
255 }
256}
257
258#[derive(Debug, Error, Eq, PartialEq)]
260pub enum IcRequestError {
261 #[error("unsupported IC request version {0}")]
263 UnsupportedVersion(u16),
264 #[error("invalid IC request target principal")]
266 InvalidTarget,
267 #[error("load snapshot request requires snapshot_id")]
269 SnapshotRequired,
270 #[error("snapshot_id is not admitted for this IC request method")]
272 UnexpectedSnapshot,
273 #[error("snapshot_id must contain 1..={MAX_IC_SNAPSHOT_ID_BYTES} raw bytes")]
275 InvalidSnapshotId,
276 #[error("IC request Candid encoding failed: {0}")]
278 Encoding(String),
279 #[error("IC request arguments exceed {MAX_IC_ARGUMENT_BYTES} bytes")]
281 ArgumentsTooLarge,
282 #[error("IC request must have effect class {expected:?}")]
284 EffectMismatch {
285 expected: IcRequestEffect,
287 },
288 #[error("IC request target differs from original binding")]
290 TargetMismatch,
291 #[error("IC request digest differs from original binding")]
293 DigestMismatch,
294}
295
296#[cfg(test)]
297mod tests;