Skip to main content

ic_backup/model/ic_snapshot_upload/
mod.rs

1//! Exact same-target snapshot upload codecs and passive original-reservation binding.
2
3use crate::model::evidence_digest::EvidenceDigest;
4mod attempt;
5mod plan;
6mod reply;
7pub(crate) use attempt::original_authority;
8pub use attempt::{IcSnapshotUploadAttempt, IcSnapshotUploadAttemptError};
9pub use plan::{IcSnapshotDataUploadPlan, IcSnapshotDataUploadPlanningError};
10pub use reply::{IcSnapshotUploadReply, IcSnapshotUploadReplyKind};
11
12use super::{
13    artifacts::{ArtifactChecksumRecord, ChecksumError},
14    ic_request::{IcRequestError, MAX_IC_SNAPSHOT_ID_BYTES, management_request_digest},
15    ic_snapshot_data::{IcSnapshotDataError, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, validate_kind},
16    ic_snapshot_metadata::IcSnapshotMetadataReply,
17    operation_plan::OperationPlanRecord,
18};
19use candid::Principal;
20use ic_management_canister_types::{
21    SnapshotDataKind, SnapshotDataOffset, UploadCanisterSnapshotDataArgs,
22    UploadCanisterSnapshotMetadataArgs,
23};
24use std::fmt;
25use thiserror::Error;
26
27/// Maximum upload arguments, including finite Candid overhead around a 1 MiB chunk.
28pub const MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES: usize = 2 * 1024 * 1024;
29/// Maximum raw upload reply bytes before finite decoding.
30pub const MAX_IC_SNAPSHOT_UPLOAD_REPLY_BYTES: usize = 4096;
31
32/// Closed upload declaration; each branch retains exact original source association.
33#[derive(Debug)]
34pub enum IcSnapshotUploadKind {
35    /// Allocate a new snapshot with exact representable metadata, without replacement.
36    Metadata,
37    /// Write exact bounded bytes into an independently qualified new upload snapshot.
38    Data {
39        /// Exact destination raw ID, separate from the retained source raw ID/token.
40        snapshot_id: Vec<u8>,
41        /// Original source extent or known hash; chunk hashes are not part of upload wire.
42        source_kind: SnapshotDataKind,
43        /// Hash of the actual bounded chunk bytes encoded in the arguments.
44        chunk_checksum: ArtifactChecksumRecord,
45        /// Exact original metadata-allocation wire request digest.
46        metadata_request: ArtifactChecksumRecord,
47    },
48}
49
50/// Ephemeral exact wire payload with retained source evidence; no dispatch permission.
51///
52/// Pure construction admits declarations only. Guarded persistence operations supply
53/// fresh local byte binding. Integrations qualify authentic source/complete transfer,
54/// new destination attribution, original spending, current controller/custody and
55/// same-release application safety. No serialization, replacement, relocation,
56/// allowance or automatic outcome is introduced.
57pub struct IcSnapshotUploadRequest<'source> {
58    source_plan: &'source OperationPlanRecord,
59    source: &'source IcSnapshotMetadataReply<'source>,
60    source_checksum: ArtifactChecksumRecord,
61    kind: IcSnapshotUploadKind,
62    arguments: Vec<u8>,
63    target_bytes: Vec<u8>,
64}
65
66impl<'source> IcSnapshotUploadRequest<'source> {
67    /// Encode exact uploadable original metadata for the same declared canister.
68    ///
69    /// Globals retain order and bits. Unavailable slots reject; absent timer/hook
70    /// values stay absent and do not default to inactive/ready states. Replacement
71    /// is always absent; this payload has no source-snapshot deletion lane.
72    /// Build these nonrecursive bytes before retaining their original operation plan.
73    /// # Errors
74    /// Rejects unavailable globals, encoding failure and excessive arguments.
75    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 = Principal::from_text(source.request().target())
95            .map_err(|_| IcRequestError::InvalidTarget)?;
96        let arguments = candid::encode_one(UploadCanisterSnapshotMetadataArgs {
97            canister_id: target,
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    /// Encode one exact source extent/chunk for a separately qualified new raw ID.
118    ///
119    /// Requires the original metadata upload declaration. Regions are nonempty and
120    /// bounded to 1 MiB; an empty known chunk requires its exact SHA-256. Actual
121    /// new snapshot attribution is not inferred from a supplied ID or decoded reply.
122    /// Data requests include their new ID before their own plan/reservation is retained;
123    /// metadata authority never supplies data-call spending or resets prior journals.
124    /// # Errors
125    /// Rejects a non-metadata original, invalid/reused ID, source range/hash or byte
126    /// mismatch, excessive data/arguments and encoding failure.
127    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 =
163            Principal::from_text(metadata.target()).map_err(|_| IcRequestError::InvalidTarget)?;
164        let arguments = candid::encode_one(UploadCanisterSnapshotDataArgs {
165            canister_id: target,
166            snapshot_id: snapshot_id.to_vec(),
167            kind,
168            chunk: chunk.to_vec(),
169        })
170        .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
171        Self::from_arguments(
172            metadata.source_plan,
173            metadata.source,
174            &metadata.source_checksum,
175            IcSnapshotUploadKind::Data {
176                snapshot_id: snapshot_id.to_vec(),
177                source_kind,
178                chunk_checksum: checksum,
179                metadata_request: metadata.digest(),
180            },
181            arguments,
182        )
183    }
184
185    fn from_arguments(
186        source_plan: &'source OperationPlanRecord,
187        source: &'source IcSnapshotMetadataReply<'source>,
188        source_checksum: &ArtifactChecksumRecord,
189        kind: IcSnapshotUploadKind,
190        arguments: Vec<u8>,
191    ) -> Result<Self, IcSnapshotUploadError> {
192        if arguments.len() > MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES {
193            return Err(IcSnapshotUploadError::ArgumentsTooLarge);
194        }
195        let target = Principal::from_text(source.request().target())
196            .map_err(|_| IcRequestError::InvalidTarget)?;
197        Ok(Self {
198            source_plan,
199            source,
200            source_checksum: source_checksum.clone(),
201            kind,
202            arguments,
203            target_bytes: target.as_slice().to_vec(),
204        })
205    }
206
207    pub(crate) fn validate_data_destination(
208        &self,
209        snapshot_id: &[u8],
210    ) -> Result<(), IcSnapshotUploadError> {
211        if !matches!(self.kind, IcSnapshotUploadKind::Metadata) {
212            return Err(IcSnapshotUploadError::MetadataRequestRequired);
213        }
214        validate_destination(self.source, snapshot_id)
215    }
216
217    /// Read the exact original source plan; later upload intent must retain its context.
218    #[must_use]
219    pub const fn source_plan(&self) -> &'source OperationPlanRecord {
220        self.source_plan
221    }
222
223    /// Read original metadata/request evidence; it authenticates no source.
224    #[must_use]
225    pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
226        self.source
227    }
228    /// Read declared retained tree checksum; pure construction verifies no files.
229    #[must_use]
230    pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
231        &self.source_checksum
232    }
233    /// Read the closed kind and exact source/destination association.
234    #[must_use]
235    pub const fn kind(&self) -> &IcSnapshotUploadKind {
236        &self.kind
237    }
238    /// Read the same canonical source/destination canister target.
239    #[must_use]
240    pub fn target(&self) -> &str {
241        self.source.request().target()
242    }
243    /// Read fixed management receiver, separate from effective routing.
244    #[must_use]
245    pub const fn receiver(&self) -> &'static str {
246        "aaaaa-aa"
247    }
248    /// Read the exact replicated host-update method; no query mode exists.
249    #[must_use]
250    pub const fn method(&self) -> &'static str {
251        match self.kind {
252            IcSnapshotUploadKind::Metadata => "upload_canister_snapshot_metadata",
253            IcSnapshotUploadKind::Data { .. } => "upload_canister_snapshot_data",
254        }
255    }
256    /// Read exact canonical encoded bytes, without signing or spending.
257    #[must_use]
258    pub fn arguments(&self) -> &[u8] {
259        &self.arguments
260    }
261    /// Hash wire payload only, before plan/authority derivation, using its canonical owner.
262    #[must_use]
263    pub fn digest(&self) -> ArtifactChecksumRecord {
264        management_request_digest(&self.target_bytes, self.method(), &self.arguments)
265    }
266    /// Bind original source evidence/tree and metadata origin separately from wire intent.
267    #[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.append(self.source_plan.digest().hash().as_bytes());
271        bytes.append(self.source.digest().hash().as_bytes());
272        bytes.append(self.source_checksum.hash().as_bytes());
273        if let IcSnapshotUploadKind::Data {
274            metadata_request, ..
275        } = &self.kind
276        {
277            bytes.byte(1);
278            bytes.append(metadata_request.hash().as_bytes());
279        } else {
280            bytes.byte(0);
281        }
282        bytes.append(self.digest().hash().as_bytes());
283        bytes.finish()
284    }
285}
286
287pub(super) fn validate_destination(
288    source: &IcSnapshotMetadataReply<'_>,
289    snapshot_id: &[u8],
290) -> Result<(), IcSnapshotUploadError> {
291    if snapshot_id.is_empty() || snapshot_id.len() > MAX_IC_SNAPSHOT_ID_BYTES {
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/// Typed bounded upload denial; no error changes retained spending or source evidence.
318#[derive(Debug, Error)]
319pub enum IcSnapshotUploadError {
320    /// Exact original source target is outside its retained plan selection.
321    #[error("snapshot upload source target differs from the original selection")]
322    SourceTargetMismatch,
323    /// A missing original global cannot be represented by the upstream upload type.
324    #[error("snapshot upload requires every original global value")]
325    UnavailableGlobal,
326    /// Data upload must retain its exact original metadata-allocation declaration.
327    #[error("snapshot upload requires an original metadata request")]
328    MetadataRequestRequired,
329    /// Destination raw bytes violate the existing 1..=256 ID bounds.
330    #[error("snapshot upload destination ID is invalid")]
331    InvalidDestination,
332    /// The new upload declaration attempts to reuse its retained original source ID.
333    #[error("snapshot upload destination reuses the original source ID")]
334    DestinationReusesSource,
335    /// Actual bounded bytes differ from the requested source range length.
336    #[error("snapshot upload chunk length differs from the source range")]
337    ChunkLengthMismatch,
338    /// Actual chunk exceeds 1 MiB before cloning or encoding.
339    #[error("snapshot upload chunk exceeds bound")]
340    ChunkTooLarge,
341    /// Encoded arguments exceed the separate 2 MiB upload ceiling.
342    #[error("snapshot upload arguments exceed bound")]
343    ArgumentsTooLarge,
344    /// Raw reply exceeds 4 KiB before decoding.
345    #[error("snapshot upload reply exceeds bound")]
346    ReplyTooLarge,
347    /// Finite exact method-specific reply decoding rejected bytes.
348    #[error("snapshot upload reply is invalid")]
349    InvalidReply,
350    /// Existing range/known-hash owner rejected the source kind.
351    #[error(transparent)]
352    Data(#[from] IcSnapshotDataError),
353    /// The exact original chunk digest differs.
354    #[error(transparent)]
355    Checksum(#[from] ChecksumError),
356    /// Canonical principal or upstream Candid encoding failed.
357    #[error(transparent)]
358    Request(#[from] IcRequestError),
359}
360
361#[cfg(test)]
362mod tests;