ic_backup/model/ic_snapshot_data/
mod.rs1use ic_platform::candid::contract;
4mod extents;
5mod kind;
6mod wire;
7pub(crate) use extents::{ExtentPlanningError, planned_extents};
8pub(crate) use kind::DataExtent;
9
10use super::{
11 artifacts::ArtifactChecksumRecord,
12 ic_request::{IcRequestError, MAX_IC_ARGUMENT_BYTES, management_request_digest},
13 ic_snapshot_metadata::IcSnapshotMetadataReply,
14};
15use ic_platform::Principal;
16use ic_platform::management_canister_types::{ReadCanisterSnapshotDataArgs, SnapshotDataKind};
17use std::fmt;
18use thiserror::Error;
19
20pub const MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES: usize = 1024 * 1024;
22pub const MAX_IC_SNAPSHOT_DATA_REPLY_BYTES: usize = 2 * 1024 * 1024;
24
25pub struct IcSnapshotDataRequest<'metadata> {
31 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
32 kind: SnapshotDataKind,
33 target_bytes: Vec<u8>,
34 arguments: Vec<u8>,
35}
36
37impl<'metadata> IcSnapshotDataRequest<'metadata> {
38 pub fn new(
44 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
45 kind: SnapshotDataKind,
46 ) -> Result<Self, IcSnapshotDataError> {
47 validate_kind(metadata, &kind)?;
48 let target = Principal::from_text(metadata.request().target())
49 .map_err(|_| IcRequestError::InvalidTarget)?;
50 let arguments = contract::encode_one(ReadCanisterSnapshotDataArgs {
51 canister_id: target.into(),
52 snapshot_id: metadata.request().snapshot_id().to_vec(),
53 kind: kind.clone(),
54 })
55 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
56 if arguments.len() > MAX_IC_ARGUMENT_BYTES {
57 return Err(IcRequestError::ArgumentsTooLarge.into());
58 }
59 Ok(Self {
60 metadata,
61 kind,
62 target_bytes: target.as_slice().to_vec(),
63 arguments,
64 })
65 }
66
67 #[must_use]
69 pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
70 self.metadata
71 }
72
73 #[must_use]
75 pub const fn kind(&self) -> &SnapshotDataKind {
76 &self.kind
77 }
78
79 pub(crate) fn matches_kind(&self, original: &SnapshotDataKind) -> bool {
80 DataExtent::from(&self.kind) == DataExtent::from(original)
81 }
82
83 #[must_use]
85 pub fn target(&self) -> &str {
86 self.metadata.request().target()
87 }
88
89 #[must_use]
91 pub fn snapshot_id(&self) -> &[u8] {
92 self.metadata.request().snapshot_id()
93 }
94
95 #[must_use]
97 pub fn arguments(&self) -> &[u8] {
98 &self.arguments
99 }
100
101 #[must_use]
103 pub const fn receiver(&self) -> &'static str {
104 "aaaaa-aa"
105 }
106
107 #[must_use]
109 pub const fn method(&self) -> &'static str {
110 "read_canister_snapshot_data"
111 }
112
113 #[must_use]
118 pub fn digest(&self) -> ArtifactChecksumRecord {
119 management_request_digest(&self.target_bytes, self.method(), &self.arguments)
120 }
121}
122
123pub(crate) fn validate_kind(
124 metadata: &IcSnapshotMetadataReply<'_>,
125 kind: &SnapshotDataKind,
126) -> Result<(), IcSnapshotDataError> {
127 let values = metadata.metadata();
128 match DataExtent::from(kind) {
129 DataExtent::Region {
130 index,
131 offset,
132 size,
133 } => {
134 let sizes = [
135 values.wasm_module_size,
136 values.wasm_memory_size,
137 values.stable_memory_size,
138 ];
139 range(offset, size, sizes[index])?;
140 }
141 DataExtent::Chunk { hash } => {
142 if hash.len() != 32 {
143 return Err(IcSnapshotDataError::InvalidChunkHash);
144 }
145 if !values
146 .wasm_chunk_store
147 .iter()
148 .any(|chunk| chunk.hash == hash)
149 {
150 return Err(IcSnapshotDataError::ChunkNotInMetadata);
151 }
152 }
153 }
154 Ok(())
155}
156
157fn range(offset: u64, size: u64, total: u64) -> Result<(), IcSnapshotDataError> {
158 let admitted_size = usize::try_from(size)
159 .is_ok_and(|size| (1..=MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES).contains(&size));
160 if !admitted_size {
161 return Err(IcSnapshotDataError::InvalidRangeSize);
162 }
163 if offset.checked_add(size).is_none_or(|end| end > total) {
164 return Err(IcSnapshotDataError::RangeOutsideMetadata);
165 }
166 Ok(())
167}
168
169impl fmt::Debug for IcSnapshotDataRequest<'_> {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.debug_struct("IcSnapshotDataRequest")
172 .field("target", &self.target())
173 .field("argument_bytes", &self.arguments.len())
174 .finish_non_exhaustive()
175 }
176}
177
178pub struct IcSnapshotDataReply<'request, 'metadata> {
184 request: &'request IcSnapshotDataRequest<'metadata>,
185 chunk: Vec<u8>,
186 chunk_checksum: ArtifactChecksumRecord,
187 payload_checksum: ArtifactChecksumRecord,
188}
189
190impl<'request, 'metadata> IcSnapshotDataReply<'request, 'metadata> {
191 pub fn decode(
197 request: &'request IcSnapshotDataRequest<'metadata>,
198 bytes: &[u8],
199 ) -> Result<Self, IcSnapshotDataError> {
200 if bytes.len() > MAX_IC_SNAPSHOT_DATA_REPLY_BYTES {
201 return Err(IcSnapshotDataError::ReplyTooLarge);
202 }
203 let chunk = wire::decode(bytes)?;
204 let chunk_digest = ic_host_artifacts::artifact::Sha256Digest::compute(&chunk);
205 let chunk_checksum = ArtifactChecksumRecord::from_digest(*chunk_digest.as_bytes());
206 match DataExtent::from(request.kind()) {
207 DataExtent::Region { size, .. } => {
208 if u64::try_from(chunk.len()).ok() != Some(size) {
209 return Err(IcSnapshotDataError::LengthMismatch);
210 }
211 }
212 DataExtent::Chunk { hash } => {
213 if chunk_digest.as_bytes().as_slice() != hash {
214 return Err(IcSnapshotDataError::ChunkHashMismatch);
215 }
216 }
217 }
218 Ok(Self {
219 request,
220 chunk,
221 chunk_checksum,
222 payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
223 })
224 }
225
226 #[must_use]
228 pub const fn request(&self) -> &'request IcSnapshotDataRequest<'metadata> {
229 self.request
230 }
231
232 #[must_use]
234 pub fn chunk(&self) -> &[u8] {
235 &self.chunk
236 }
237
238 #[must_use]
240 pub const fn chunk_checksum(&self) -> &ArtifactChecksumRecord {
241 &self.chunk_checksum
242 }
243
244 #[must_use]
246 pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
247 &self.payload_checksum
248 }
249
250 #[must_use]
252 pub fn digest(&self) -> ArtifactChecksumRecord {
253 super::evidence_digest::reply(
254 b"ic-backup/ic-snapshot-data-reply/v1\0",
255 [
256 &self.request.metadata().digest(),
257 &self.request.digest(),
258 &self.payload_checksum,
259 ],
260 )
261 }
262}
263
264impl fmt::Debug for IcSnapshotDataReply<'_, '_> {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 f.debug_struct("IcSnapshotDataReply")
267 .field("request", &self.request)
268 .field("chunk_bytes", &self.chunk.len())
269 .field("payload_checksum", &self.payload_checksum)
270 .finish_non_exhaustive()
271 }
272}
273
274#[derive(Debug, Error, Eq, PartialEq)]
276pub enum IcSnapshotDataError {
277 #[error(transparent)]
279 Request(#[from] IcRequestError),
280 #[error("snapshot range size must be 1..={MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES}")]
282 InvalidRangeSize,
283 #[error("snapshot range is outside retained metadata")]
285 RangeOutsideMetadata,
286 #[error("snapshot chunk identity must be 32 bytes")]
288 InvalidChunkHash,
289 #[error("snapshot chunk identity is absent from retained metadata")]
291 ChunkNotInMetadata,
292 #[error("snapshot data reply exceeds {MAX_IC_SNAPSHOT_DATA_REPLY_BYTES} bytes")]
294 ReplyTooLarge,
295 #[error("invalid or unsupported snapshot data reply")]
297 InvalidReply,
298 #[error("snapshot data length differs from the requested range")]
300 LengthMismatch,
301 #[error("snapshot data differs from the requested chunk hash")]
303 ChunkHashMismatch,
304}
305
306#[cfg(test)]
307mod tests;