mod attempt;
mod reply;
pub(crate) use attempt::original_authority;
pub use attempt::{IcSnapshotUploadAttempt, IcSnapshotUploadAttemptError};
pub use reply::{IcSnapshotUploadReply, IcSnapshotUploadReplyKind};
use super::{
artifacts::{ArtifactChecksumRecord, ChecksumError},
ic_request::{IcRequestError, MAX_IC_SNAPSHOT_ID_BYTES, management_request_digest},
ic_snapshot_data::{IcSnapshotDataError, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, validate_kind},
ic_snapshot_metadata::IcSnapshotMetadataReply,
operation_plan::OperationPlanRecord,
};
use candid::Principal;
use ic_management_canister_types::{
SnapshotDataKind, SnapshotDataOffset, UploadCanisterSnapshotDataArgs,
UploadCanisterSnapshotMetadataArgs,
};
use std::fmt;
use thiserror::Error;
pub const MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES: usize = 2 * 1024 * 1024;
pub const MAX_IC_SNAPSHOT_UPLOAD_REPLY_BYTES: usize = 4096;
#[derive(Debug)]
pub enum IcSnapshotUploadKind {
Metadata,
Data {
snapshot_id: Vec<u8>,
source_kind: SnapshotDataKind,
chunk_checksum: ArtifactChecksumRecord,
metadata_request: ArtifactChecksumRecord,
},
}
pub struct IcSnapshotUploadRequest<'source> {
source_plan: &'source OperationPlanRecord,
source: &'source IcSnapshotMetadataReply<'source>,
source_checksum: ArtifactChecksumRecord,
kind: IcSnapshotUploadKind,
arguments: Vec<u8>,
target_bytes: Vec<u8>,
}
impl<'source> IcSnapshotUploadRequest<'source> {
pub fn metadata(
source_plan: &'source OperationPlanRecord,
source: &'source IcSnapshotMetadataReply<'source>,
source_checksum: &ArtifactChecksumRecord,
) -> Result<Self, IcSnapshotUploadError> {
if !source_plan
.selected_targets()
.iter()
.any(|target| target == source.request().target())
{
return Err(IcSnapshotUploadError::SourceTargetMismatch);
}
let values = source.metadata();
let globals = values
.globals
.iter()
.cloned()
.collect::<Option<Vec<_>>>()
.ok_or(IcSnapshotUploadError::UnavailableGlobal)?;
let target = Principal::from_text(source.request().target())
.map_err(|_| IcRequestError::InvalidTarget)?;
let arguments = candid::encode_one(UploadCanisterSnapshotMetadataArgs {
canister_id: target,
replace_snapshot: None,
wasm_module_size: values.wasm_module_size,
globals,
wasm_memory_size: values.wasm_memory_size,
stable_memory_size: values.stable_memory_size,
certified_data: values.certified_data.clone(),
global_timer: values.global_timer.clone(),
on_low_wasm_memory_hook_status: values.on_low_wasm_memory_hook_status.clone(),
})
.map_err(|error| IcRequestError::Encoding(error.to_string()))?;
Self::from_arguments(
source_plan,
source,
source_checksum,
IcSnapshotUploadKind::Metadata,
arguments,
)
}
pub fn data(
metadata: &Self,
snapshot_id: &[u8],
source_kind: SnapshotDataKind,
chunk: &[u8],
) -> Result<Self, IcSnapshotUploadError> {
metadata.validate_data_destination(snapshot_id)?;
validate_kind(metadata.source, &source_kind)?;
if chunk.len() > MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES {
return Err(IcSnapshotUploadError::ChunkTooLarge);
}
let checksum = ArtifactChecksumRecord::from_bytes(chunk);
let kind = match &source_kind {
SnapshotDataKind::WasmModule { offset, size } => {
require_length(chunk, *size)?;
SnapshotDataOffset::WasmModule { offset: *offset }
}
SnapshotDataKind::WasmMemory { offset, size } => {
require_length(chunk, *size)?;
SnapshotDataOffset::WasmMemory { offset: *offset }
}
SnapshotDataKind::StableMemory { offset, size } => {
require_length(chunk, *size)?;
SnapshotDataOffset::StableMemory { offset: *offset }
}
SnapshotDataKind::WasmChunk { hash } => {
checksum.verify(&crate::hash::hex_bytes(hash))?;
SnapshotDataOffset::WasmChunk
}
};
let target =
Principal::from_text(metadata.target()).map_err(|_| IcRequestError::InvalidTarget)?;
let arguments = candid::encode_one(UploadCanisterSnapshotDataArgs {
canister_id: target,
snapshot_id: snapshot_id.to_vec(),
kind,
chunk: chunk.to_vec(),
})
.map_err(|error| IcRequestError::Encoding(error.to_string()))?;
Self::from_arguments(
metadata.source_plan,
metadata.source,
&metadata.source_checksum,
IcSnapshotUploadKind::Data {
snapshot_id: snapshot_id.to_vec(),
source_kind,
chunk_checksum: checksum,
metadata_request: metadata.digest(),
},
arguments,
)
}
fn from_arguments(
source_plan: &'source OperationPlanRecord,
source: &'source IcSnapshotMetadataReply<'source>,
source_checksum: &ArtifactChecksumRecord,
kind: IcSnapshotUploadKind,
arguments: Vec<u8>,
) -> Result<Self, IcSnapshotUploadError> {
if arguments.len() > MAX_IC_SNAPSHOT_UPLOAD_ARGUMENT_BYTES {
return Err(IcSnapshotUploadError::ArgumentsTooLarge);
}
let target = Principal::from_text(source.request().target())
.map_err(|_| IcRequestError::InvalidTarget)?;
Ok(Self {
source_plan,
source,
source_checksum: source_checksum.clone(),
kind,
arguments,
target_bytes: target.as_slice().to_vec(),
})
}
pub(crate) fn validate_data_destination(
&self,
snapshot_id: &[u8],
) -> Result<(), IcSnapshotUploadError> {
if !matches!(self.kind, IcSnapshotUploadKind::Metadata) {
return Err(IcSnapshotUploadError::MetadataRequestRequired);
}
validate_destination(self.source, snapshot_id)
}
#[must_use]
pub const fn source_plan(&self) -> &'source OperationPlanRecord {
self.source_plan
}
#[must_use]
pub const fn source(&self) -> &'source IcSnapshotMetadataReply<'source> {
self.source
}
#[must_use]
pub const fn source_checksum(&self) -> &ArtifactChecksumRecord {
&self.source_checksum
}
#[must_use]
pub const fn kind(&self) -> &IcSnapshotUploadKind {
&self.kind
}
#[must_use]
pub fn target(&self) -> &str {
self.source.request().target()
}
#[must_use]
pub const fn receiver(&self) -> &'static str {
"aaaaa-aa"
}
#[must_use]
pub const fn method(&self) -> &'static str {
match self.kind {
IcSnapshotUploadKind::Metadata => "upload_canister_snapshot_metadata",
IcSnapshotUploadKind::Data { .. } => "upload_canister_snapshot_data",
}
}
#[must_use]
pub fn arguments(&self) -> &[u8] {
&self.arguments
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
management_request_digest(&self.target_bytes, self.method(), &self.arguments)
}
#[must_use]
pub fn binding_digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/ic-snapshot-upload-binding/v1\0".to_vec();
bytes.extend_from_slice(self.source_plan.digest().hash().as_bytes());
bytes.extend_from_slice(self.source.digest().hash().as_bytes());
bytes.extend_from_slice(self.source_checksum.hash().as_bytes());
if let IcSnapshotUploadKind::Data {
metadata_request, ..
} = &self.kind
{
bytes.push(1);
bytes.extend_from_slice(metadata_request.hash().as_bytes());
} else {
bytes.push(0);
}
bytes.extend_from_slice(self.digest().hash().as_bytes());
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
pub(super) fn validate_destination(
source: &IcSnapshotMetadataReply<'_>,
snapshot_id: &[u8],
) -> Result<(), IcSnapshotUploadError> {
if snapshot_id.is_empty() || snapshot_id.len() > MAX_IC_SNAPSHOT_ID_BYTES {
return Err(IcSnapshotUploadError::InvalidDestination);
}
if snapshot_id == source.request().snapshot_id() {
return Err(IcSnapshotUploadError::DestinationReusesSource);
}
Ok(())
}
fn require_length(chunk: &[u8], length: u64) -> Result<(), IcSnapshotUploadError> {
if u64::try_from(chunk.len()).ok() != Some(length) {
return Err(IcSnapshotUploadError::ChunkLengthMismatch);
}
Ok(())
}
impl fmt::Debug for IcSnapshotUploadRequest<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("IcSnapshotUploadRequest")
.field("target", &self.target())
.field("method", &self.method())
.field("argument_bytes", &self.arguments.len())
.finish_non_exhaustive()
}
}
#[derive(Debug, Error)]
pub enum IcSnapshotUploadError {
#[error("snapshot upload source target differs from the original selection")]
SourceTargetMismatch,
#[error("snapshot upload requires every original global value")]
UnavailableGlobal,
#[error("snapshot upload requires an original metadata request")]
MetadataRequestRequired,
#[error("snapshot upload destination ID is invalid")]
InvalidDestination,
#[error("snapshot upload destination reuses the original source ID")]
DestinationReusesSource,
#[error("snapshot upload chunk length differs from the source range")]
ChunkLengthMismatch,
#[error("snapshot upload chunk exceeds bound")]
ChunkTooLarge,
#[error("snapshot upload arguments exceed bound")]
ArgumentsTooLarge,
#[error("snapshot upload reply exceeds bound")]
ReplyTooLarge,
#[error("snapshot upload reply is invalid")]
InvalidReply,
#[error(transparent)]
Data(#[from] IcSnapshotDataError),
#[error(transparent)]
Checksum(#[from] ChecksumError),
#[error(transparent)]
Request(#[from] IcRequestError),
}
#[cfg(test)]
mod tests;