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