use super::*;
use crate::model::ic_request::MAX_IC_SNAPSHOT_ID_BYTES;
use crate::tests::support::hex_bytes;
use crate::tests::support::{encode_fixture, encode_fixture_args};
use ic_platform::candid_codec::{Nat, Reserved};
use ic_platform::management_canister_types::{
CanisterTimer, ChunkHash, OnLowWasmMemoryHookStatus, SnapshotMetadataGlobal, SnapshotSource,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct GoldenCase {
name: String,
target: String,
snapshot_id: Vec<u8>,
arguments_hex: String,
request_digest: String,
reply_hex: String,
payload_checksum: String,
digest: String,
globals: usize,
source_present: bool,
}
#[test]
fn admits_every_registered_independent_wire_and_evidence_digest() {
let cases: Vec<GoldenCase> = serde_json::from_str(include_str!("golden.json")).unwrap();
for case in cases {
let request = IcSnapshotMetadataRequest::new(&case.target, &case.snapshot_id).unwrap();
assert_eq!(
request.arguments(),
hex_bytes(&case.arguments_hex),
"{}",
case.name
);
assert_eq!(
request.digest().hash(),
case.request_digest,
"{}",
case.name
);
let raw = hex_bytes(&case.reply_hex);
let reply = IcSnapshotMetadataReply::decode(&request, &raw).unwrap();
let official: ReadCanisterSnapshotMetadataResult =
crate::tests::support::decode_fixture(&raw).unwrap();
assert_eq!(
encode_fixture(reply.metadata()).unwrap(),
encode_fixture(official).unwrap()
);
assert_eq!(reply.payload_checksum().hash(), case.payload_checksum);
assert_eq!(reply.digest().hash(), case.digest);
assert_eq!(reply.metadata().globals.len(), case.globals);
assert_eq!(reply.metadata().source.is_some(), case.source_present);
}
}
fn request() -> IcSnapshotMetadataRequest {
IcSnapshotMetadataRequest::new("renrk-eyaaa-aaaaa-aaada-cai", &[0, 255, 17]).unwrap()
}
fn metadata() -> ReadCanisterSnapshotMetadataResult {
ReadCanisterSnapshotMetadataResult {
source: Some(SnapshotSource::TakenFromCanister(Reserved)),
taken_at_timestamp: u64::MAX,
wasm_module_size: u64::MAX,
globals: vec![
Some(SnapshotMetadataGlobal::I32(i32::MIN)),
Some(SnapshotMetadataGlobal::I64(i64::MIN)),
Some(SnapshotMetadataGlobal::F32(f32::from_bits(0x7fc0_0042))),
Some(SnapshotMetadataGlobal::F64(f64::from_bits(
0x8000_0000_0000_0000,
))),
Some(SnapshotMetadataGlobal::V128(Nat::from(u128::MAX))),
None,
],
wasm_memory_size: 0,
stable_memory_size: u64::MAX,
wasm_chunk_store: vec![
ChunkHash { hash: vec![2; 32] },
ChunkHash { hash: vec![1; 32] },
],
canister_version: u64::MAX,
certified_data: vec![17; 32],
global_timer: Some(CanisterTimer::Active(u64::MAX)),
on_low_wasm_memory_hook_status: Some(OnLowWasmMemoryHookStatus::Executed),
}
}
#[test]
fn exact_arguments_normalization_and_independent_digests() {
let request = request();
let args: ReadCanisterSnapshotMetadataArgs =
crate::tests::support::decode_fixture(request.arguments()).unwrap();
assert_eq!(args.canister_id.to_text(), request.target());
assert_eq!(args.snapshot_id, request.snapshot_id());
assert_eq!(request.receiver(), "aaaaa-aa");
assert_eq!(request.method(), "read_canister_snapshot_metadata");
let principal = args.canister_id.as_slice();
let mut preimage = b"ic-backup/ic-management-request/v1\0".to_vec();
preimage.extend([0, u8::try_from(principal.len()).unwrap()]);
preimage.extend(principal);
preimage.push(1);
preimage.push(31);
preimage.extend(b"read_canister_snapshot_metadata");
preimage.extend(
u32::try_from(request.arguments().len())
.unwrap()
.to_be_bytes(),
);
preimage.extend(request.arguments());
assert_eq!(
request.digest(),
ArtifactChecksumRecord::from_bytes(&preimage)
);
let alias =
IcSnapshotMetadataRequest::new(&request.target().to_uppercase(), request.snapshot_id())
.unwrap();
assert_eq!(alias.arguments(), request.arguments());
assert_eq!(alias.digest(), request.digest());
let different = IcSnapshotMetadataRequest::new(request.target(), &[0, 255, 18]).unwrap();
assert_ne!(different.digest(), request.digest());
let other_target = IcSnapshotMetadataRequest::new("2vxsx-fae", request.snapshot_id()).unwrap();
assert_ne!(other_target.digest(), request.digest());
let raw = encode_fixture(metadata()).unwrap();
let reply = IcSnapshotMetadataReply::decode(&request, &raw).unwrap();
let mut expected = b"ic-backup/ic-snapshot-metadata-reply/v1\0".to_vec();
expected.extend(request.digest().hash().as_bytes());
expected.extend(ArtifactChecksumRecord::from_bytes(&raw).hash().as_bytes());
assert_eq!(
reply.digest(),
ArtifactChecksumRecord::from_bytes(&expected)
);
assert_eq!(
reply.payload_checksum(),
&ArtifactChecksumRecord::from_bytes(&raw)
);
assert_eq!(reply.request().arguments(), request.arguments());
assert_ne!(
reply.digest(),
IcSnapshotMetadataReply::decode(&different, &raw)
.unwrap()
.digest()
);
}
#[test]
fn preserves_order_optional_values_and_float_bits_without_completeness_claims() {
let request = request();
let raw = encode_fixture(metadata()).unwrap();
let reply = IcSnapshotMetadataReply::decode(&request, &raw).unwrap();
let value = reply.metadata();
assert_eq!(value.taken_at_timestamp, u64::MAX);
assert_eq!(value.wasm_module_size, u64::MAX);
assert_eq!(value.wasm_memory_size, 0);
assert_eq!(value.stable_memory_size, u64::MAX);
assert_eq!(value.canister_version, u64::MAX);
assert_eq!(value.certified_data, vec![17; 32]);
assert_eq!(value.wasm_chunk_store[0].hash, vec![2; 32]);
assert_eq!(value.wasm_chunk_store[1].hash, vec![1; 32]);
assert!(matches!(
value.source,
Some(SnapshotSource::TakenFromCanister(_))
));
assert!(matches!(
value.globals[0],
Some(SnapshotMetadataGlobal::I32(i32::MIN))
));
assert!(matches!(
value.globals[1],
Some(SnapshotMetadataGlobal::I64(i64::MIN))
));
assert!(
matches!(value.globals[2], Some(SnapshotMetadataGlobal::F32(f)) if f.to_bits() == 0x7fc0_0042)
);
assert!(
matches!(value.globals[3], Some(SnapshotMetadataGlobal::F64(f)) if f.to_bits() == 0x8000_0000_0000_0000)
);
let maximum = Nat::from(u128::MAX);
assert!(matches!(&value.globals[4], Some(SnapshotMetadataGlobal::V128(n)) if *n == maximum));
assert!(value.globals[5].is_none());
assert!(matches!(
value.global_timer,
Some(CanisterTimer::Active(u64::MAX))
));
assert!(matches!(
value.on_low_wasm_memory_hook_status,
Some(OnLowWasmMemoryHookStatus::Executed)
));
for source in [None, Some(SnapshotSource::MetadataUpload(Reserved))] {
for timer in [None, Some(CanisterTimer::Inactive)] {
for hook in [
None,
Some(OnLowWasmMemoryHookStatus::Ready),
Some(OnLowWasmMemoryHookStatus::ConditionNotSatisfied),
] {
let mut value = metadata();
value.source = source.clone();
value.global_timer = timer.clone();
value.on_low_wasm_memory_hook_status = hook.clone();
value.globals = vec![None];
value.certified_data.clear();
value.wasm_chunk_store.clear();
let reply =
IcSnapshotMetadataReply::decode(&request, &encode_fixture(&value).unwrap())
.unwrap();
assert_eq!(reply.metadata().source, source);
assert_eq!(
encode_fixture(reply.metadata()).unwrap(),
encode_fixture(value).unwrap()
);
}
}
}
let diagnostic = format!("{reply:?}");
assert!(!diagnostic.contains("certified_data"));
assert!(!diagnostic.contains("globals"));
assert!(!diagnostic.contains("snapshot_id: ["));
}
#[test]
fn enforces_request_raw_sequence_hash_and_global_bounds() {
assert_eq!(
IcSnapshotMetadataRequest::new("bad", &[1]).unwrap_err(),
IcRequestError::InvalidTarget
);
for id in [vec![], vec![1; MAX_IC_SNAPSHOT_ID_BYTES + 1]] {
assert_eq!(
IcSnapshotMetadataRequest::new("2vxsx-fae", &id).unwrap_err(),
IcRequestError::InvalidSnapshotId
);
}
assert!(IcSnapshotMetadataRequest::new("2vxsx-fae", &[1; MAX_IC_SNAPSHOT_ID_BYTES]).is_ok());
let request = request();
assert_eq!(
IcSnapshotMetadataReply::decode(&request, &vec![0; MAX_IC_SNAPSHOT_METADATA_BYTES + 1])
.unwrap_err(),
IcSnapshotMetadataError::ReplyTooLarge
);
let check = |value: &ReadCanisterSnapshotMetadataResult| {
IcSnapshotMetadataReply::decode(&request, &encode_fixture(value).unwrap()).map(|_| ())
};
let mut value = metadata();
value.globals = vec![None; MAX_IC_SNAPSHOT_GLOBALS];
assert_eq!(check(&value), Ok(()));
value.globals.push(None);
assert_eq!(check(&value), Err(IcSnapshotMetadataError::InvalidReply));
value = metadata();
value.certified_data.push(0);
assert_eq!(check(&value), Err(IcSnapshotMetadataError::InvalidReply));
value = metadata();
value.wasm_chunk_store[0].hash.pop();
assert_eq!(
check(&value),
Err(IcSnapshotMetadataError::InvalidChunkHash)
);
value.wasm_chunk_store[0].hash = vec![1; 32];
assert_eq!(
check(&value),
Err(IcSnapshotMetadataError::InvalidChunkHash)
);
value.wasm_chunk_store[0].hash = vec![1; 33];
assert_eq!(check(&value), Err(IcSnapshotMetadataError::InvalidReply));
value = metadata();
value.wasm_chunk_store = (0..MAX_IC_SNAPSHOT_CHUNKS)
.map(|i| {
let mut hash = vec![0; 32];
hash[..8].copy_from_slice(&u64::try_from(i).unwrap().to_be_bytes());
ChunkHash { hash }
})
.collect();
assert_eq!(check(&value), Ok(()));
value.wasm_chunk_store.push(ChunkHash { hash: vec![3; 32] });
assert_eq!(check(&value), Err(IcSnapshotMetadataError::InvalidReply));
value = metadata();
value.globals = vec![Some(SnapshotMetadataGlobal::V128(Nat(Nat::from(
u128::MAX,
)
.0 + 1_u8)))];
assert_eq!(check(&value), Err(IcSnapshotMetadataError::InvalidGlobal));
}
#[test]
fn rejects_malformed_truncated_extra_and_skipped_wire_without_payload_diagnostics() {
struct Extra {
value: ReadCanisterSnapshotMetadataResult,
extra: u64,
}
ic_platform::candid_record!(Extra {
834_174_833 => value: ReadCanisterSnapshotMetadataResult,
1_998_756_496 => extra: u64,
});
let request = request();
let raw = encode_fixture(metadata()).unwrap();
for length in 0..raw.len() {
assert_eq!(
IcSnapshotMetadataReply::decode(&request, &raw[..length]).unwrap_err(),
IcSnapshotMetadataError::InvalidReply
);
}
for invalid in [
b"bad wire".to_vec(),
encode_fixture_args(&()).unwrap(),
encode_fixture_args(&(metadata(), 1_u64)).unwrap(),
[raw.as_slice(), &[0]].concat(),
encode_fixture(123_u64).unwrap(),
] {
let error = IcSnapshotMetadataReply::decode(&request, &invalid).unwrap_err();
assert_eq!(error, IcSnapshotMetadataError::InvalidReply);
assert_eq!(
error.to_string(),
"invalid or unsupported snapshot metadata reply"
);
}
let bytes = encode_fixture(Extra {
value: metadata(),
extra: 0,
})
.unwrap();
assert_eq!(
IcSnapshotMetadataReply::decode(&request, &bytes).unwrap_err(),
IcSnapshotMetadataError::InvalidReply
);
}
struct MetadataShape<S, G, T> {
source: S,
taken_at_timestamp: u64,
wasm_module_size: u64,
globals: Vec<G>,
wasm_memory_size: u64,
stable_memory_size: u64,
wasm_chunk_store: Vec<ChunkHash>,
canister_version: u64,
certified_data: Vec<u8>,
global_timer: T,
on_low_wasm_memory_hook_status: Option<OnLowWasmMemoryHookStatus>,
}
impl<
S: ic_platform::candid_codec::Encode,
G: ic_platform::candid_codec::Encode,
T: ic_platform::candid_codec::Encode,
> ic_platform::candid_codec::Encode for MetadataShape<S, G, T>
{
const TYPE: &'static ic_platform::candid_codec::Type =
&ic_platform::candid_codec::Type::Record(&[
(
339_281_328,
<Vec<G> as ic_platform::candid_codec::Encode>::TYPE,
),
(
783_802_816,
<u64 as ic_platform::candid_codec::Encode>::TYPE,
),
(842_117_339, <S as ic_platform::candid_codec::Encode>::TYPE),
(
864_574_658,
<Vec<u8> as ic_platform::candid_codec::Encode>::TYPE,
),
(903_275_689, <T as ic_platform::candid_codec::Encode>::TYPE),
(
1_282_538_240,
<Option<OnLowWasmMemoryHookStatus> as ic_platform::candid_codec::Encode>::TYPE,
),
(
1_299_009_433,
<u64 as ic_platform::candid_codec::Encode>::TYPE,
),
(
1_710_030_651,
<u64 as ic_platform::candid_codec::Encode>::TYPE,
),
(
1_767_008_372,
<Vec<ChunkHash> as ic_platform::candid_codec::Encode>::TYPE,
),
(
3_569_147_778,
<u64 as ic_platform::candid_codec::Encode>::TYPE,
),
(
3_598_099_748,
<u64 as ic_platform::candid_codec::Encode>::TYPE,
),
]);
fn write(
&self,
out: &mut ic_platform::candid_codec::Writer,
) -> Result<(), ic_platform::candid_codec::Error> {
ic_platform::candid_codec::Encode::write(&self.globals, out)?;
ic_platform::candid_codec::Encode::write(&self.canister_version, out)?;
ic_platform::candid_codec::Encode::write(&self.source, out)?;
ic_platform::candid_codec::Encode::write(&self.certified_data, out)?;
ic_platform::candid_codec::Encode::write(&self.global_timer, out)?;
ic_platform::candid_codec::Encode::write(&self.on_low_wasm_memory_hook_status, out)?;
ic_platform::candid_codec::Encode::write(&self.wasm_module_size, out)?;
ic_platform::candid_codec::Encode::write(&self.stable_memory_size, out)?;
ic_platform::candid_codec::Encode::write(&self.wasm_chunk_store, out)?;
ic_platform::candid_codec::Encode::write(&self.taken_at_timestamp, out)?;
ic_platform::candid_codec::Encode::write(&self.wasm_memory_size, out)?;
Ok(())
}
}
fn shape<S, G, T>(source: S, globals: Vec<G>, timer: T) -> MetadataShape<S, G, T> {
MetadataShape {
source,
globals,
global_timer: timer,
taken_at_timestamp: 0,
wasm_module_size: 0,
wasm_memory_size: 0,
stable_memory_size: 0,
wasm_chunk_store: vec![],
canister_version: 0,
certified_data: vec![],
on_low_wasm_memory_hook_status: None,
}
}
#[test]
fn rejects_unknown_optional_variants_reserved_payloads_and_type_header_work() {
enum Unknown {
Future(u64),
}
ic_platform::candid_variant!(Unknown {
2_816_674_915 => Future(u64),
});
#[derive(Deserialize)]
enum BadSource {
#[serde(rename = "taken_from_canister")]
Taken(u64),
}
ic_platform::candid_variant!(BadSource {
2_359_638_948 => Taken(u64),
});
let request = request();
let invalid = [
encode_fixture(shape(
Some(Unknown::Future(0)),
vec![None::<SnapshotMetadataGlobal>],
None::<CanisterTimer>,
))
.unwrap(),
encode_fixture(shape(
None::<SnapshotSource>,
vec![Some(Unknown::Future(0))],
None::<CanisterTimer>,
))
.unwrap(),
encode_fixture(shape(
None::<SnapshotSource>,
vec![None::<SnapshotMetadataGlobal>],
Some(Unknown::Future(0)),
))
.unwrap(),
encode_fixture(shape(
Some(BadSource::Taken(0)),
vec![None::<SnapshotMetadataGlobal>],
None::<CanisterTimer>,
))
.unwrap(),
[
b"DIDL".as_slice(),
&[65],
&[0x6e, 0x7f].repeat(65),
&[1, 0, 0],
]
.concat(),
[b"DIDL".as_slice(), &[1, 0x6c, 0xff, 0xff, 0xff, 0xff, 0x0f]].concat(),
];
for raw in invalid {
assert_eq!(
IcSnapshotMetadataReply::decode(&request, &raw).unwrap_err(),
IcSnapshotMetadataError::InvalidReply
);
}
let raw = encode_fixture(shape(
None::<SnapshotSource>,
vec![None::<SnapshotMetadataGlobal>],
None::<CanisterTimer>,
))
.unwrap();
let reply = IcSnapshotMetadataReply::decode(&request, &raw).unwrap();
assert!(reply.metadata().source.is_none());
assert!(reply.metadata().globals[0].is_none());
}