ic_backup/model/ic_request/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4use ic_platform::candid::contract;
5mod method;
6pub use method::{IcManagementMethodRecord, IcRequestEffect};
7
8use crate::model::{
9 artifacts::ArtifactChecksumRecord, attempt_journal::OperationBindingRecord, bounded_sequence,
10};
11use ic_platform::management_canister_types::{
12 CanisterIdRecord, LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs,
13};
14use serde::{Deserialize, Deserializer, Serialize};
15use thiserror::Error;
16
17pub const MAX_IC_SNAPSHOT_ID_BYTES: usize = 256;
19pub const MAX_IC_ARGUMENT_BYTES: usize = 4096;
21pub const MAX_IC_REQUEST_RECORD_BYTES: u64 = 8192;
23
24#[derive(Clone, Debug)]
26pub struct IcManagementRequest {
27 pub method: IcManagementMethodRecord,
29 pub target: String,
31 pub snapshot_id: Option<Vec<u8>>,
33}
34
35#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
41#[serde(try_from = "RequestFields")]
42pub struct IcManagementRequestRecord {
43 version: u16,
44 method: IcManagementMethodRecord,
45 target: String,
46 snapshot_id: Option<Vec<u8>>,
47 #[serde(skip)]
48 target_bytes: Vec<u8>,
49 #[serde(skip)]
50 arguments: Vec<u8>,
51}
52#[derive(Deserialize)]
53#[serde(deny_unknown_fields)]
54struct RequestFields {
55 version: u16,
56 method: IcManagementMethodRecord,
57 target: String,
58 #[serde(deserialize_with = "required_snapshot")]
59 snapshot_id: Option<Vec<u8>>,
60}
61impl TryFrom<RequestFields> for IcManagementRequestRecord {
62 type Error = IcRequestError;
63 fn try_from(fields: RequestFields) -> Result<Self, Self::Error> {
64 if fields.version != 1 {
65 return Err(IcRequestError::UnsupportedVersion(fields.version));
66 }
67 Self::new(IcManagementRequest {
68 method: fields.method,
69 target: fields.target,
70 snapshot_id: fields.snapshot_id,
71 })
72 }
73}
74impl IcManagementRequestRecord {
75 pub fn new(request: IcManagementRequest) -> Result<Self, IcRequestError> {
84 let (target, principal) = crate::model::principal::canonical_parts(&request.target)
85 .ok_or(IcRequestError::InvalidTarget)?;
86 let arguments = match (request.method, request.snapshot_id.as_deref()) {
87 (IcManagementMethodRecord::LoadCanisterSnapshot, None) => {
88 return Err(IcRequestError::SnapshotRequired);
89 }
90 (IcManagementMethodRecord::LoadCanisterSnapshot, Some(bytes)) => {
91 if !admits_snapshot_id(bytes) {
92 return Err(IcRequestError::InvalidSnapshotId);
93 }
94 encode_arguments(LoadCanisterSnapshotArgs {
95 canister_id: principal.into(),
96 snapshot_id: bytes.to_vec(),
97 sender_canister_version: None,
98 })
99 }
100 (_, Some(_)) => return Err(IcRequestError::UnexpectedSnapshot),
101 (IcManagementMethodRecord::TakeCanisterSnapshot, None) => {
102 encode_arguments(TakeCanisterSnapshotArgs {
103 canister_id: principal.into(),
104 replace_snapshot: None,
105 uninstall_code: Some(false),
106 sender_canister_version: None,
107 })
108 }
109 (
110 IcManagementMethodRecord::CanisterStatus
111 | IcManagementMethodRecord::ListCanisterSnapshots
112 | IcManagementMethodRecord::StartCanister
113 | IcManagementMethodRecord::StopCanister,
114 None,
115 ) => encode_arguments(CanisterIdRecord {
116 canister_id: principal.into(),
117 }),
118 }?;
119 Ok(Self {
120 version: 1,
121 method: request.method,
122 target,
123 snapshot_id: request.snapshot_id,
124 target_bytes: principal.as_slice().to_vec(),
125 arguments,
126 })
127 }
128 #[must_use]
130 pub const fn method(&self) -> IcManagementMethodRecord {
131 self.method
132 }
133 #[must_use]
135 pub fn target(&self) -> &str {
136 &self.target
137 }
138 #[must_use]
140 pub fn snapshot_id(&self) -> Option<&[u8]> {
141 self.snapshot_id.as_deref()
142 }
143 #[must_use]
145 pub fn arguments(&self) -> &[u8] {
146 &self.arguments
147 }
148 #[must_use]
150 pub const fn receiver(&self) -> &'static str {
151 "aaaaa-aa"
152 }
153 #[must_use]
158 pub fn digest(&self) -> ArtifactChecksumRecord {
159 management_request_digest(&self.target_bytes, self.method.name(), &self.arguments)
160 }
161 pub fn validate_mutation_binding(
167 &self,
168 binding: &OperationBindingRecord,
169 ) -> Result<(), IcRequestError> {
170 self.require_effect(IcRequestEffect::Mutation)?;
171 self.validate_identity(binding, binding.request())
172 }
173 pub fn validate_observation_binding(
180 &self,
181 binding: &OperationBindingRecord,
182 request: &ArtifactChecksumRecord,
183 ) -> Result<(), IcRequestError> {
184 self.require_effect(IcRequestEffect::Observation)?;
185 self.validate_identity(binding, request.hash())
186 }
187 fn require_effect(&self, expected: IcRequestEffect) -> Result<(), IcRequestError> {
188 if self.method.effect() != expected {
189 return Err(IcRequestError::EffectMismatch { expected });
190 }
191 Ok(())
192 }
193 fn validate_identity(
194 &self,
195 binding: &OperationBindingRecord,
196 expected: &str,
197 ) -> Result<(), IcRequestError> {
198 if self.target != binding.target() {
199 return Err(IcRequestError::TargetMismatch);
200 }
201 if self.digest().hash() != expected {
202 return Err(IcRequestError::DigestMismatch);
203 }
204 Ok(())
205 }
206}
207
208pub(super) fn encode_arguments(
209 value: impl contract::CandidType,
210) -> Result<Vec<u8>, IcRequestError> {
211 let arguments =
212 contract::encode_one(value).map_err(|error| IcRequestError::Encoding(error.to_string()))?;
213 if arguments.len() > MAX_IC_ARGUMENT_BYTES {
214 return Err(IcRequestError::ArgumentsTooLarge);
215 }
216 Ok(arguments)
217}
218
219pub(super) fn admits_snapshot_id(bytes: &[u8]) -> bool {
222 !bytes.is_empty() && bytes.len() <= MAX_IC_SNAPSHOT_ID_BYTES
223}
224
225pub(super) fn management_request_digest(
227 target_bytes: &[u8],
228 method: &str,
229 arguments: &[u8],
230) -> ArtifactChecksumRecord {
231 let mut bytes = EvidenceDigest::new(b"ic-backup/ic-management-request/v1\0");
232 bytes.byte(0); bytes.bytes_u8(target_bytes);
234 bytes.byte(1); bytes.bytes_u8(method.as_bytes());
236 bytes.bytes_u32(arguments);
237 bytes.finish()
238}
239
240fn required_snapshot<'de, D: Deserializer<'de>>(
241 deserializer: D,
242) -> Result<Option<Vec<u8>>, D::Error> {
243 Ok(Option::<SnapshotBytes>::deserialize(deserializer)?.map(|bytes| bytes.0))
244}
245struct SnapshotBytes(Vec<u8>);
246impl<'de> Deserialize<'de> for SnapshotBytes {
247 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
248 bounded_sequence::deserialize::<_, u8, MAX_IC_SNAPSHOT_ID_BYTES>(
249 deserializer,
250 "at most 256 exact snapshot bytes",
251 IcRequestError::InvalidSnapshotId,
252 )
253 .map(Self)
254 }
255}
256
257#[derive(Debug, Error, Eq, PartialEq)]
259pub enum IcRequestError {
260 #[error("unsupported IC request version {0}")]
262 UnsupportedVersion(u16),
263 #[error("invalid IC request target principal")]
265 InvalidTarget,
266 #[error("load snapshot request requires snapshot_id")]
268 SnapshotRequired,
269 #[error("snapshot_id is not admitted for this IC request method")]
271 UnexpectedSnapshot,
272 #[error("snapshot_id must contain 1..={MAX_IC_SNAPSHOT_ID_BYTES} raw bytes")]
274 InvalidSnapshotId,
275 #[error("IC request Candid encoding failed: {0}")]
277 Encoding(String),
278 #[error("IC request arguments exceed {MAX_IC_ARGUMENT_BYTES} bytes")]
280 ArgumentsTooLarge,
281 #[error("IC request must have effect class {expected:?}")]
283 EffectMismatch {
284 expected: IcRequestEffect,
286 },
287 #[error("IC request target differs from original binding")]
289 TargetMismatch,
290 #[error("IC request digest differs from original binding")]
292 DigestMismatch,
293}
294
295#[cfg(test)]
296mod tests;