ic_backup/model/ic_snapshot_upload/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4use crate::model::ic_snapshot_data::DataExtent;
5use ic_platform::candid::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, IcSnapshotDataUploadPlanningError};
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(UploadCanisterSnapshotMetadataArgs {
98 canister_id: target.into(),
99 replace_snapshot: None,
100 wasm_module_size: values.wasm_module_size,
101 globals,
102 wasm_memory_size: values.wasm_memory_size,
103 stable_memory_size: values.stable_memory_size,
104 certified_data: values.certified_data.clone(),
105 global_timer: values.global_timer.clone(),
106 on_low_wasm_memory_hook_status: values.on_low_wasm_memory_hook_status.clone(),
107 })
108 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
109 Self::from_arguments(
110 source_plan,
111 source,
112 source_checksum,
113 IcSnapshotUploadKind::Metadata,
114 arguments,
115 )
116 }
117
118 pub fn data(
129 metadata: &Self,
130 snapshot_id: &[u8],
131 source_kind: SnapshotDataKind,
132 chunk: &[u8],
133 ) -> Result<Self, IcSnapshotUploadError> {
134 metadata.validate_data_destination(snapshot_id)?;
135 validate_kind(metadata.source, &source_kind)?;
136 if chunk.len() > MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES {
137 return Err(IcSnapshotUploadError::ChunkTooLarge);
138 }
139 let checksum = ArtifactChecksumRecord::from_bytes(chunk);
140 let kind = match DataExtent::from(&source_kind) {
141 DataExtent::Region {
142 region,
143 offset,
144 size,
145 } => {
146 require_length(chunk, size)?;
147 region.upload_offset(offset)
148 }
149 DataExtent::Chunk { hash } => {
150 let digest = ic_host_artifacts::artifact::Sha256Digest::from_bytes(
151 hash.try_into()
152 .map_err(|_| IcSnapshotDataError::InvalidChunkHash)?,
153 );
154 checksum.verify(&digest.to_string())?;
155 SnapshotDataOffset::WasmChunk
156 }
157 };
158 let target = metadata.source.request().principal();
159 let arguments = contract::encode_one(UploadCanisterSnapshotDataArgs {
160 canister_id: target.into(),
161 snapshot_id: snapshot_id.to_vec(),
162 kind,
163 chunk: chunk.to_vec(),
164 })
165 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
166 Self::from_arguments(
167 metadata.source_plan,
168 metadata.source,
169 &metadata.source_checksum,
170 IcSnapshotUploadKind::Data {
171 snapshot_id: snapshot_id.to_vec(),
172 source_kind,
173 chunk_checksum: checksum,
174 metadata_request: metadata.digest(),
175 },
176 arguments,
177 )
178 }
179
180 fn from_arguments(
181 source_plan: &'source OperationPlanRecord,
182 source: &'source IcSnapshotMetadataReply<'source>,
183 source_checksum: &ArtifactChecksumRecord,
184 kind: IcSnapshotUploadKind,
185 arguments: Vec<u8>,
186 ) -> Result<Self, IcSnapshotUploadError> {
187 if arguments.len() > MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES {
188 return Err(IcSnapshotUploadError::ArgumentsTooLarge);
189 }
190 Ok(Self {
191 source_plan,
192 source,
193 source_checksum: source_checksum.clone(),
194 kind,
195 arguments,
196 })
197 }
198
199 pub(crate) fn validate_data_destination(
200 &self,
201 snapshot_id: &[u8],
202 ) -> Result<(), IcSnapshotUploadError> {
203 if !matches!(self.kind, IcSnapshotUploadKind::Metadata) {
204 return Err(IcSnapshotUploadError::MetadataRequestRequired);
205 }
206 validate_destination(self.source, snapshot_id)
207 }
208
209 #[must_use]
211 pub const fn source_plan(&self) -> &'source OperationPlanRecord {
212 self.source_plan
213 }
214
215 #[must_use]
217 pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
218 self.source
219 }
220 #[must_use]
222 pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
223 &self.source_checksum
224 }
225 #[must_use]
227 pub const fn kind(&self) -> &IcSnapshotUploadKind {
228 &self.kind
229 }
230 #[must_use]
232 pub fn target(&self) -> &str {
233 self.source.request().target()
234 }
235 #[must_use]
237 pub const fn receiver(&self) -> &'static str {
238 "aaaaa-aa"
239 }
240 #[must_use]
242 pub const fn method(&self) -> &'static str {
243 match self.kind {
244 IcSnapshotUploadKind::Metadata => "upload_canister_snapshot_metadata",
245 IcSnapshotUploadKind::Data { .. } => "upload_canister_snapshot_data",
246 }
247 }
248 #[must_use]
250 pub fn arguments(&self) -> &[u8] {
251 &self.arguments
252 }
253 #[must_use]
255 pub fn digest(&self) -> ArtifactChecksumRecord {
256 management_request_digest(
257 self.source.request().principal().as_slice(),
258 self.method(),
259 &self.arguments,
260 )
261 }
262 #[must_use]
264 pub fn binding_digest(&self) -> ArtifactChecksumRecord {
265 let mut bytes = EvidenceDigest::new(b"ic-backup/ic-snapshot-upload-binding/v1\0");
266 bytes.checksum(&self.source_plan.digest());
267 bytes.checksum(&self.source.digest());
268 bytes.checksum(&self.source_checksum);
269 if let IcSnapshotUploadKind::Data {
270 metadata_request, ..
271 } = &self.kind
272 {
273 bytes.byte(1);
274 bytes.checksum(metadata_request);
275 } else {
276 bytes.byte(0);
277 }
278 bytes.checksum(&self.digest());
279 bytes.finish()
280 }
281}
282
283pub(super) fn validate_destination(
284 source: &IcSnapshotMetadataReply<'_>,
285 snapshot_id: &[u8],
286) -> Result<(), IcSnapshotUploadError> {
287 if !admits_snapshot_id(snapshot_id) {
288 return Err(IcSnapshotUploadError::InvalidDestination);
289 }
290 if snapshot_id == source.request().snapshot_id() {
291 return Err(IcSnapshotUploadError::DestinationReusesSource);
292 }
293 Ok(())
294}
295
296fn require_length(chunk: &[u8], length: u64) -> Result<(), IcSnapshotUploadError> {
297 if u64::try_from(chunk.len()).ok() != Some(length) {
298 return Err(IcSnapshotUploadError::ChunkLengthMismatch);
299 }
300 Ok(())
301}
302
303impl fmt::Debug for IcSnapshotUploadRequest<'_> {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 f.debug_struct("IcSnapshotUploadRequest")
306 .field("target", &self.target())
307 .field("method", &self.method())
308 .field("argument_bytes", &self.arguments.len())
309 .finish_non_exhaustive()
310 }
311}
312
313#[derive(Debug, Error)]
315pub enum IcSnapshotUploadError {
316 #[error("snapshot upload source target differs from the original selection")]
318 SourceTargetMismatch,
319 #[error("snapshot upload requires every original global value")]
321 UnavailableGlobal,
322 #[error("snapshot upload requires an original metadata request")]
324 MetadataRequestRequired,
325 #[error("snapshot upload destination ID is invalid")]
327 InvalidDestination,
328 #[error("snapshot upload destination reuses the original source ID")]
330 DestinationReusesSource,
331 #[error("snapshot upload chunk length differs from the source range")]
333 ChunkLengthMismatch,
334 #[error("snapshot upload chunk exceeds bound")]
336 ChunkTooLarge,
337 #[error("snapshot upload arguments exceed bound")]
339 ArgumentsTooLarge,
340 #[error("snapshot upload reply exceeds bound")]
342 ReplyTooLarge,
343 #[error("snapshot upload reply is invalid")]
345 InvalidReply,
346 #[error(transparent)]
348 Data(#[from] IcSnapshotDataError),
349 #[error(transparent)]
351 Checksum(#[from] ChecksumError),
352 #[error(transparent)]
354 Request(#[from] IcRequestError),
355}
356
357#[cfg(test)]
358mod tests;