ic_backup/model/ic_snapshot_upload/
mod.rs1mod attempt;
4mod reply;
5pub use attempt::{IcSnapshotUploadAttempt, IcSnapshotUploadAttemptError};
6pub use reply::{IcSnapshotUploadReply, IcSnapshotUploadReplyKind};
7
8use super::{
9 artifacts::{ArtifactChecksumRecord, ChecksumError},
10 ic_request::{IcRequestError, MAX_IC_SNAPSHOT_ID_BYTES, management_request_digest},
11 ic_snapshot_data::{IcSnapshotDataError, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, validate_kind},
12 ic_snapshot_metadata::IcSnapshotMetadataReply,
13 operation_plan::OperationPlanRecord,
14};
15use candid::Principal;
16use ic_management_canister_types::{
17 SnapshotDataKind, SnapshotDataOffset, UploadCanisterSnapshotDataArgs,
18 UploadCanisterSnapshotMetadataArgs,
19};
20use std::fmt;
21use thiserror::Error;
22
23pub const MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES: usize = 2 * 1024 * 1024;
25pub const MAX_IC_SNAPSHOT_UPLOAD_REPLY_BYTES: usize = 4096;
27
28#[derive(Debug)]
30pub enum IcSnapshotUploadKind {
31 Metadata,
33 Data {
35 snapshot_id: Vec<u8>,
37 source_kind: SnapshotDataKind,
39 chunk_checksum: ArtifactChecksumRecord,
41 metadata_request: ArtifactChecksumRecord,
43 },
44}
45
46pub struct IcSnapshotUploadRequest<'source> {
54 source_plan: &'source OperationPlanRecord,
55 source: &'source IcSnapshotMetadataReply<'source>,
56 source_checksum: ArtifactChecksumRecord,
57 kind: IcSnapshotUploadKind,
58 arguments: Vec<u8>,
59 target_bytes: Vec<u8>,
60}
61
62impl<'source> IcSnapshotUploadRequest<'source> {
63 pub fn metadata(
72 source_plan: &'source OperationPlanRecord,
73 source: &'source IcSnapshotMetadataReply<'source>,
74 source_checksum: &ArtifactChecksumRecord,
75 ) -> Result<Self, IcSnapshotUploadError> {
76 if !source_plan
77 .selected_targets()
78 .iter()
79 .any(|target| target == source.request().target())
80 {
81 return Err(IcSnapshotUploadError::SourceTargetMismatch);
82 }
83 let values = source.metadata();
84 let globals = values
85 .globals
86 .iter()
87 .cloned()
88 .collect::<Option<Vec<_>>>()
89 .ok_or(IcSnapshotUploadError::UnavailableGlobal)?;
90 let target = Principal::from_text(source.request().target())
91 .map_err(|_| IcRequestError::InvalidTarget)?;
92 let arguments = candid::encode_one(UploadCanisterSnapshotMetadataArgs {
93 canister_id: target,
94 replace_snapshot: None,
95 wasm_module_size: values.wasm_module_size,
96 globals,
97 wasm_memory_size: values.wasm_memory_size,
98 stable_memory_size: values.stable_memory_size,
99 certified_data: values.certified_data.clone(),
100 global_timer: values.global_timer.clone(),
101 on_low_wasm_memory_hook_status: values.on_low_wasm_memory_hook_status.clone(),
102 })
103 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
104 Self::from_arguments(
105 source_plan,
106 source,
107 source_checksum,
108 IcSnapshotUploadKind::Metadata,
109 arguments,
110 )
111 }
112
113 pub fn data(
124 metadata: &Self,
125 snapshot_id: &[u8],
126 source_kind: SnapshotDataKind,
127 chunk: &[u8],
128 ) -> Result<Self, IcSnapshotUploadError> {
129 metadata.validate_data_destination(snapshot_id)?;
130 validate_kind(metadata.source, &source_kind)?;
131 if chunk.len() > MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES {
132 return Err(IcSnapshotUploadError::ChunkTooLarge);
133 }
134 let checksum = ArtifactChecksumRecord::from_bytes(chunk);
135 let kind = match &source_kind {
136 SnapshotDataKind::WasmModule { offset, size } => {
137 require_length(chunk, *size)?;
138 SnapshotDataOffset::WasmModule { offset: *offset }
139 }
140 SnapshotDataKind::WasmMemory { offset, size } => {
141 require_length(chunk, *size)?;
142 SnapshotDataOffset::WasmMemory { offset: *offset }
143 }
144 SnapshotDataKind::StableMemory { offset, size } => {
145 require_length(chunk, *size)?;
146 SnapshotDataOffset::StableMemory { offset: *offset }
147 }
148 SnapshotDataKind::WasmChunk { hash } => {
149 checksum.verify(&crate::hash::hex_bytes(hash))?;
150 SnapshotDataOffset::WasmChunk
151 }
152 };
153 let target =
154 Principal::from_text(metadata.target()).map_err(|_| IcRequestError::InvalidTarget)?;
155 let arguments = candid::encode_one(UploadCanisterSnapshotDataArgs {
156 canister_id: target,
157 snapshot_id: snapshot_id.to_vec(),
158 kind,
159 chunk: chunk.to_vec(),
160 })
161 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
162 Self::from_arguments(
163 metadata.source_plan,
164 metadata.source,
165 &metadata.source_checksum,
166 IcSnapshotUploadKind::Data {
167 snapshot_id: snapshot_id.to_vec(),
168 source_kind,
169 chunk_checksum: checksum,
170 metadata_request: metadata.digest(),
171 },
172 arguments,
173 )
174 }
175
176 fn from_arguments(
177 source_plan: &'source OperationPlanRecord,
178 source: &'source IcSnapshotMetadataReply<'source>,
179 source_checksum: &ArtifactChecksumRecord,
180 kind: IcSnapshotUploadKind,
181 arguments: Vec<u8>,
182 ) -> Result<Self, IcSnapshotUploadError> {
183 if arguments.len() > MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES {
184 return Err(IcSnapshotUploadError::ArgumentsTooLarge);
185 }
186 let target = Principal::from_text(source.request().target())
187 .map_err(|_| IcRequestError::InvalidTarget)?;
188 Ok(Self {
189 source_plan,
190 source,
191 source_checksum: source_checksum.clone(),
192 kind,
193 arguments,
194 target_bytes: target.as_slice().to_vec(),
195 })
196 }
197
198 pub(crate) fn validate_data_destination(
199 &self,
200 snapshot_id: &[u8],
201 ) -> Result<(), IcSnapshotUploadError> {
202 if !matches!(self.kind, IcSnapshotUploadKind::Metadata) {
203 return Err(IcSnapshotUploadError::MetadataRequestRequired);
204 }
205 validate_destination(self.source, snapshot_id)
206 }
207
208 #[must_use]
210 pub const fn source_plan(&self) -> &'source OperationPlanRecord {
211 self.source_plan
212 }
213
214 #[must_use]
216 pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
217 self.source
218 }
219 #[must_use]
221 pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
222 &self.source_checksum
223 }
224 #[must_use]
226 pub const fn kind(&self) -> &IcSnapshotUploadKind {
227 &self.kind
228 }
229 #[must_use]
231 pub fn target(&self) -> &str {
232 self.source.request().target()
233 }
234 #[must_use]
236 pub const fn receiver(&self) -> &'static str {
237 "aaaaa-aa"
238 }
239 #[must_use]
241 pub const fn method(&self) -> &'static str {
242 match self.kind {
243 IcSnapshotUploadKind::Metadata => "upload_canister_snapshot_metadata",
244 IcSnapshotUploadKind::Data { .. } => "upload_canister_snapshot_data",
245 }
246 }
247 #[must_use]
249 pub fn arguments(&self) -> &[u8] {
250 &self.arguments
251 }
252 #[must_use]
254 pub fn digest(&self) -> ArtifactChecksumRecord {
255 management_request_digest(&self.target_bytes, self.method(), &self.arguments)
256 }
257 #[must_use]
259 pub fn binding_digest(&self) -> ArtifactChecksumRecord {
260 let mut bytes = b"ic-backup/ic-snapshot-upload-binding/v1\0".to_vec();
261 bytes.extend_from_slice(self.source_plan.digest().hash().as_bytes());
262 bytes.extend_from_slice(self.source.digest().hash().as_bytes());
263 bytes.extend_from_slice(self.source_checksum.hash().as_bytes());
264 if let IcSnapshotUploadKind::Data {
265 metadata_request, ..
266 } = &self.kind
267 {
268 bytes.push(1);
269 bytes.extend_from_slice(metadata_request.hash().as_bytes());
270 } else {
271 bytes.push(0);
272 }
273 bytes.extend_from_slice(self.digest().hash().as_bytes());
274 ArtifactChecksumRecord::from_bytes(&bytes)
275 }
276}
277
278pub(super) fn validate_destination(
279 source: &IcSnapshotMetadataReply<'_>,
280 snapshot_id: &[u8],
281) -> Result<(), IcSnapshotUploadError> {
282 if snapshot_id.is_empty() || snapshot_id.len() > MAX_IC_SNAPSHOT_ID_BYTES {
283 return Err(IcSnapshotUploadError::InvalidDestination);
284 }
285 if snapshot_id == source.request().snapshot_id() {
286 return Err(IcSnapshotUploadError::DestinationReusesSource);
287 }
288 Ok(())
289}
290
291fn require_length(chunk: &[u8], length: u64) -> Result<(), IcSnapshotUploadError> {
292 if u64::try_from(chunk.len()).ok() != Some(length) {
293 return Err(IcSnapshotUploadError::ChunkLengthMismatch);
294 }
295 Ok(())
296}
297
298impl fmt::Debug for IcSnapshotUploadRequest<'_> {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 f.debug_struct("IcSnapshotUploadRequest")
301 .field("target", &self.target())
302 .field("method", &self.method())
303 .field("argument_bytes", &self.arguments.len())
304 .finish_non_exhaustive()
305 }
306}
307
308#[derive(Debug, Error)]
310pub enum IcSnapshotUploadError {
311 #[error("snapshot upload source target differs from the original selection")]
313 SourceTargetMismatch,
314 #[error("snapshot upload requires every original global value")]
316 UnavailableGlobal,
317 #[error("snapshot upload requires an original metadata request")]
319 MetadataRequestRequired,
320 #[error("snapshot upload destination ID is invalid")]
322 InvalidDestination,
323 #[error("snapshot upload destination reuses the original source ID")]
325 DestinationReusesSource,
326 #[error("snapshot upload chunk length differs from the source range")]
328 ChunkLengthMismatch,
329 #[error("snapshot upload chunk exceeds bound")]
331 ChunkTooLarge,
332 #[error("snapshot upload arguments exceed bound")]
334 ArgumentsTooLarge,
335 #[error("snapshot upload reply exceeds bound")]
337 ReplyTooLarge,
338 #[error("snapshot upload reply is invalid")]
340 InvalidReply,
341 #[error(transparent)]
343 Data(#[from] IcSnapshotDataError),
344 #[error(transparent)]
346 Checksum(#[from] ChecksumError),
347 #[error(transparent)]
349 Request(#[from] IcRequestError),
350}
351
352#[cfg(test)]
353mod tests;