Skip to content

Commit 7f009d8

Browse files
feat: [EXC-1955] Read canister snapshot metadata (#4514)
This PR implements the download of snapshot metadata. The feature is currently behind a feature flag in the execution config. When disabled, the mgmt canister endpoint returns an empty blob. --------- Co-authored-by: Stefan Schneider <31004026+schneiderstefan@users.noreply.github.com>
1 parent e325bb0 commit 7f009d8

File tree

6 files changed

+289
-22
lines changed

6 files changed

+289
-22
lines changed

rs/execution_environment/src/canister_manager.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ use ic_interfaces::execution_environment::{IngressHistoryWriter, SubnetAvailable
2020
use ic_logger::{error, fatal, info, ReplicaLogger};
2121
use ic_management_canister_types_private::{
2222
CanisterChangeDetails, CanisterChangeOrigin, CanisterInstallModeV2, CanisterSnapshotResponse,
23-
CanisterStatusResultV2, CanisterStatusType, ChunkHash, Method as Ic00Method, StoredChunksReply,
24-
UploadChunkReply,
23+
CanisterStatusResultV2, CanisterStatusType, ChunkHash, Method as Ic00Method,
24+
ReadCanisterSnapshotMetadataResponse, StoredChunksReply, UploadChunkReply,
2525
};
2626
use ic_registry_provisional_whitelist::ProvisionalWhitelist;
27+
use ic_replicated_state::canister_state::WASM_PAGE_SIZE_IN_BYTES;
2728
use ic_replicated_state::{
2829
canister_snapshots::CanisterSnapshot,
2930
canister_state::{
@@ -1981,6 +1982,54 @@ impl CanisterManager {
19811982
WasmExecutionMode::Wasm64 => self.config.max_canister_memory_size_wasm64,
19821983
}
19831984
}
1985+
1986+
pub(crate) fn read_snapshot_metadata(
1987+
&self,
1988+
sender: PrincipalId,
1989+
snapshot_id: SnapshotId,
1990+
canister: &CanisterState,
1991+
state: &ReplicatedState,
1992+
) -> Result<ReadCanisterSnapshotMetadataResponse, CanisterManagerError> {
1993+
// Check sender is a controller.
1994+
validate_controller(canister, &sender)?;
1995+
// If not found, the operation fails due to invalid parameters.
1996+
let Some(snapshot) = state.canister_snapshots.get(snapshot_id) else {
1997+
return Err(CanisterManagerError::CanisterSnapshotNotFound {
1998+
canister_id: canister.canister_id(),
1999+
snapshot_id,
2000+
});
2001+
};
2002+
// Verify the provided `snapshot_id` belongs to this canister.
2003+
if snapshot.canister_id() != canister.canister_id() {
2004+
return Err(CanisterManagerError::CanisterSnapshotInvalidOwnership {
2005+
canister_id: canister.canister_id(),
2006+
snapshot_id,
2007+
});
2008+
}
2009+
2010+
Ok(ReadCanisterSnapshotMetadataResponse {
2011+
source: snapshot.source(),
2012+
taken_at_timestamp: snapshot.taken_at_timestamp().as_nanos_since_unix_epoch(),
2013+
wasm_module_size: snapshot.execution_snapshot().wasm_binary.len() as u64,
2014+
exported_globals: snapshot.exported_globals().clone(),
2015+
wasm_memory_size: snapshot.execution_snapshot().wasm_memory.size.get() as u64
2016+
* WASM_PAGE_SIZE_IN_BYTES as u64,
2017+
stable_memory_size: snapshot.execution_snapshot().stable_memory.size.get() as u64
2018+
* WASM_PAGE_SIZE_IN_BYTES as u64,
2019+
wasm_chunk_store: snapshot
2020+
.chunk_store()
2021+
.keys()
2022+
.cloned()
2023+
.map(|x| ChunkHash { hash: x.to_vec() })
2024+
.collect(),
2025+
canister_version: snapshot.canister_version(),
2026+
certified_data: snapshot.certified_data().clone(),
2027+
global_timer: snapshot.execution_snapshot().global_timer.into(),
2028+
on_low_wasm_memory_hook_status: snapshot
2029+
.execution_snapshot()
2030+
.on_low_wasm_memory_hook_status,
2031+
})
2032+
}
19842033
}
19852034

19862035
/// Uninstalls a canister.

rs/execution_environment/src/execution_environment.rs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,10 +1657,16 @@ impl ExecutionEnvironment {
16571657
}
16581658

16591659
Ok(Ic00Method::ReadCanisterSnapshotMetadata) => {
1660-
// TODO: EXC-1955
1661-
#[allow(clippy::bind_instead_of_map)]
1662-
let res = ReadCanisterSnapshotMetadataArgs::decode(payload)
1663-
.and_then(|_args| Ok((vec![], None)));
1660+
let res = ReadCanisterSnapshotMetadataArgs::decode(payload).and_then(|args| {
1661+
match self.config.canister_snapshot_download {
1662+
FlagStatus::Disabled => Ok((vec![], None)),
1663+
FlagStatus::Enabled => {
1664+
let canister_id = args.get_canister_id();
1665+
self.read_canister_snapshot_metadata(*msg.sender(), &state, args)
1666+
.map(|x| (x, Some(canister_id)))
1667+
}
1668+
}
1669+
});
16641670
ExecuteSubnetMessageResult::Finished {
16651671
response: res,
16661672
refund: msg.take_cycles(),
@@ -1670,8 +1676,13 @@ impl ExecutionEnvironment {
16701676
Ok(Ic00Method::ReadCanisterSnapshotData) => {
16711677
// TODO: EXC-1957
16721678
#[allow(clippy::bind_instead_of_map)]
1673-
let res = ReadCanisterSnapshotDataArgs::decode(payload)
1674-
.and_then(|_args| Ok((vec![], None)));
1679+
let res =
1680+
ReadCanisterSnapshotDataArgs::decode(payload).and_then(|_args| {
1681+
match self.config.canister_snapshot_download {
1682+
FlagStatus::Disabled => Ok((vec![], None)),
1683+
FlagStatus::Enabled => Ok((vec![], None)),
1684+
}
1685+
});
16751686
ExecuteSubnetMessageResult::Finished {
16761687
response: res,
16771688
refund: msg.take_cycles(),
@@ -1681,8 +1692,13 @@ impl ExecutionEnvironment {
16811692
Ok(Ic00Method::UploadCanisterSnapshotMetadata) => {
16821693
// TODO: EXC-1959
16831694
#[allow(clippy::bind_instead_of_map)]
1684-
let res = UploadCanisterSnapshotMetadataArgs::decode(payload)
1685-
.and_then(|_args| Ok((vec![], None)));
1695+
let res =
1696+
UploadCanisterSnapshotMetadataArgs::decode(payload).and_then(
1697+
|_args| match self.config.canister_snapshot_upload {
1698+
FlagStatus::Disabled => Ok((vec![], None)),
1699+
FlagStatus::Enabled => Ok((vec![], None)),
1700+
},
1701+
);
16861702
ExecuteSubnetMessageResult::Finished {
16871703
response: res,
16881704
refund: msg.take_cycles(),
@@ -1692,8 +1708,12 @@ impl ExecutionEnvironment {
16921708
Ok(Ic00Method::UploadCanisterSnapshotData) => {
16931709
// TODO: EXC-1960
16941710
#[allow(clippy::bind_instead_of_map)]
1695-
let res = UploadCanisterSnapshotDataArgs::decode(payload)
1696-
.and_then(|_args| Ok((vec![], None)));
1711+
let res = UploadCanisterSnapshotDataArgs::decode(payload).and_then(|_args| {
1712+
match self.config.canister_snapshot_upload {
1713+
FlagStatus::Disabled => Ok((vec![], None)),
1714+
FlagStatus::Enabled => Ok((vec![], None)),
1715+
}
1716+
});
16971717
ExecuteSubnetMessageResult::Finished {
16981718
response: res,
16991719
refund: msg.take_cycles(),
@@ -2428,6 +2448,20 @@ impl ExecutionEnvironment {
24282448
result
24292449
}
24302450

2451+
fn read_canister_snapshot_metadata(
2452+
&self,
2453+
sender: PrincipalId,
2454+
state: &ReplicatedState,
2455+
args: ReadCanisterSnapshotMetadataArgs,
2456+
) -> Result<Vec<u8>, UserError> {
2457+
let canister = get_canister(args.get_canister_id(), state)?;
2458+
let snapshot_id = args.get_snapshot_id();
2459+
self.canister_manager
2460+
.read_snapshot_metadata(sender, snapshot_id, canister, state)
2461+
.map(|res| Encode!(&res).unwrap())
2462+
.map_err(|e| e.into())
2463+
}
2464+
24312465
fn node_metrics_history(
24322466
&self,
24332467
state: &ReplicatedState,

rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@ use ic_cycles_account_manager::ResourceSaturation;
66
use ic_error_types::{ErrorCode, RejectCode};
77
use ic_management_canister_types_private::{
88
self as ic00, CanisterChange, CanisterChangeDetails, CanisterSettingsArgsBuilder,
9-
CanisterSnapshotResponse, ClearChunkStoreArgs, DeleteCanisterSnapshotArgs,
9+
CanisterSnapshotResponse, ClearChunkStoreArgs, DeleteCanisterSnapshotArgs, GlobalTimer,
1010
ListCanisterSnapshotArgs, LoadCanisterSnapshotArgs, Method, OnLowWasmMemoryHookStatus,
11-
Payload as Ic00Payload, TakeCanisterSnapshotArgs, UpdateSettingsArgs, UploadChunkArgs,
11+
Payload as Ic00Payload, ReadCanisterSnapshotMetadataArgs, ReadCanisterSnapshotMetadataResponse,
12+
SnapshotSource, TakeCanisterSnapshotArgs, UpdateSettingsArgs, UploadChunkArgs,
1213
};
1314
use ic_registry_subnet_type::SubnetType;
1415
use ic_replicated_state::{
1516
canister_snapshots::SnapshotOperation,
1617
canister_state::{
1718
execution_state::{WasmBinary, WasmExecutionMode},
1819
system_state::CyclesUseCase,
20+
WASM_PAGE_SIZE_IN_BYTES,
1921
},
2022
CanisterState, ExecutionState, SchedulerState,
2123
};
@@ -30,6 +32,7 @@ use ic_types::{
3032
time::UNIX_EPOCH,
3133
CanisterId, Cycles, NumInstructions, SnapshotId,
3234
};
35+
use ic_types_test_utils::ids::user_test_id;
3336
use ic_universal_canister::{wasm, UNIVERSAL_CANISTER_WASM};
3437
use more_asserts::assert_gt;
3538
use serde_bytes::ByteBuf;
@@ -1995,6 +1998,155 @@ fn snapshot_must_include_globals() {
19951998
assert_eq!(result, WasmResult::Reply(vec![1, 0, 0, 0]));
19961999
}
19972000

2001+
#[test]
2002+
fn read_canister_snapshot_metadata_succeeds() {
2003+
let own_subnet = subnet_test_id(1);
2004+
let caller_canister = canister_test_id(1);
2005+
let mut test = ExecutionTestBuilder::new()
2006+
.with_snapshot_metadata_download()
2007+
.with_own_subnet_id(own_subnet)
2008+
.with_caller(own_subnet, caller_canister)
2009+
.build();
2010+
// Create new canister.
2011+
let uni_canister_wasm = UNIVERSAL_CANISTER_WASM.to_vec();
2012+
let canister_id = test
2013+
.canister_from_cycles_and_binary(
2014+
Cycles::new(1_000_000_000_000_000),
2015+
uni_canister_wasm.clone(),
2016+
)
2017+
.unwrap();
2018+
2019+
// Upload chunk.
2020+
let chunk = vec![1, 2, 3, 4, 5];
2021+
let upload_args = UploadChunkArgs {
2022+
canister_id: canister_id.into(),
2023+
chunk,
2024+
};
2025+
let result = test.subnet_message("upload_chunk", upload_args.encode());
2026+
assert!(result.is_ok());
2027+
// Grow the stable memory
2028+
let stable_pages = 13;
2029+
let payload = wasm().stable64_grow(stable_pages).reply().build();
2030+
let _res = test.ingress(canister_id, "update", payload).unwrap();
2031+
// Set some cert data
2032+
let cert_data = [42];
2033+
let payload = wasm().certified_data_set(&cert_data).reply().build();
2034+
let _res = test.ingress(canister_id, "update", payload).unwrap();
2035+
// Set a global timer
2036+
let timestamp = 43;
2037+
let payload = wasm().api_global_timer_set(timestamp).reply().build();
2038+
let _res = test.ingress(canister_id, "update", payload).unwrap();
2039+
2040+
// Take a snapshot of the canister.
2041+
let args: TakeCanisterSnapshotArgs = TakeCanisterSnapshotArgs::new(canister_id, None);
2042+
let result = test.subnet_message("take_canister_snapshot", args.encode());
2043+
let snapshot_id = CanisterSnapshotResponse::decode(&result.unwrap().bytes())
2044+
.unwrap()
2045+
.snapshot_id();
2046+
2047+
// Get the metadata
2048+
let args = ReadCanisterSnapshotMetadataArgs::new(canister_id, snapshot_id);
2049+
let WasmResult::Reply(bytes) = test
2050+
.subnet_message("read_canister_snapshot_metadata", args.encode())
2051+
.unwrap()
2052+
else {
2053+
panic!("expected WasmResult::Reply")
2054+
};
2055+
let metadata = Decode!(&bytes, ReadCanisterSnapshotMetadataResponse).unwrap();
2056+
assert_eq!(metadata.source, SnapshotSource::TakenFromCanister);
2057+
assert_eq!(
2058+
metadata.stable_memory_size,
2059+
WASM_PAGE_SIZE_IN_BYTES as u64 * stable_pages
2060+
);
2061+
assert_eq!(metadata.wasm_module_size, uni_canister_wasm.len() as u64);
2062+
assert_eq!(metadata.wasm_chunk_store.len(), 1);
2063+
assert_eq!(metadata.certified_data, cert_data);
2064+
assert_eq!(metadata.global_timer, GlobalTimer::Active(timestamp));
2065+
assert_eq!(metadata.canister_version, 4);
2066+
}
2067+
2068+
#[test]
2069+
fn read_canister_snapshot_metadata_fails_canister_and_snapshot_must_match() {
2070+
let own_subnet = subnet_test_id(1);
2071+
let caller_canister = canister_test_id(1);
2072+
let mut test = ExecutionTestBuilder::new()
2073+
.with_snapshot_metadata_download()
2074+
.with_own_subnet_id(own_subnet)
2075+
.with_manual_execution()
2076+
.with_caller(own_subnet, caller_canister)
2077+
.build();
2078+
2079+
// Create canister
2080+
let canister_id = test
2081+
.canister_from_cycles_and_binary(
2082+
Cycles::new(1_000_000_000_000_000),
2083+
UNIVERSAL_CANISTER_WASM.to_vec(),
2084+
)
2085+
.unwrap();
2086+
2087+
// Create other canister.
2088+
let other_canister_id = test
2089+
.canister_from_cycles_and_binary(
2090+
Cycles::new(1_000_000_000_000_000),
2091+
UNIVERSAL_CANISTER_WASM.to_vec(),
2092+
)
2093+
.unwrap();
2094+
2095+
// Take a snapshot of the first canister.
2096+
let args: TakeCanisterSnapshotArgs = TakeCanisterSnapshotArgs::new(canister_id, None);
2097+
let result = test.subnet_message("take_canister_snapshot", args.encode());
2098+
let snapshot_id = CanisterSnapshotResponse::decode(&result.unwrap().bytes())
2099+
.unwrap()
2100+
.snapshot_id();
2101+
2102+
// Try to access metadata via the wrong canister id
2103+
let args = ReadCanisterSnapshotMetadataArgs::new(other_canister_id, snapshot_id);
2104+
let error = test
2105+
.subnet_message("read_canister_snapshot_metadata", args.encode())
2106+
.unwrap_err();
2107+
assert_eq!(error.code(), ErrorCode::CanisterRejectedMessage);
2108+
2109+
// Try to access metadata via a bad snapshot id
2110+
let args = ReadCanisterSnapshotMetadataArgs::new(canister_id, (canister_id, 42).into());
2111+
let error = test
2112+
.subnet_message("read_canister_snapshot_metadata", args.encode())
2113+
.unwrap_err();
2114+
assert_eq!(error.code(), ErrorCode::CanisterSnapshotNotFound);
2115+
}
2116+
2117+
#[test]
2118+
fn read_canister_snapshot_metadata_fails_invalid_controller() {
2119+
let own_subnet = subnet_test_id(1);
2120+
let mut test = ExecutionTestBuilder::new()
2121+
.with_snapshot_metadata_download()
2122+
.with_own_subnet_id(own_subnet)
2123+
.with_manual_execution()
2124+
.build();
2125+
// Create new canister.
2126+
let uni_canister_wasm = UNIVERSAL_CANISTER_WASM.to_vec();
2127+
let canister_id = test
2128+
.canister_from_cycles_and_binary(
2129+
Cycles::new(1_000_000_000_000_000),
2130+
uni_canister_wasm.clone(),
2131+
)
2132+
.unwrap();
2133+
2134+
// Take a snapshot of the canister.
2135+
let args: TakeCanisterSnapshotArgs = TakeCanisterSnapshotArgs::new(canister_id, None);
2136+
let result = test.subnet_message("take_canister_snapshot", args.encode());
2137+
let snapshot_id = CanisterSnapshotResponse::decode(&result.unwrap().bytes())
2138+
.unwrap()
2139+
.snapshot_id();
2140+
2141+
// Non-controller user tries to get metadata
2142+
test.set_user_id(user_test_id(42));
2143+
let args = ReadCanisterSnapshotMetadataArgs::new(canister_id, snapshot_id);
2144+
let error = test
2145+
.subnet_message("read_canister_snapshot_metadata", args.encode())
2146+
.unwrap_err();
2147+
assert_eq!(error.code(), ErrorCode::CanisterInvalidController);
2148+
}
2149+
19982150
/// Early warning system / stumbling block forcing the authors of changes adding
19992151
/// or removing canister state fields to think about and/or ask the Execution
20002152
/// team to think about any repercussions to the canister snapshot logic.

rs/execution_environment/tests/dts.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,14 +1231,17 @@ fn dts_aborted_execution_does_not_block_subnet_messages() {
12311231
(method, call_args().other_side(args))
12321232
}),
12331233
Method::ReadCanisterSnapshotMetadata => test_supported(|aborted_canister_id| {
1234-
let args =
1235-
ReadCanisterSnapshotMetadataArgs::new(aborted_canister_id, vec![]).encode();
1234+
let args = ReadCanisterSnapshotMetadataArgs::new(
1235+
aborted_canister_id,
1236+
(aborted_canister_id, 0).into(),
1237+
)
1238+
.encode();
12361239
(method, call_args().other_side(args))
12371240
}),
12381241
Method::ReadCanisterSnapshotData => test_supported(|aborted_canister_id| {
12391242
let args = ReadCanisterSnapshotDataArgs::new(
12401243
aborted_canister_id,
1241-
vec![],
1244+
(aborted_canister_id, 0).into(),
12421245
CanisterSnapshotDataKind::WasmModule { size: 0, offset: 0 },
12431246
)
12441247
.encode();

rs/test_utilities/execution_environment/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,6 +2204,16 @@ impl ExecutionTestBuilder {
22042204
self
22052205
}
22062206

2207+
pub fn with_snapshot_metadata_download(mut self) -> Self {
2208+
self.execution_config.canister_snapshot_download = FlagStatus::Enabled;
2209+
self
2210+
}
2211+
2212+
pub fn with_snapshot_metadata_upload(mut self) -> Self {
2213+
self.execution_config.canister_snapshot_upload = FlagStatus::Enabled;
2214+
self
2215+
}
2216+
22072217
pub fn build(self) -> ExecutionTest {
22082218
let own_range = CanisterIdRange {
22092219
start: CanisterId::from(CANISTER_IDS_PER_SUBNET),

0 commit comments

Comments
 (0)