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