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
3mod attempt;
4mod reply;
5pub(crate) use attempt::original_authority;
6pub use attempt::{IcSnapshotUploadAttempt, IcSnapshotUploadAttemptError};
7pub use reply::{IcSnapshotUploadReply, IcSnapshotUploadReplyKind};
8
9use super::{
10    artifacts::{ArtifactChecksumRecord, ChecksumError},
11    ic_request::{IcRequestError, MAX_IC_SNAPSHOT_ID_BYTES, management_request_digest},
12    ic_snapshot_data::{IcSnapshotDataError, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, validate_kind},
13    ic_snapshot_metadata::IcSnapshotMetadataReply,
14    operation_plan::OperationPlanRecord,
15};
16use candid::Principal;
17use ic_management_canister_types::{
18    SnapshotDataKind, SnapshotDataOffset, UploadCanisterSnapshotDataArgs,
19    UploadCanisterSnapshotMetadataArgs,
20};
21use std::fmt;
22use thiserror::Error;
23
24/// Maximum upload arguments, including finite Candid overhead around a 1 MiB chunk.
25pub const MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES: usize = 2 * 1024 * 1024;
26/// Maximum raw upload reply bytes before finite decoding.
27pub const MAX_IC_SNAPSHOT_UPLOAD_REPLY_BYTES: usize = 4096;
28
29/// Closed upload declaration; each branch retains exact original source association.
30#[derive(Debug)]
31pub enum IcSnapshotUploadKind {
32    /// Allocate a new snapshot with exact representable metadata, without replacement.
33    Metadata,
34    /// Write exact bounded bytes into an independently qualified new upload snapshot.
35    Data {
36        /// Exact destination raw ID, separate from the retained source raw ID/token.
37        snapshot_id: Vec<u8>,
38        /// Original source extent or known hash; chunk hashes are not part of upload wire.
39        source_kind: SnapshotDataKind,
40        /// Hash of the actual bounded chunk bytes encoded in the arguments.
41        chunk_checksum: ArtifactChecksumRecord,
42        /// Exact original metadata-allocation wire request digest.
43        metadata_request: ArtifactChecksumRecord,
44    },
45}
46
47/// Ephemeral exact wire payload with retained source evidence; no dispatch permission.
48///
49/// Pure construction admits declarations only. Guarded persistence operations supply
50/// fresh local byte binding. Integrations qualify authentic source/complete transfer,
51/// new destination attribution, original spending, current controller/custody and
52/// same-release application safety. No serialization, replacement, relocation,
53/// allowance or automatic outcome is introduced.
54pub struct IcSnapshotUploadRequest<'source> {
55    source_plan: &'source OperationPlanRecord,
56    source: &'source IcSnapshotMetadataReply<'source>,
57    source_checksum: ArtifactChecksumRecord,
58    kind: IcSnapshotUploadKind,
59    arguments: Vec<u8>,
60    target_bytes: Vec<u8>,
61}
62
63impl<'source> IcSnapshotUploadRequest<'source> {
64    /// Encode exact uploadable original metadata for the same declared canister.
65    ///
66    /// Globals retain order and bits. Unavailable slots reject; absent timer/hook
67    /// values stay absent and do not default to inactive/ready states. Replacement
68    /// is always absent; this payload has no source-snapshot deletion lane.
69    /// Build these nonrecursive bytes before retaining their original operation plan.
70    /// # Errors
71    /// Rejects unavailable globals, encoding failure and excessive arguments.
72    pub fn metadata(
73        source_plan: &'source OperationPlanRecord,
74        source: &'source IcSnapshotMetadataReply<'source>,
75        source_checksum: &ArtifactChecksumRecord,
76    ) -> Result<Self, IcSnapshotUploadError> {
77        if !source_plan
78            .selected_targets()
79            .iter()
80            .any(|target| target == source.request().target())
81        {
82            return Err(IcSnapshotUploadError::SourceTargetMismatch);
83        }
84        let values = source.metadata();
85        let globals = values
86            .globals
87            .iter()
88            .cloned()
89            .collect::<Option<Vec<_>>>()
90            .ok_or(IcSnapshotUploadError::UnavailableGlobal)?;
91        let target = Principal::from_text(source.request().target())
92            .map_err(|_| IcRequestError::InvalidTarget)?;
93        let arguments = candid::encode_one(UploadCanisterSnapshotMetadataArgs {
94            canister_id: target,
95            replace_snapshot: None,
96            wasm_module_size: values.wasm_module_size,
97            globals,
98            wasm_memory_size: values.wasm_memory_size,
99            stable_memory_size: values.stable_memory_size,
100            certified_data: values.certified_data.clone(),
101            global_timer: values.global_timer.clone(),
102            on_low_wasm_memory_hook_status: values.on_low_wasm_memory_hook_status.clone(),
103        })
104        .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
105        Self::from_arguments(
106            source_plan,
107            source,
108            source_checksum,
109            IcSnapshotUploadKind::Metadata,
110            arguments,
111        )
112    }
113
114    /// Encode one exact source extent/chunk for a separately qualified new raw ID.
115    ///
116    /// Requires the original metadata upload declaration. Regions are nonempty and
117    /// bounded to 1 MiB; an empty known chunk requires its exact SHA-256. Actual
118    /// new snapshot attribution is not inferred from a supplied ID or decoded reply.
119    /// Data requests include their new ID before their own plan/reservation is retained;
120    /// metadata authority never supplies data-call spending or resets prior journals.
121    /// # Errors
122    /// Rejects a non-metadata original, invalid/reused ID, source range/hash or byte
123    /// mismatch, excessive data/arguments and encoding failure.
124    pub fn data(
125        metadata: &Self,
126        snapshot_id: &[u8],
127        source_kind: SnapshotDataKind,
128        chunk: &[u8],
129    ) -> Result<Self, IcSnapshotUploadError> {
130        metadata.validate_data_destination(snapshot_id)?;
131        validate_kind(metadata.source, &source_kind)?;
132        if chunk.len() > MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES {
133            return Err(IcSnapshotUploadError::ChunkTooLarge);
134        }
135        let checksum = ArtifactChecksumRecord::from_bytes(chunk);
136        let kind = match &source_kind {
137            SnapshotDataKind::WasmModule { offset, size } => {
138                require_length(chunk, *size)?;
139                SnapshotDataOffset::WasmModule { offset: *offset }
140            }
141            SnapshotDataKind::WasmMemory { offset, size } => {
142                require_length(chunk, *size)?;
143                SnapshotDataOffset::WasmMemory { offset: *offset }
144            }
145            SnapshotDataKind::StableMemory { offset, size } => {
146                require_length(chunk, *size)?;
147                SnapshotDataOffset::StableMemory { offset: *offset }
148            }
149            SnapshotDataKind::WasmChunk { hash } => {
150                checksum.verify(&crate::hash::hex_bytes(hash))?;
151                SnapshotDataOffset::WasmChunk
152            }
153        };
154        let target =
155            Principal::from_text(metadata.target()).map_err(|_| IcRequestError::InvalidTarget)?;
156        let arguments = candid::encode_one(UploadCanisterSnapshotDataArgs {
157            canister_id: target,
158            snapshot_id: snapshot_id.to_vec(),
159            kind,
160            chunk: chunk.to_vec(),
161        })
162        .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
163        Self::from_arguments(
164            metadata.source_plan,
165            metadata.source,
166            &metadata.source_checksum,
167            IcSnapshotUploadKind::Data {
168                snapshot_id: snapshot_id.to_vec(),
169                source_kind,
170                chunk_checksum: checksum,
171                metadata_request: metadata.digest(),
172            },
173            arguments,
174        )
175    }
176
177    fn from_arguments(
178        source_plan: &'source OperationPlanRecord,
179        source: &'source IcSnapshotMetadataReply<'source>,
180        source_checksum: &ArtifactChecksumRecord,
181        kind: IcSnapshotUploadKind,
182        arguments: Vec<u8>,
183    ) -> Result<Self, IcSnapshotUploadError> {
184        if arguments.len() > MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES {
185            return Err(IcSnapshotUploadError::ArgumentsTooLarge);
186        }
187        let target = Principal::from_text(source.request().target())
188            .map_err(|_| IcRequestError::InvalidTarget)?;
189        Ok(Self {
190            source_plan,
191            source,
192            source_checksum: source_checksum.clone(),
193            kind,
194            arguments,
195            target_bytes: target.as_slice().to_vec(),
196        })
197    }
198
199    pub(crate) fn validate_data_destination(
200        &self,
201        snapshot_id: &[u8],
202    ) -> Result<(), IcSnapshotUploadError> {
203        if !matches!(self.kind, IcSnapshotUploadKind::Metadata) {
204            return Err(IcSnapshotUploadError::MetadataRequestRequired);
205        }
206        validate_destination(self.source, snapshot_id)
207    }
208
209    /// Read the exact original source plan; later upload intent must retain its context.
210    #[must_use]
211    pub const fn source_plan(&self) -> &'source OperationPlanRecord {
212        self.source_plan
213    }
214
215    /// Read original metadata/request evidence; it authenticates no source.
216    #[must_use]
217    pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
218        self.source
219    }
220    /// Read declared retained tree checksum; pure construction verifies no files.
221    #[must_use]
222    pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
223        &self.source_checksum
224    }
225    /// Read the closed kind and exact source/destination association.
226    #[must_use]
227    pub const fn kind(&self) -> &IcSnapshotUploadKind {
228        &self.kind
229    }
230    /// Read the same canonical source/destination canister target.
231    #[must_use]
232    pub fn target(&self) -> &str {
233        self.source.request().target()
234    }
235    /// Read fixed management receiver, separate from effective routing.
236    #[must_use]
237    pub const fn receiver(&self) -> &'static str {
238        "aaaaa-aa"
239    }
240    /// Read the exact replicated host-update method; no query mode exists.
241    #[must_use]
242    pub const fn method(&self) -> &'static str {
243        match self.kind {
244            IcSnapshotUploadKind::Metadata => "upload_canister_snapshot_metadata",
245            IcSnapshotUploadKind::Data { .. } => "upload_canister_snapshot_data",
246        }
247    }
248    /// Read exact canonical encoded bytes, without signing or spending.
249    #[must_use]
250    pub fn arguments(&self) -> &[u8] {
251        &self.arguments
252    }
253    /// Hash wire payload only, before plan/authority derivation, using its canonical owner.
254    #[must_use]
255    pub fn digest(&self) -> ArtifactChecksumRecord {
256        management_request_digest(&self.target_bytes, self.method(), &self.arguments)
257    }
258    /// Bind original source evidence/tree and metadata origin separately from wire intent.
259    #[must_use]
260    pub fn binding_digest(&self) -> ArtifactChecksumRecord {
261        let mut bytes = b"ic-backup/ic-snapshot-upload-binding/v1\0".to_vec();
262        bytes.extend_from_slice(self.source_plan.digest().hash().as_bytes());
263        bytes.extend_from_slice(self.source.digest().hash().as_bytes());
264        bytes.extend_from_slice(self.source_checksum.hash().as_bytes());
265        if let IcSnapshotUploadKind::Data {
266            metadata_request, ..
267        } = &self.kind
268        {
269            bytes.push(1);
270            bytes.extend_from_slice(metadata_request.hash().as_bytes());
271        } else {
272            bytes.push(0);
273        }
274        bytes.extend_from_slice(self.digest().hash().as_bytes());
275        ArtifactChecksumRecord::from_bytes(&bytes)
276    }
277}
278
279pub(super) fn validate_destination(
280    source: &IcSnapshotMetadataReply<'_>,
281    snapshot_id: &[u8],
282) -> Result<(), IcSnapshotUploadError> {
283    if snapshot_id.is_empty() || snapshot_id.len() > MAX_IC_SNAPSHOT_ID_BYTES {
284        return Err(IcSnapshotUploadError::InvalidDestination);
285    }
286    if snapshot_id == source.request().snapshot_id() {
287        return Err(IcSnapshotUploadError::DestinationReusesSource);
288    }
289    Ok(())
290}
291
292fn require_length(chunk: &[u8], length: u64) -> Result<(), IcSnapshotUploadError> {
293    if u64::try_from(chunk.len()).ok() != Some(length) {
294        return Err(IcSnapshotUploadError::ChunkLengthMismatch);
295    }
296    Ok(())
297}
298
299impl fmt::Debug for IcSnapshotUploadRequest<'_> {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        f.debug_struct("IcSnapshotUploadRequest")
302            .field("target", &self.target())
303            .field("method", &self.method())
304            .field("argument_bytes", &self.arguments.len())
305            .finish_non_exhaustive()
306    }
307}
308
309/// Typed bounded upload denial; no error changes retained spending or source evidence.
310#[derive(Debug, Error)]
311pub enum IcSnapshotUploadError {
312    /// Exact original source target is outside its retained plan selection.
313    #[error("snapshot upload source target differs from the original selection")]
314    SourceTargetMismatch,
315    /// A missing original global cannot be represented by the upstream upload type.
316    #[error("snapshot upload requires every original global value")]
317    UnavailableGlobal,
318    /// Data upload must retain its exact original metadata-allocation declaration.
319    #[error("snapshot upload requires an original metadata request")]
320    MetadataRequestRequired,
321    /// Destination raw bytes violate the existing 1..=256 ID bounds.
322    #[error("snapshot upload destination ID is invalid")]
323    InvalidDestination,
324    /// The new upload declaration attempts to reuse its retained original source ID.
325    #[error("snapshot upload destination reuses the original source ID")]
326    DestinationReusesSource,
327    /// Actual bounded bytes differ from the requested source range length.
328    #[error("snapshot upload chunk length differs from the source range")]
329    ChunkLengthMismatch,
330    /// Actual chunk exceeds 1 MiB before cloning or encoding.
331    #[error("snapshot upload chunk exceeds bound")]
332    ChunkTooLarge,
333    /// Encoded arguments exceed the separate 2 MiB upload ceiling.
334    #[error("snapshot upload arguments exceed bound")]
335    ArgumentsTooLarge,
336    /// Raw reply exceeds 4 KiB before decoding.
337    #[error("snapshot upload reply exceeds bound")]
338    ReplyTooLarge,
339    /// Finite exact method-specific reply decoding rejected bytes.
340    #[error("snapshot upload reply is invalid")]
341    InvalidReply,
342    /// Existing range/known-hash owner rejected the source kind.
343    #[error(transparent)]
344    Data(#[from] IcSnapshotDataError),
345    /// The exact original chunk digest differs.
346    #[error(transparent)]
347    Checksum(#[from] ChecksumError),
348    /// Canonical principal or upstream Candid encoding failed.
349    #[error(transparent)]
350    Request(#[from] IcRequestError),
351}
352
353#[cfg(test)]
354mod tests;