Skip to main content

ic_backup/model/ic_snapshot_data/
mod.rs

1//! Metadata-bound snapshot data codecs, without dispatch or transfer completion.
2
3mod extents;
4mod wire;
5pub(crate) use extents::{ExtentPlanningError, planned_extents};
6
7use super::{
8    artifacts::ArtifactChecksumRecord,
9    ic_request::{IcRequestError, MAX_IC_ARGUMENT_BYTES, management_request_digest},
10    ic_snapshot_metadata::IcSnapshotMetadataReply,
11};
12use candid::Principal;
13use ic_management_canister_types::{ReadCanisterSnapshotDataArgs, SnapshotDataKind};
14use std::fmt;
15use thiserror::Error;
16
17/// Maximum requested/decoded data bytes per call; no aggregate allowance is granted.
18pub const MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES: usize = 1024 * 1024;
19/// Maximum raw data reply bytes, allowing wire overhead around a 1 MiB chunk.
20pub const MAX_IC_SNAPSHOT_DATA_REPLY_BYTES: usize = 2 * 1024 * 1024;
21
22/// Exact ephemeral read for one range or chunk in retained snapshot metadata.
23///
24/// The borrowed metadata owns the target/raw ID, declared sizes and chunk set.
25/// Construction validates bytes only, without authenticating that metadata or
26/// granting a reservation, fresh read access, signing, dispatch or repeat call.
27pub struct IcSnapshotDataRequest<'metadata> {
28    metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
29    kind: SnapshotDataKind,
30    target_bytes: Vec<u8>,
31    arguments: Vec<u8>,
32}
33
34impl<'metadata> IcSnapshotDataRequest<'metadata> {
35    /// Encode one bounded nonempty range or exact known chunk-store identity.
36    ///
37    /// # Errors
38    /// Rejects zero/oversized ranges, overflow or ranges beyond declared metadata,
39    /// malformed/unknown chunk hashes and existing argument encoding/size failures.
40    pub fn new(
41        metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
42        kind: SnapshotDataKind,
43    ) -> Result<Self, IcSnapshotDataError> {
44        validate_kind(metadata, &kind)?;
45        let target = Principal::from_text(metadata.request().target())
46            .map_err(|_| IcRequestError::InvalidTarget)?;
47        let arguments = candid::encode_one(ReadCanisterSnapshotDataArgs {
48            canister_id: target,
49            snapshot_id: metadata.request().snapshot_id().to_vec(),
50            kind: kind.clone(),
51        })
52        .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
53        if arguments.len() > MAX_IC_ARGUMENT_BYTES {
54            return Err(IcRequestError::ArgumentsTooLarge.into());
55        }
56        Ok(Self {
57            metadata,
58            kind,
59            target_bytes: target.as_slice().to_vec(),
60            arguments,
61        })
62    }
63
64    /// Read the retained metadata evidence used for range/hash admission.
65    #[must_use]
66    pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
67        self.metadata
68    }
69
70    /// Read the exact admitted upstream range or chunk identity.
71    #[must_use]
72    pub const fn kind(&self) -> &SnapshotDataKind {
73        &self.kind
74    }
75
76    pub(crate) fn matches_kind(&self, original: &SnapshotDataKind) -> bool {
77        use SnapshotDataKind::{StableMemory, WasmChunk, WasmMemory, WasmModule};
78        match (&self.kind, original) {
79            (WasmModule { offset: a, size: b }, WasmModule { offset: c, size: d })
80            | (WasmMemory { offset: a, size: b }, WasmMemory { offset: c, size: d })
81            | (StableMemory { offset: a, size: b }, StableMemory { offset: c, size: d }) => {
82                a == c && b == d
83            }
84            (WasmChunk { hash: a }, WasmChunk { hash: b }) => a == b,
85            _ => false,
86        }
87    }
88
89    /// Read the canonical effective routing target, also encoded in the arguments.
90    #[must_use]
91    pub fn target(&self) -> &str {
92        self.metadata.request().target()
93    }
94
95    /// Read the exact raw snapshot identity, distinct from generic backend tokens.
96    #[must_use]
97    pub fn snapshot_id(&self) -> &[u8] {
98        self.metadata.request().snapshot_id()
99    }
100
101    /// Read exact Candid arguments, without a transport or execution permit.
102    #[must_use]
103    pub fn arguments(&self) -> &[u8] {
104        &self.arguments
105    }
106
107    /// Read the management receiver, separate from effective routing.
108    #[must_use]
109    pub const fn receiver(&self) -> &'static str {
110        "aaaaa-aa"
111    }
112
113    /// Read the sole data-read method, using replicated host update ingress.
114    #[must_use]
115    pub const fn method(&self) -> &'static str {
116        "read_canister_snapshot_data"
117    }
118
119    /// Hash exact wire identity with the existing management payload owner.
120    ///
121    /// Metadata evidence is excluded from this nonrecursive request hash and
122    /// separately included in the reply evidence digest. Spending stays external.
123    #[must_use]
124    pub fn digest(&self) -> ArtifactChecksumRecord {
125        management_request_digest(&self.target_bytes, self.method(), &self.arguments)
126    }
127}
128
129pub(crate) fn validate_kind(
130    metadata: &IcSnapshotMetadataReply<'_>,
131    kind: &SnapshotDataKind,
132) -> Result<(), IcSnapshotDataError> {
133    let values = metadata.metadata();
134    match kind {
135        SnapshotDataKind::WasmModule { offset, size } => {
136            range(*offset, *size, values.wasm_module_size)?;
137        }
138        SnapshotDataKind::WasmMemory { offset, size } => {
139            range(*offset, *size, values.wasm_memory_size)?;
140        }
141        SnapshotDataKind::StableMemory { offset, size } => {
142            range(*offset, *size, values.stable_memory_size)?;
143        }
144        SnapshotDataKind::WasmChunk { hash } => {
145            if hash.len() != 32 {
146                return Err(IcSnapshotDataError::InvalidChunkHash);
147            }
148            if !values
149                .wasm_chunk_store
150                .iter()
151                .any(|chunk| chunk.hash == *hash)
152            {
153                return Err(IcSnapshotDataError::ChunkNotInMetadata);
154            }
155        }
156    }
157    Ok(())
158}
159
160fn range(offset: u64, size: u64, total: u64) -> Result<(), IcSnapshotDataError> {
161    let admitted_size = usize::try_from(size)
162        .is_ok_and(|size| (1..=MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES).contains(&size));
163    if !admitted_size {
164        return Err(IcSnapshotDataError::InvalidRangeSize);
165    }
166    if offset.checked_add(size).is_none_or(|end| end > total) {
167        return Err(IcSnapshotDataError::RangeOutsideMetadata);
168    }
169    Ok(())
170}
171
172impl fmt::Debug for IcSnapshotDataRequest<'_> {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        f.debug_struct("IcSnapshotDataRequest")
175            .field("target", &self.target())
176            .field("argument_bytes", &self.arguments.len())
177            .finish_non_exhaustive()
178    }
179}
180
181/// Exact bounded data bytes associated with one declared request and metadata reply.
182///
183/// Range replies must have the exact requested length. Chunk-store replies must
184/// hash to the requested retained identity, including a valid empty chunk. Neither
185/// condition authenticates origin, proves whole-snapshot coverage or settles effects.
186pub struct IcSnapshotDataReply<'request, 'metadata> {
187    request: &'request IcSnapshotDataRequest<'metadata>,
188    chunk: Vec<u8>,
189    chunk_checksum: ArtifactChecksumRecord,
190    payload_checksum: ArtifactChecksumRecord,
191}
192
193impl<'request, 'metadata> IcSnapshotDataReply<'request, 'metadata> {
194    /// Decode exactly one bounded data argument, then admit length/hash association.
195    ///
196    /// # Errors
197    /// Rejects excessive raw/chunk bytes, malformed/unsupported/skipped/extra/trailing
198    /// wire, an inexact range length or bytes with a different chunk-store hash.
199    pub fn decode(
200        request: &'request IcSnapshotDataRequest<'metadata>,
201        bytes: &[u8],
202    ) -> Result<Self, IcSnapshotDataError> {
203        if bytes.len() > MAX_IC_SNAPSHOT_DATA_REPLY_BYTES {
204            return Err(IcSnapshotDataError::ReplyTooLarge);
205        }
206        let chunk = wire::decode(bytes)?;
207        let chunk_digest = ic_host_artifacts::artifact::Sha256Digest::compute(&chunk);
208        let chunk_checksum = ArtifactChecksumRecord::from_digest(*chunk_digest.as_bytes());
209        match request.kind() {
210            SnapshotDataKind::WasmModule { size, .. }
211            | SnapshotDataKind::WasmMemory { size, .. }
212            | SnapshotDataKind::StableMemory { size, .. } => {
213                if u64::try_from(chunk.len()).ok() != Some(*size) {
214                    return Err(IcSnapshotDataError::LengthMismatch);
215                }
216            }
217            SnapshotDataKind::WasmChunk { hash } => {
218                if chunk_digest.as_bytes().as_slice() != hash.as_slice() {
219                    return Err(IcSnapshotDataError::ChunkHashMismatch);
220                }
221            }
222        }
223        Ok(Self {
224            request,
225            chunk,
226            chunk_checksum,
227            payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
228        })
229    }
230
231    /// Read the exact request and retained metadata association.
232    #[must_use]
233    pub const fn request(&self) -> &'request IcSnapshotDataRequest<'metadata> {
234        self.request
235    }
236
237    /// Read exact admitted data bytes, with no mutable access or persistence side effect.
238    #[must_use]
239    pub fn chunk(&self) -> &[u8] {
240        &self.chunk
241    }
242
243    /// Read SHA-256 of actual data, distinct from raw Candid wire identity.
244    #[must_use]
245    pub const fn chunk_checksum(&self) -> &ArtifactChecksumRecord {
246        &self.chunk_checksum
247    }
248
249    /// Read SHA-256 of exact raw reply bytes.
250    #[must_use]
251    pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
252        &self.payload_checksum
253    }
254
255    /// Bind exact retained metadata, request and raw reply hashes in the data domain.
256    #[must_use]
257    pub fn digest(&self) -> ArtifactChecksumRecord {
258        super::evidence_digest::reply(
259            b"ic-backup/ic-snapshot-data-reply/v1\0",
260            [
261                &self.request.metadata().digest(),
262                &self.request.digest(),
263                &self.payload_checksum,
264            ],
265        )
266    }
267}
268
269impl fmt::Debug for IcSnapshotDataReply<'_, '_> {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        f.debug_struct("IcSnapshotDataReply")
272            .field("request", &self.request)
273            .field("chunk_bytes", &self.chunk.len())
274            .field("payload_checksum", &self.payload_checksum)
275            .finish_non_exhaustive()
276    }
277}
278
279/// Typed metadata/range/data admission failures, without raw payload diagnostics.
280#[derive(Debug, Error, Eq, PartialEq)]
281pub enum IcSnapshotDataError {
282    /// Existing canonical IC argument boundary rejected the request.
283    #[error(transparent)]
284    Request(#[from] IcRequestError),
285    /// Range size is zero or greater than the local single-call bound.
286    #[error("snapshot range size must be 1..={MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES}")]
287    InvalidRangeSize,
288    /// Checked offset plus size exceeds the retained region or overflows nat64.
289    #[error("snapshot range is outside retained metadata")]
290    RangeOutsideMetadata,
291    /// Chunk identity is not exactly 32 SHA-256 bytes.
292    #[error("snapshot chunk identity must be 32 bytes")]
293    InvalidChunkHash,
294    /// The exact chunk identity is absent from retained metadata.
295    #[error("snapshot chunk identity is absent from retained metadata")]
296    ChunkNotInMetadata,
297    /// Raw wire input exceeded its bound before parsing.
298    #[error("snapshot data reply exceeds {MAX_IC_SNAPSHOT_DATA_REPLY_BYTES} bytes")]
299    ReplyTooLarge,
300    /// Wire shape, decoder work, header or actual chunk bounds failed.
301    #[error("invalid or unsupported snapshot data reply")]
302    InvalidReply,
303    /// The decoded range is shorter or longer than the exact request.
304    #[error("snapshot data length differs from the requested range")]
305    LengthMismatch,
306    /// Actual chunk bytes do not hash to the requested retained identity.
307    #[error("snapshot data differs from the requested chunk hash")]
308    ChunkHashMismatch,
309}
310
311#[cfg(test)]
312mod tests;