ic_backup/model/ic_snapshot_upload/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4use crate::model::ic_snapshot_data::DataExtent;
5use ic_platform::candid_codec as contract;
6mod attempt;
7mod plan;
8pub(crate) use plan::UploadCompletion;
9mod reply;
10pub(crate) use attempt::original_authority;
11pub use attempt::{IcSnapshotUploadAttempt, IcSnapshotUploadAttemptError};
12pub(crate) use plan::DataUploadPreparation;
13pub use plan::IcSnapshotDataUploadPlan;
14pub use reply::{IcSnapshotUploadReply, IcSnapshotUploadReplyKind};
15
16use super::{
17 artifacts::{ArtifactChecksumRecord, ChecksumError},
18 ic_request::{IcRequestError, admits_snapshot_id, management_request_digest},
19 ic_snapshot_data::{IcSnapshotDataError, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, validate_kind},
20 ic_snapshot_metadata::IcSnapshotMetadataReply,
21 operation_plan::OperationPlanRecord,
22};
23use ic_platform::management_canister_types::{
24 SnapshotDataKind, SnapshotDataOffset, UploadCanisterSnapshotDataArgs,
25 UploadCanisterSnapshotMetadataArgs,
26};
27use std::fmt;
28use thiserror::Error;
29
30pub const MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES: usize = 2 * 1024 * 1024;
32pub const MAX_IC_SNAPSHOT_UPLOAD_REPLY_BYTES: usize = 4096;
34
35#[derive(Debug)]
37pub enum IcSnapshotUploadKind {
38 Metadata,
40 Data {
42 snapshot_id: Vec<u8>,
44 source_kind: SnapshotDataKind,
46 chunk_checksum: ArtifactChecksumRecord,
48 metadata_request: ArtifactChecksumRecord,
50 },
51}
52
53pub struct IcSnapshotUploadRequest<'source> {
61 source_plan: &'source OperationPlanRecord,
62 source: &'source IcSnapshotMetadataReply<'source>,
63 source_checksum: ArtifactChecksumRecord,
64 kind: IcSnapshotUploadKind,
65 arguments: Vec<u8>,
66}
67
68impl<'source> IcSnapshotUploadRequest<'source> {
69 pub fn metadata(
78 source_plan: &'source OperationPlanRecord,
79 source: &'source IcSnapshotMetadataReply<'source>,
80 source_checksum: &ArtifactChecksumRecord,
81 ) -> Result<Self, IcSnapshotUploadError> {
82 if !source_plan
83 .selected_targets()
84 .iter()
85 .any(|target| target == source.request().target())
86 {
87 return Err(IcSnapshotUploadError::SourceTargetMismatch);
88 }
89 let values = source.metadata();
90 let globals = values
91 .globals
92 .iter()
93 .cloned()
94 .collect::<Option<Vec<_>>>()
95 .ok_or(IcSnapshotUploadError::UnavailableGlobal)?;
96 let target = source.request().principal();
97 let arguments = contract::encode_one(
98 &UploadCanisterSnapshotMetadataArgs {
99 canister_id: target,
100 replace_snapshot: None,
101 wasm_module_size: values.wasm_module_size,
102 globals,
103 wasm_memory_size: values.wasm_memory_size,
104 stable_memory_size: values.stable_memory_size,
105 certified_data: values.certified_data.clone(),
106 global_timer: values.global_timer.clone(),
107 on_low_wasm_memory_hook_status: values.on_low_wasm_memory_hook_status.clone(),
108 },
109 MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES,
110 )
111 .map_err(|error| match error {
112 contract::Error::Limit => IcSnapshotUploadError::ArgumentsTooLarge,
113 contract::Error::Schema => IcRequestError::Encoding(format!("{error:?}")).into(),
114 })?;
115 Self::from_arguments(
116 source_plan,
117 source,
118 source_checksum,
119 IcSnapshotUploadKind::Metadata,
120 arguments,
121 )
122 }
123
124 pub fn data(
135 metadata: &Self,
136 snapshot_id: &[u8],
137 source_kind: SnapshotDataKind,
138 chunk: &[u8],
139 ) -> Result<Self, IcSnapshotUploadError> {
140 metadata.validate_data_destination(snapshot_id)?;
141 validate_kind(metadata.source, &source_kind)?;
142 if chunk.len() > MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES {
143 return Err(IcSnapshotUploadError::ChunkTooLarge);
144 }
145 let checksum = ArtifactChecksumRecord::from_bytes(chunk);
146 let kind = match DataExtent::from(&source_kind) {
147 DataExtent::Region {
148 region,
149 offset,
150 size,
151 } => {
152 require_length(chunk, size)?;
153 region.upload_offset(offset)
154 }
155 DataExtent::Chunk { hash } => {
156 let digest = ic_host_artifacts::artifact::Sha256Digest::from_bytes(
157 hash.try_into()
158 .map_err(|_| IcSnapshotDataError::InvalidChunkHash)?,
159 );
160 checksum.verify(&digest.to_string())?;
161 SnapshotDataOffset::WasmChunk
162 }
163 };
164 let target = metadata.source.request().principal();
165 let arguments = contract::encode_one(
166 &UploadCanisterSnapshotDataArgs {
167 canister_id: target,
168 snapshot_id: snapshot_id.to_vec(),
169 kind,
170 chunk: chunk.to_vec(),
171 },
172 MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES,
173 )
174 .map_err(|error| match error {
175 contract::Error::Limit => IcSnapshotUploadError::ArgumentsTooLarge,
176 contract::Error::Schema => IcRequestError::Encoding(format!("{error:?}")).into(),
177 })?;
178 Self::from_arguments(
179 metadata.source_plan,
180 metadata.source,
181 &metadata.source_checksum,
182 IcSnapshotUploadKind::Data {
183 snapshot_id: snapshot_id.to_vec(),
184 source_kind,
185 chunk_checksum: checksum,
186 metadata_request: metadata.digest(),
187 },
188 arguments,
189 )
190 }
191
192 fn from_arguments(
193 source_plan: &'source OperationPlanRecord,
194 source: &'source IcSnapshotMetadataReply<'source>,
195 source_checksum: &ArtifactChecksumRecord,
196 kind: IcSnapshotUploadKind,
197 arguments: Vec<u8>,
198 ) -> Result<Self, IcSnapshotUploadError> {
199 if arguments.len() > MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES {
200 return Err(IcSnapshotUploadError::ArgumentsTooLarge);
201 }
202 Ok(Self {
203 source_plan,
204 source,
205 source_checksum: source_checksum.clone(),
206 kind,
207 arguments,
208 })
209 }
210
211 pub(crate) fn validate_data_destination(
212 &self,
213 snapshot_id: &[u8],
214 ) -> Result<(), IcSnapshotUploadError> {
215 if !matches!(self.kind, IcSnapshotUploadKind::Metadata) {
216 return Err(IcSnapshotUploadError::MetadataRequestRequired);
217 }
218 validate_destination(self.source, snapshot_id)
219 }
220
221 #[must_use]
223 pub const fn source_plan(&self) -> &'source OperationPlanRecord {
224 self.source_plan
225 }
226
227 #[must_use]
229 pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
230 self.source
231 }
232 #[must_use]
234 pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
235 &self.source_checksum
236 }
237 #[must_use]
239 pub const fn kind(&self) -> &IcSnapshotUploadKind {
240 &self.kind
241 }
242 #[must_use]
244 pub fn target(&self) -> &str {
245 self.source.request().target()
246 }
247 #[must_use]
249 pub const fn receiver(&self) -> &'static str {
250 "aaaaa-aa"
251 }
252 #[must_use]
254 pub const fn method(&self) -> &'static str {
255 match self.kind {
256 IcSnapshotUploadKind::Metadata => "upload_canister_snapshot_metadata",
257 IcSnapshotUploadKind::Data { .. } => "upload_canister_snapshot_data",
258 }
259 }
260 #[must_use]
262 pub fn arguments(&self) -> &[u8] {
263 &self.arguments
264 }
265 #[must_use]
267 pub fn digest(&self) -> ArtifactChecksumRecord {
268 management_request_digest(
269 self.source.request().principal().as_slice(),
270 self.method(),
271 &self.arguments,
272 )
273 }
274 #[must_use]
276 pub fn binding_digest(&self) -> ArtifactChecksumRecord {
277 let mut bytes = EvidenceDigest::new(b"ic-backup/ic-snapshot-upload-binding/v1\0");
278 bytes.checksum(&self.source_plan.digest());
279 bytes.checksum(&self.source.digest());
280 bytes.checksum(&self.source_checksum);
281 if let IcSnapshotUploadKind::Data {
282 metadata_request, ..
283 } = &self.kind
284 {
285 bytes.byte(1);
286 bytes.checksum(metadata_request);
287 } else {
288 bytes.byte(0);
289 }
290 bytes.checksum(&self.digest());
291 bytes.finish()
292 }
293}
294
295pub(super) fn validate_destination(
296 source: &IcSnapshotMetadataReply<'_>,
297 snapshot_id: &[u8],
298) -> Result<(), IcSnapshotUploadError> {
299 if !admits_snapshot_id(snapshot_id) {
300 return Err(IcSnapshotUploadError::InvalidDestination);
301 }
302 if snapshot_id == source.request().snapshot_id() {
303 return Err(IcSnapshotUploadError::DestinationReusesSource);
304 }
305 Ok(())
306}
307
308fn require_length(chunk: &[u8], length: u64) -> Result<(), IcSnapshotUploadError> {
309 if u64::try_from(chunk.len()).ok() != Some(length) {
310 return Err(IcSnapshotUploadError::ChunkLengthMismatch);
311 }
312 Ok(())
313}
314
315impl fmt::Debug for IcSnapshotUploadRequest<'_> {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 f.debug_struct("IcSnapshotUploadRequest")
318 .field("target", &self.target())
319 .field("method", &self.method())
320 .field("argument_bytes", &self.arguments.len())
321 .finish_non_exhaustive()
322 }
323}
324
325#[derive(Debug, Error)]
327pub enum IcSnapshotUploadError {
328 #[error("snapshot upload source target differs from the original selection")]
330 SourceTargetMismatch,
331 #[error("snapshot upload requires every original global value")]
333 UnavailableGlobal,
334 #[error("snapshot upload requires an original metadata request")]
336 MetadataRequestRequired,
337 #[error("snapshot upload destination ID is invalid")]
339 InvalidDestination,
340 #[error("snapshot upload destination reuses the original source ID")]
342 DestinationReusesSource,
343 #[error("snapshot upload chunk length differs from the source range")]
345 ChunkLengthMismatch,
346 #[error("snapshot upload chunk exceeds bound")]
348 ChunkTooLarge,
349 #[error("snapshot upload arguments exceed bound")]
351 ArgumentsTooLarge,
352 #[error("snapshot upload reply exceeds bound")]
354 ReplyTooLarge,
355 #[error("snapshot upload reply is invalid")]
357 InvalidReply,
358 #[error(transparent)]
360 Data(#[from] IcSnapshotDataError),
361 #[error(transparent)]
363 Checksum(#[from] ChecksumError),
364 #[error(transparent)]
366 Request(#[from] IcRequestError),
367}
368
369#[cfg(test)]
370mod tests;