From 194648a9fde71e38669bee90b29c23ac38e8801c Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Wed, 8 Jan 2025 20:25:51 +0100 Subject: [PATCH] feat(nervous-system): Enable Root to upgrade canisters using chunked Wasms (#3300) This PR enables Root to upgrade canisters using chunked Wasms. This is relevant to both the NNS and the SNSs. --------- Co-authored-by: max-dfinity <100170574+max-dfinity@users.noreply.github.com> --- Cargo.lock | 1 + .../common/test_utils/BUILD.bazel | 1 + .../common/test_utils/src/wasm_helpers.rs | 13 +- .../integration_tests/BUILD.bazel | 21 ++ .../integration_tests/Cargo.toml | 1 + .../src/pocket_ic_helpers.rs | 27 ++ ...sns_controlled_canister_with_large_wasm.rs | 281 ++++++++++++++++++ rs/nervous_system/root/src/change_canister.rs | 119 ++++++-- .../governance/src/proposals/install_code.rs | 3 + rs/nns/handlers/root/impl/canister/root.did | 7 + .../src/governance_upgrade.rs | 1 + rs/registry/admin/src/main.rs | 1 + rs/sns/integration_tests/src/root.rs | 1 + rs/sns/root/canister/root.did | 7 + rs/types/management_canister_types/src/lib.rs | 16 +- 15 files changed, 474 insertions(+), 26 deletions(-) create mode 100644 rs/nervous_system/integration_tests/tests/upgrade_sns_controlled_canister_with_large_wasm.rs diff --git a/Cargo.lock b/Cargo.lock index 5228d801ea2..df0f6ed8a2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9909,6 +9909,7 @@ dependencies = [ "ic-nervous-system-clients", "ic-nervous-system-common", "ic-nervous-system-common-test-keys", + "ic-nervous-system-common-test-utils", "ic-nervous-system-proto", "ic-nervous-system-root", "ic-nervous-system-runtime", diff --git a/rs/nervous_system/common/test_utils/BUILD.bazel b/rs/nervous_system/common/test_utils/BUILD.bazel index ab304da284e..b62ecb544dd 100644 --- a/rs/nervous_system/common/test_utils/BUILD.bazel +++ b/rs/nervous_system/common/test_utils/BUILD.bazel @@ -8,6 +8,7 @@ package_group( name = "ic_nervous_system_common_test_utils_visibility", packages = [ "//rs/nervous_system/common/...", + "//rs/nervous_system/integration_tests/...", "//rs/nns/...", "//rs/sns/...", ], diff --git a/rs/nervous_system/common/test_utils/src/wasm_helpers.rs b/rs/nervous_system/common/test_utils/src/wasm_helpers.rs index 979f379df7b..124d4b3235f 100644 --- a/rs/nervous_system/common/test_utils/src/wasm_helpers.rs +++ b/rs/nervous_system/common/test_utils/src/wasm_helpers.rs @@ -1,5 +1,6 @@ use ic_wasm; use libflate::gzip; +use std::io::Read; /// A small, valid WASM suitable for tests. pub const SMALLEST_VALID_WASM_BYTES: &[u8; 8] = &[0, 0x61, 0x73, 0x6D, 1, 0, 0, 0]; @@ -25,7 +26,7 @@ pub fn annotate_wasm_with_metadata( wasm_module.emit_wasm() } -// Gzips a wasm, returning the hash of its compressed representation. +/// Gzips a wasm, returning the hash of its compressed representation. pub fn gzip_wasm(wasm: &[u8]) -> Vec { let mut encoder = gzip::Encoder::new(Vec::new()).expect("Failed to create gzip encoder."); std::io::copy(&mut &wasm[..], &mut encoder).expect("Failed to copy WASM bytes."); @@ -34,3 +35,13 @@ pub fn gzip_wasm(wasm: &[u8]) -> Vec { .into_result() .expect("Failed to finish gzip encoding.") } + +/// Decompresses a previously gzipped wasm. +pub fn ungzip_wasm(gzipped_bytes: &[u8]) -> Vec { + let mut decoder = gzip::Decoder::new(gzipped_bytes).expect("Failed to create gzip decoder."); + let mut wasm_buf = Vec::new(); + decoder + .read_to_end(&mut wasm_buf) + .expect("Failed decoding Wasm."); + wasm_buf +} diff --git a/rs/nervous_system/integration_tests/BUILD.bazel b/rs/nervous_system/integration_tests/BUILD.bazel index bc1a77c66d3..5ed2885da3c 100644 --- a/rs/nervous_system/integration_tests/BUILD.bazel +++ b/rs/nervous_system/integration_tests/BUILD.bazel @@ -41,6 +41,7 @@ BASE_DEPENDENCIES = [ "//packages/pocket-ic", "//rs/crypto/sha2", "//rs/nervous_system/common/test_keys", + "//rs/nervous_system/common/test_utils", "//rs/nns/constants", "//rs/protobuf", "//rs/registry/canister", @@ -100,6 +101,7 @@ DEV_DATA = [ "//rs/sns/root:sns-root-canister", "//rs/sns/swap:sns-swap-canister", "//rs/universal_canister/impl:universal_canister.wasm.gz", + "//testnet/prebuilt-canisters:image-classification", "@cycles-ledger.wasm.gz//file", "@mainnet_ic-icrc1-archive//file", "@mainnet_ic-icrc1-index-ng//file", @@ -124,6 +126,7 @@ DEV_ENV = { "IC_ICRC1_ARCHIVE_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/archive:archive_canister)", "IC_ICRC1_INDEX_NG_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/index-ng:index_ng_canister)", "IC_ICRC1_LEDGER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/ledger:ledger_canister)", + "IMAGE_CLASSIFICATION_CANISTER_WASM_PATH": "$(rootpath //testnet/prebuilt-canisters:image-classification)", "LEDGER_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icp/ledger:ledger-canister-wasm)", "LEDGER_CANISTER_NOTIFY_METHOD_WASM_PATH": "$(rootpath //rs/ledger_suite/icp/ledger:ledger-canister-wasm-notify-method)", "LEDGER_ARCHIVE_NODE_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icp/archive:ledger-archive-node-canister-wasm)", @@ -178,6 +181,7 @@ rust_test_suite_with_extra_srcs( "tests/deploy_fresh_sns_test.rs", "tests/sns_release_qualification_legacy.rs", "tests/sns_upgrade_test_utils_legacy.rs", + "tests/upgrade_sns_controlled_canister_with_large_wasm.rs", ], ), aliases = ALIASES, @@ -279,3 +283,20 @@ rust_test( ], deps = [":nervous_system_integration_tests"] + DEPENDENCIES_WITH_TEST_FEATURES + DEV_DEPENDENCIES, ) + +rust_test( + name = "upgrade_sns_controlled_canister_with_large_wasm", + timeout = "long", + srcs = [ + "tests/upgrade_sns_controlled_canister_with_large_wasm.rs", + ], + aliases = ALIASES, + data = DEV_DATA, + env = DEV_ENV | {"RUST_TEST_NOCAPTURE": "1"}, + flaky = True, + proc_macro_deps = MACRO_DEPENDENCIES + MACRO_DEV_DEPENDENCIES, + tags = [ + "cpu:4", + ], + deps = [":nervous_system_integration_tests"] + DEPENDENCIES_WITH_TEST_FEATURES + DEV_DEPENDENCIES, +) diff --git a/rs/nervous_system/integration_tests/Cargo.toml b/rs/nervous_system/integration_tests/Cargo.toml index 35e0c69236a..15a1334d584 100644 --- a/rs/nervous_system/integration_tests/Cargo.toml +++ b/rs/nervous_system/integration_tests/Cargo.toml @@ -49,6 +49,7 @@ ic-icrc1-index-ng = { path = "../../ledger_suite/icrc1/index-ng" } ic-icrc1-tokens-u64 = { path = "../../ledger_suite/icrc1/tokens_u64" } ic-management-canister-types = { path = "../../types/management_canister_types" } ic-nervous-system-common-test-keys = { path = "../common/test_keys" } +ic-nervous-system-common-test-utils = { path = "../../nervous_system/common/test_utils" } ic-nervous-system-root = { path = "../root" } ic-nns-constants = { path = "../../nns/constants" } ic-nns-gtc = { path = "../../nns/gtc" } diff --git a/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs b/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs index c75a6354ab3..ceee91c722d 100644 --- a/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs +++ b/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs @@ -185,6 +185,33 @@ pub async fn install_canister_with_controllers( ); } +pub async fn install_canister_on_subnet( + pocket_ic: &PocketIc, + subnet_id: Principal, + arg: Vec, + wasm: Option, + controllers: Vec, +) -> CanisterId { + let controllers = controllers.into_iter().map(|c| c.0).collect::>(); + let controller_principal = controllers.first().cloned(); + let settings = Some(CanisterSettings { + controllers: Some(controllers), + ..Default::default() + }); + let canister_id = pocket_ic + .create_canister_on_subnet(None, settings, subnet_id) + .await; + pocket_ic + .add_cycles(canister_id, STARTING_CYCLES_PER_CANISTER) + .await; + if let Some(wasm) = wasm { + pocket_ic + .install_canister(canister_id, wasm.bytes(), arg, controller_principal) + .await; + } + CanisterId::unchecked_from_principal(canister_id.into()) +} + // TODO migrate this to nns::governance pub async fn add_wasm_via_nns_proposal( pocket_ic: &PocketIc, diff --git a/rs/nervous_system/integration_tests/tests/upgrade_sns_controlled_canister_with_large_wasm.rs b/rs/nervous_system/integration_tests/tests/upgrade_sns_controlled_canister_with_large_wasm.rs new file mode 100644 index 00000000000..b0617beb5e0 --- /dev/null +++ b/rs/nervous_system/integration_tests/tests/upgrade_sns_controlled_canister_with_large_wasm.rs @@ -0,0 +1,281 @@ +use std::collections::BTreeSet; + +use candid::Principal; +use canister_test::Wasm; +use ic_base_types::PrincipalId; +use ic_management_canister_types::CanisterInstallMode; +use ic_nervous_system_integration_tests::pocket_ic_helpers::{ + await_with_timeout, install_canister_on_subnet, nns, sns, +}; +use ic_nervous_system_integration_tests::{ + create_service_nervous_system_builder::CreateServiceNervousSystemBuilder, + pocket_ic_helpers::{add_wasms_to_sns_wasm, install_nns_canisters}, +}; +use ic_nervous_system_root::change_canister::ChangeCanisterRequest; +use ic_nervous_system_root::change_canister::ChunkedCanisterWasm; +use ic_nns_constants::ROOT_CANISTER_ID; +use ic_nns_test_utils::common::modify_wasm_bytes; +use ic_sns_swap::pb::v1::Lifecycle; +use pocket_ic::nonblocking::PocketIc; +use pocket_ic::PocketIcBuilder; + +const MIN_INSTALL_CHUNKED_CODE_TIME_SECONDS: u64 = 20; +const MAX_INSTALL_CHUNKED_CODE_TIME_SECONDS: u64 = 5 * 60; + +const CHUNK_SIZE: usize = 1024 * 1024; // 1 MiB + +#[tokio::test] +async fn test_store_same_as_target() { + let store_same_as_target = true; + run_test(store_same_as_target).await; +} + +#[tokio::test] +async fn test_store_different_from_target() { + let store_same_as_target = false; + run_test(store_same_as_target).await; +} + +mod interim_sns_helpers { + use super::*; + + use candid::{Decode, Encode}; + use pocket_ic::nonblocking::PocketIc; + use pocket_ic::WasmResult; + + /// Interim test function for calling Root.change_canister. + /// + /// This function is not in src/pocket_ic_helpers.rs because it's going to be replaced with + /// a proposal with the same effect. It should not be used in any other tests. + pub async fn change_canister( + pocket_ic: &PocketIc, + canister_id: PrincipalId, + sender: PrincipalId, + request: ChangeCanisterRequest, + ) { + let result = pocket_ic + .update_call( + canister_id.into(), + sender.into(), + "change_canister", + Encode!(&request).unwrap(), + ) + .await + .unwrap(); + let result = match result { + WasmResult::Reply(result) => result, + WasmResult::Reject(s) => panic!("Call to change_canister failed: {:#?}", s), + }; + Decode!(&result, ()).unwrap() + } +} + +fn very_large_wasm_bytes() -> Vec { + let image_classification_canister_wasm_path = + std::env::var("IMAGE_CLASSIFICATION_CANISTER_WASM_PATH") + .expect("Please ensure that this Bazel test target correctly specifies env and data."); + + let wasm_path = std::path::PathBuf::from(image_classification_canister_wasm_path); + + std::fs::read(&wasm_path).expect("Failed to read WASM file") +} + +fn format_full_hash(hash: &[u8]) -> String { + hash.iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join("") +} + +/// Uploads `wasm` into the store canister, one [`CHUNK_SIZE`]-sized chunk at a time. +/// +/// Returns the vector of uploaded chunk hashes. +async fn upload_wasm_as_chunks( + pocket_ic: &PocketIc, + store_controller_id: Principal, + store_canister_id: Principal, + wasm: Wasm, + num_chunks_expected: usize, +) -> Vec> { + let sender = Some(store_controller_id); + + let mut uploaded_chunk_hashes = Vec::new(); + + for chunk in wasm.bytes().chunks(CHUNK_SIZE) { + let uploaded_chunk_hash = pocket_ic + .upload_chunk(store_canister_id, sender, chunk.to_vec()) + .await + .unwrap(); + + uploaded_chunk_hashes.push(uploaded_chunk_hash); + } + + // Smoke test + { + let stored_chunk_hashes = pocket_ic + .stored_chunks(store_canister_id, sender) + .await + .unwrap() + .into_iter() + .map(|hash| format_full_hash(&hash[..])) + .collect::>(); + + let stored_chunk_hashes = BTreeSet::from_iter(stored_chunk_hashes.iter()); + + let uploaded_chunk_hashes = uploaded_chunk_hashes + .iter() + .map(|hash| format_full_hash(&hash[..])) + .collect::>(); + let uploaded_chunk_hashes = BTreeSet::from_iter(uploaded_chunk_hashes.iter()); + + assert!(uploaded_chunk_hashes.is_subset(&stored_chunk_hashes)); + assert_eq!(uploaded_chunk_hashes.len(), num_chunks_expected); + } + + uploaded_chunk_hashes +} + +async fn run_test(store_same_as_target: bool) { + // 1. Prepare the world + let pocket_ic = PocketIcBuilder::new() + .with_nns_subnet() + .with_sns_subnet() + .with_application_subnet() + .build_async() + .await; + + // Install the NNS canisters. + { + let with_mainnet_nns_canisters = false; + install_nns_canisters(&pocket_ic, vec![], with_mainnet_nns_canisters, None, vec![]).await; + } + + // Publish SNS Wasms to SNS-W. + { + let with_mainnet_sns_canisters = false; + add_wasms_to_sns_wasm(&pocket_ic, with_mainnet_sns_canisters) + .await + .unwrap(); + }; + + // Install a dapp canister. + let original_wasm = Wasm::from_bytes(very_large_wasm_bytes()); + let original_wasm_hash = original_wasm.sha256_hash(); + + let app_subnet = pocket_ic.topology().await.get_app_subnets()[0]; + + let target_canister_id = install_canister_on_subnet( + &pocket_ic, + app_subnet, + vec![], + Some(original_wasm.clone()), + vec![ROOT_CANISTER_ID.into()], + ) + .await; + + let sns = { + let create_service_nervous_system = CreateServiceNervousSystemBuilder::default() + .with_dapp_canisters(vec![target_canister_id]) + .build(); + + let swap_parameters = create_service_nervous_system + .swap_parameters + .clone() + .unwrap(); + + let sns_instance_label = "1"; + let (sns, _) = nns::governance::propose_to_deploy_sns_and_wait( + &pocket_ic, + create_service_nervous_system, + sns_instance_label, + ) + .await; + + sns::swap::await_swap_lifecycle(&pocket_ic, sns.swap.canister_id, Lifecycle::Open) + .await + .unwrap(); + sns::swap::smoke_test_participate_and_finalize( + &pocket_ic, + sns.swap.canister_id, + swap_parameters, + ) + .await; + + sns + }; + + let store_canister_id = if store_same_as_target { + target_canister_id + } else { + install_canister_on_subnet( + &pocket_ic, + app_subnet, + vec![], + None, + vec![sns.root.canister_id], + ) + .await + }; + + let new_wasm = { + let new_wasm_bytes = modify_wasm_bytes(&original_wasm.bytes(), 42); + Wasm::from_bytes(&new_wasm_bytes[..]) + }; + let new_wasm_hash = new_wasm.sha256_hash(); + + // Smoke test + assert_ne!(new_wasm_hash, original_wasm_hash); + + // WASM with 15_843_866 bytes (`image-classification.wasm.gz`) is split into 1 MiB chunks. + let num_chunks_expected = 16; + + let chunk_hashes_list = upload_wasm_as_chunks( + &pocket_ic, + sns.root.canister_id.into(), + store_canister_id.into(), + new_wasm, + num_chunks_expected, + ) + .await; + + // 2. Run code under test. + interim_sns_helpers::change_canister( + &pocket_ic, + sns.root.canister_id, + sns.governance.canister_id, + ChangeCanisterRequest { + stop_before_installing: true, + mode: CanisterInstallMode::Upgrade, + canister_id: target_canister_id, + // This is the old field being generalized. + wasm_module: vec![], + // This is the new field we want to test. + chunked_canister_wasm: Some(ChunkedCanisterWasm { + wasm_module_hash: new_wasm_hash.clone().to_vec(), + store_canister_id, + chunk_hashes_list, + }), + arg: vec![], + compute_allocation: None, + memory_allocation: None, + }, + ) + .await; + + // 3. Inspect the resulting state. + await_with_timeout( + &pocket_ic, + MIN_INSTALL_CHUNKED_CODE_TIME_SECONDS..MAX_INSTALL_CHUNKED_CODE_TIME_SECONDS, + |pocket_ic| async { + let status = pocket_ic + .canister_status(target_canister_id.into(), Some(sns.root.canister_id.into())) + .await; + status + .expect("canister status must be available") + .module_hash + }, + &Some(new_wasm_hash.to_vec()), + ) + .await + .unwrap(); +} diff --git a/rs/nervous_system/root/src/change_canister.rs b/rs/nervous_system/root/src/change_canister.rs index 8cddc7ebb92..e96dfefc4df 100644 --- a/rs/nervous_system/root/src/change_canister.rs +++ b/rs/nervous_system/root/src/change_canister.rs @@ -4,7 +4,10 @@ use dfn_core::api::CanisterId; #[cfg(target_arch = "wasm32")] use dfn_core::println; use ic_crypto_sha2::Sha256; -use ic_management_canister_types::{CanisterInstallMode, InstallCodeArgs, IC_00}; +use ic_management_canister_types::{ + CanisterInstallMode, CanisterInstallModeV2, ChunkHash, InstallChunkedCodeArgs, InstallCodeArgs, + IC_00, +}; use ic_nervous_system_clients::{ canister_id_record::CanisterIdRecord, canister_status::{ @@ -14,6 +17,23 @@ use ic_nervous_system_clients::{ use ic_nervous_system_runtime::Runtime; use serde::Serialize; +/// The structure allows reconstructing a potentially large WASM from chunks needed to upgrade or +/// reinstall some target canister. +#[derive(Clone, Debug, Eq, PartialEq, CandidType, Deserialize, Serialize)] +pub struct ChunkedCanisterWasm { + /// Check sum of the overall WASM to be reassembled from chunks. + pub wasm_module_hash: Vec, + + /// Indicates which canister stores the WASM chunks. The store canister must be on the same + /// subnet as the target canister (Root must be one of the controllers of both of them). + /// May be the same as the target canister ID. + pub store_canister_id: CanisterId, + + /// Specifies a list of hash values for the chunks that comprise this WASM. Must contain + /// at least one chunk. + pub chunk_hashes_list: Vec>, +} + /// Argument to the similarly-named methods on the NNS and SNS root canisters. #[derive(Clone, Eq, PartialEq, CandidType, Deserialize, Serialize)] pub struct ChangeCanisterRequest { @@ -44,6 +64,10 @@ pub struct ChangeCanisterRequest { #[serde(with = "serde_bytes")] pub wasm_module: Vec, + /// If the entire WASM does not into the 2 MiB ingress limit, then `new_canister_wasm` + /// should be empty, and this field should be set instead. + pub chunked_canister_wasm: Option, + /// The new canister args #[serde(with = "serde_bytes")] pub arg: Vec, @@ -68,6 +92,7 @@ impl ChangeCanisterRequest { .field("mode", &self.mode) .field("canister_id", &self.canister_id) .field("wasm_module_sha256", &format!("{:x?}", wasm_sha)) + .field("chunked_canister_wasm", &self.chunked_canister_wasm) .field("arg_sha256", &format!("{:x?}", arg_sha)) .field("compute_allocation", &self.compute_allocation) .field("memory_allocation", &self.memory_allocation) @@ -98,6 +123,7 @@ impl ChangeCanisterRequest { mode, canister_id, wasm_module: Vec::new(), + chunked_canister_wasm: None, arg: Encode!().unwrap(), compute_allocation: None, memory_allocation: None, @@ -114,6 +140,20 @@ impl ChangeCanisterRequest { self } + pub fn with_chunked_wasm( + mut self, + wasm_module_hash: Vec, + store_canister_id: CanisterId, + chunk_hashes_list: Vec>, + ) -> Self { + self.chunked_canister_wasm = Some(ChunkedCanisterWasm { + wasm_module_hash, + store_canister_id, + chunk_hashes_list, + }); + self + } + pub fn with_arg(mut self, arg: Vec) -> Self { self.arg = arg; self @@ -218,6 +258,8 @@ where } } + let request_str = format!("{:?}", request); + // Ship code to the canister. // // Note that there's no guarantee that the canister to install/reinstall/upgrade @@ -225,7 +267,7 @@ where // because there could be a concurrent request to restart it. This could be // guaranteed with a "stopped precondition" in the management canister, or // with some locking here. - let res = install_code(request.clone()).await; + let res = install_code(request).await; // For once, we don't want to unwrap the result here. The reason is that, if the // installation failed (e.g., the wasm was rejected because it's invalid), // then we want to restart the canister. So we just keep the res to be @@ -237,15 +279,21 @@ where } // Check the result of the install_code - res.map_err(|(rejection_code, message)| format!("Attempt to call install_code with request {request:?} failed with code {rejection_code:?}: {message}")) + res.map_err(|(rejection_code, message)| { + format!( + "Attempt to call install_code with request {request_str} failed with code \ + {rejection_code:?}: {message}" + ) + }) } -/// Calls the "install_code" method of the management canister. +/// Calls a function of the management canister to install the requested code. async fn install_code(request: ChangeCanisterRequest) -> ic_cdk::api::call::CallResult<()> { let ChangeCanisterRequest { mode, canister_id, wasm_module, + chunked_canister_wasm, arg, compute_allocation, memory_allocation, @@ -256,24 +304,51 @@ async fn install_code(request: ChangeCanisterRequest) -> ic_cdk::api::call::Call let canister_id = canister_id.get(); let sender_canister_version = Some(ic_cdk::api::canister_version()); - let install_code_args = InstallCodeArgs { - mode, - canister_id, - wasm_module, - arg, - compute_allocation, - memory_allocation, - sender_canister_version, - }; - // Warning: despite dfn_core::call returning a Result, it actually traps when - // the callee traps! Use the public cdk instead, which does not have this - // issue. - ic_cdk::api::call::call( - Principal::try_from(IC_00.get().as_slice()).unwrap(), - "install_code", - (&install_code_args,), - ) - .await + if let Some(ChunkedCanisterWasm { + wasm_module_hash, + store_canister_id, + chunk_hashes_list, + }) = chunked_canister_wasm + { + let target_canister = canister_id; + let store_canister = Some(store_canister_id.get()); + let chunk_hashes_list = chunk_hashes_list + .into_iter() + .map(|hash| ChunkHash { hash }) + .collect(); + let mode = CanisterInstallModeV2::from(mode); + let argument = InstallChunkedCodeArgs { + mode, + target_canister, + store_canister, + chunk_hashes_list, + wasm_module_hash, + arg, + sender_canister_version, + }; + ic_cdk::api::call::call( + Principal::try_from(IC_00.get().as_slice()).unwrap(), + "install_chunked_code", + (&argument,), + ) + .await + } else { + let argument = InstallCodeArgs { + mode, + canister_id, + wasm_module, + arg, + compute_allocation, + memory_allocation, + sender_canister_version, + }; + ic_cdk::api::call::call( + Principal::try_from(IC_00.get().as_slice()).unwrap(), + "install_code", + (&argument,), + ) + .await + } } pub async fn start_canister(canister_id: CanisterId) -> Result<(), (i32, String)> diff --git a/rs/nns/governance/src/proposals/install_code.rs b/rs/nns/governance/src/proposals/install_code.rs index 1211db9fdba..3b53094d2b8 100644 --- a/rs/nns/governance/src/proposals/install_code.rs +++ b/rs/nns/governance/src/proposals/install_code.rs @@ -110,6 +110,7 @@ impl InstallCode { arg, compute_allocation, memory_allocation, + chunked_canister_wasm: None, }) .map_err(|e| invalid_proposal_error(&format!("Failed to encode payload: {}", e))) } @@ -307,6 +308,7 @@ mod tests { arg: vec![4, 5, 6], compute_allocation: None, memory_allocation: None, + chunked_canister_wasm: None, } ); } @@ -375,6 +377,7 @@ mod tests { arg: vec![], compute_allocation: None, memory_allocation: None, + chunked_canister_wasm: None, } ); } diff --git a/rs/nns/handlers/root/impl/canister/root.did b/rs/nns/handlers/root/impl/canister/root.did index ee6ab152c0b..3e7099b6891 100644 --- a/rs/nns/handlers/root/impl/canister/root.did +++ b/rs/nns/handlers/root/impl/canister/root.did @@ -68,9 +68,16 @@ type ChangeCanisterControllersResult = variant { Err : ChangeCanisterControllersError; }; +type ChunkedCanisterWasm = record { + wasm_module_hash : blob; + store_canister_id : principal; + chunk_hashes_list : vec blob; +}; + type ChangeCanisterRequest = record { arg : blob; wasm_module : blob; + chunked_canister_wasm : opt ChunkedCanisterWasm; stop_before_installing : bool; mode : CanisterInstallMode; canister_id : principal; diff --git a/rs/nns/integration_tests/src/governance_upgrade.rs b/rs/nns/integration_tests/src/governance_upgrade.rs index 51f27993ad0..15d49a492c0 100644 --- a/rs/nns/integration_tests/src/governance_upgrade.rs +++ b/rs/nns/integration_tests/src/governance_upgrade.rs @@ -130,6 +130,7 @@ fn test_root_restarts_canister_during_upgrade_canister_with_stop_canister_timeou arg: vec![], compute_allocation: None, memory_allocation: None, + chunked_canister_wasm: None, }; let _: () = update_with_sender( diff --git a/rs/registry/admin/src/main.rs b/rs/registry/admin/src/main.rs index 4a5ca745546..c32e582296d 100644 --- a/rs/registry/admin/src/main.rs +++ b/rs/registry/admin/src/main.rs @@ -1022,6 +1022,7 @@ impl ProposalPayload for ProposeToChangeNnsCanisterCmd { arg, compute_allocation: self.compute_allocation.map(candid::Nat::from), memory_allocation: self.memory_allocation.map(candid::Nat::from), + chunked_canister_wasm: None, } } } diff --git a/rs/sns/integration_tests/src/root.rs b/rs/sns/integration_tests/src/root.rs index 65ab0246be6..7d240bd854b 100644 --- a/rs/sns/integration_tests/src/root.rs +++ b/rs/sns/integration_tests/src/root.rs @@ -317,6 +317,7 @@ fn test_root_restarts_governance_on_stop_canister_timeout() { arg: vec![], compute_allocation: None, memory_allocation: None, + chunked_canister_wasm: None, }; let _: () = update_with_sender( diff --git a/rs/sns/root/canister/root.did b/rs/sns/root/canister/root.did index bc3856b691d..352ee49fd64 100644 --- a/rs/sns/root/canister/root.did +++ b/rs/sns/root/canister/root.did @@ -43,9 +43,16 @@ type CanisterSummary = record { canister_id : opt principal; }; +type ChunkedCanisterWasm = record { + wasm_module_hash : blob; + store_canister_id : principal; + chunk_hashes_list : vec blob; +}; + type ChangeCanisterRequest = record { arg : blob; wasm_module : blob; + chunked_canister_wasm : opt ChunkedCanisterWasm; stop_before_installing : bool; mode : CanisterInstallMode; canister_id : principal; diff --git a/rs/types/management_canister_types/src/lib.rs b/rs/types/management_canister_types/src/lib.rs index e55a3d88925..0997a188ce3 100644 --- a/rs/types/management_canister_types/src/lib.rs +++ b/rs/types/management_canister_types/src/lib.rs @@ -1473,9 +1473,19 @@ impl From for CanisterInstallMode { /// The function is lossy, hence it should be avoided when possible. fn from(item: CanisterInstallModeV2) -> Self { match item { - CanisterInstallModeV2::Install => CanisterInstallMode::Install, - CanisterInstallModeV2::Reinstall => CanisterInstallMode::Reinstall, - CanisterInstallModeV2::Upgrade(_) => CanisterInstallMode::Upgrade, + CanisterInstallModeV2::Install => Self::Install, + CanisterInstallModeV2::Reinstall => Self::Reinstall, + CanisterInstallModeV2::Upgrade(_) => Self::Upgrade, + } + } +} + +impl From for CanisterInstallModeV2 { + fn from(item: CanisterInstallMode) -> Self { + match item { + CanisterInstallMode::Install => Self::Install, + CanisterInstallMode::Reinstall => Self::Reinstall, + CanisterInstallMode::Upgrade => Self::Upgrade(None), } } }