Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions rs/nervous_system/clients/src/update_settings.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use candid::CandidType;
use ic_base_types::PrincipalId;
use ic_management_canister_types_private as management_canister;
use ic_management_canister_types_private::IC_00;
use ic_nervous_system_runtime::Runtime;
use serde::Deserialize;
Expand Down Expand Up @@ -46,6 +47,55 @@ pub struct CanisterSettings {
pub wasm_memory_threshold: Option<candid::Nat>,
}

impl From<LogVisibility> for management_canister::LogVisibilityV2 {
fn from(original: LogVisibility) -> Self {
match original {
LogVisibility::Controllers => management_canister::LogVisibilityV2::Controllers,
LogVisibility::Public => management_canister::LogVisibilityV2::Public,
}
}
}

impl From<SnapshotVisibility> for management_canister::SnapshotVisibility {
fn from(original: SnapshotVisibility) -> Self {
match original {
SnapshotVisibility::Controllers => management_canister::SnapshotVisibility::Controllers,
SnapshotVisibility::Public => management_canister::SnapshotVisibility::Public,
}
}
}

impl From<CanisterSettings> for management_canister::CanisterSettingsArgs {
fn from(original: CanisterSettings) -> Self {
let CanisterSettings {
controllers,
compute_allocation,
memory_allocation,
freezing_threshold,
reserved_cycles_limit,
log_visibility,
wasm_memory_limit,
wasm_memory_threshold,
snapshot_visibility,
} = original;

management_canister::CanisterSettingsArgs {
controllers: controllers.map(management_canister::BoundedControllers::new),
compute_allocation,
memory_allocation,
freezing_threshold,
reserved_cycles_limit,
log_visibility: log_visibility.map(management_canister::LogVisibilityV2::from),
snapshot_visibility: snapshot_visibility
.map(management_canister::SnapshotVisibility::from),
log_memory_limit: None,
wasm_memory_limit,
wasm_memory_threshold,
environment_variables: None,
}
}
}

/// A wrapper call to the management canister `update_settings` API.
pub async fn update_settings<Rt>(update_settings: UpdateSettings) -> Result<(), (i32, String)>
where
Expand Down
11 changes: 11 additions & 0 deletions rs/nns/handlers/root/impl/canister/canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use ic_nns_handler_root::{
};
use ic_nns_handler_root_interface::{
ChangeCanisterControllersRequest, ChangeCanisterControllersResponse,
CreateCanisterAndInstallCodeRequest, CreateCanisterAndInstallCodeResponse,
LoadCanisterSnapshotRequest, LoadCanisterSnapshotResponse, TakeCanisterSnapshotRequest,
TakeCanisterSnapshotResponse, UpdateCanisterSettingsRequest, UpdateCanisterSettingsResponse,
};
Expand Down Expand Up @@ -228,6 +229,16 @@ async fn update_canister_settings(
.await
}

/// Creates a new canister on the specified subnet and installs code into it.
/// Only callable by NNS Governance.
#[update]
async fn create_canister_and_install_code(
request: CreateCanisterAndInstallCodeRequest,
) -> CreateCanisterAndInstallCodeResponse {
check_caller_is_governance();
canister_management::create_canister_and_install_code(request).await
}

/// Takes a snapshot of a canister controlled by NNS Root. Only callable by NNS
/// Governance.
#[update]
Expand Down
24 changes: 24 additions & 0 deletions rs/nns/handlers/root/impl/canister/root.did
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,27 @@ type LoadCanisterSnapshotError = record {
description : text;
};

type CreateCanisterAndInstallCodeRequest = record {
host_subnet_id : principal;
canister_settings : opt CanisterSettings;
wasm_module : blob;
install_arg : blob;
Comment thread
daniel-wong-dfinity-org marked this conversation as resolved.
};

type CreateCanisterAndInstallCodeResponse = variant {
Ok : CreateCanisterAndInstallCodeOk;
Err : CreateCanisterAndInstallCodeError;
};

type CreateCanisterAndInstallCodeOk = record {
canister_id : principal;
};

type CreateCanisterAndInstallCodeError = record {
code : opt int32;
description : text;
};

service : () -> {
canister_status : (CanisterIdRecord) -> (CanisterStatusResult);
get_build_metadata : () -> (text) query;
Expand All @@ -213,6 +234,9 @@ service : () -> {
UpdateCanisterSettingsResponse,
);
call_canister : (CallCanisterRequest) -> ();
create_canister_and_install_code : (CreateCanisterAndInstallCodeRequest) -> (
CreateCanisterAndInstallCodeResponse,
);
take_canister_snapshot : (TakeCanisterSnapshotArgs) -> (
TakeCanisterSnapshotResponse,
);
Expand Down
124 changes: 121 additions & 3 deletions rs/nns/handlers/root/impl/src/canister_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ use crate::PROXIED_CANISTER_CALLS_TRACKER;
use ic_base_types::{CanisterId, PrincipalId};
use ic_cdk::{
api::call::{RejectionCode, call_with_payment},
call, caller, print,
call,
call::{Call, CallFailed},
caller, print,
};
use ic_management_canister_types_private::{
CanisterInstallMode::Install, CanisterSettingsArgsBuilder, CreateCanisterArgs, InstallCodeArgs,
self as management_canister, CanisterInstallMode::Install, CanisterSettingsArgsBuilder,
CreateCanisterArgs, InstallCodeArgs,
};
use ic_nervous_system_clients::{
canister_id_record::CanisterIdRecord,
Expand All @@ -24,7 +27,9 @@ use ic_nns_common::{
};
use ic_nns_handler_root_interface::{
ChangeCanisterControllersRequest, ChangeCanisterControllersResponse,
UpdateCanisterSettingsError, UpdateCanisterSettingsRequest, UpdateCanisterSettingsResponse,
CreateCanisterAndInstallCodeError, CreateCanisterAndInstallCodeRequest,
CreateCanisterAndInstallCodeResponse, UpdateCanisterSettingsError,
UpdateCanisterSettingsRequest, UpdateCanisterSettingsResponse,
};
use ic_protobuf::{
registry::nns::v1::{NnsCanisterRecord, NnsCanisterRecords},
Expand Down Expand Up @@ -251,3 +256,116 @@ pub async fn update_canister_settings(
}
}
}

// Unlike update_canister_settings and change_canister_controllers, this does
// not use ManagementCanisterClient because:
// 1. We need to target a specific subnet (host_subnet_id), not IC_00.
// 2. We use Call::bounded_wait for timeout protection against slow/malicious
// host subnets.
// 3. ManagementCanisterClient lacks create_canister and install_code methods.
// 4. Rate limiting (LimitedOutstandingCallsManagementCanisterClient) is not
// needed here, since only Governance can call this.
pub async fn create_canister_and_install_code(
request: CreateCanisterAndInstallCodeRequest,
) -> CreateCanisterAndInstallCodeResponse {
let CreateCanisterAndInstallCodeRequest {
host_subnet_id,
canister_settings,
wasm_module,
install_arg,
} = request;

// We call host_subnet_id directly (rather than the Management (virtual)
// canister) so that the canister is created on that specific subnet
// (not the one where this canister (i.e. Root) lives).
let callee = host_subnet_id.0;

let main = async {
// Step 1: Create canister.

// Step 1.1: Send create_canister call (to the subnet, not "aaaaa-aa", and attach cycles).
let create_canister_args = CreateCanisterArgs {
settings: canister_settings.map(management_canister::CanisterSettingsArgs::from),
sender_canister_version: Some(ic_cdk::api::canister_version()),
};
let create_canister_result = Call::bounded_wait(callee, "create_canister")
.with_arg(create_canister_args)
.await;

// Step 1.2: Handle the create_canister result. This consists of decoding
// the reply, and handling errors.
let response = create_canister_result.map_err(|err| {
convert_call_failed_to_create_canister_and_install_code_error("create_canister", err)
})?;
let create_canister_reply: CanisterIdRecord = response.candid().map_err(|err| {
// Show raw bytes to aid debugging when the response can't be decoded.
let response = humanize_blob(response.as_ref());
CreateCanisterAndInstallCodeError {
code: None,
description: format!(
"Failed to decode create_canister response: {err}. Raw reply (hex): {response}"
),
}
})?;
// This will be returned (assuming install_code goes well).
let new_canister_id = create_canister_reply.get_canister_id();

// Step 2: Install code into the new canister.
let install_code_args = InstallCodeArgs {
mode: Install,
canister_id: new_canister_id.get(),
wasm_module,
arg: install_arg,
sender_canister_version: None,
};
Call::bounded_wait(callee, "install_code")
.with_arg(install_code_args)
.await
.map_err(|err| {
convert_call_failed_to_create_canister_and_install_code_error("install_code", err)
})?;

Ok(new_canister_id)
};

let result = main.await;
CreateCanisterAndInstallCodeResponse::from(result)
}

/// Hex-encodes `bytes`, truncating to the first 200 bytes with a suffix if longer.
fn humanize_blob(bytes: &[u8]) -> String {
if bytes.len() <= 200 {
bytes.iter().map(|b| format!("{b:02x}")).collect()
} else {
let encoded: String = bytes[..200].iter().map(|b| format!("{b:02x}")).collect();
format!("{encoded}... ({} bytes total)", bytes.len())
}
}

fn convert_call_failed_to_create_canister_and_install_code_error(
method_name: &str,
err: CallFailed,
) -> CreateCanisterAndInstallCodeError {
match err {
CallFailed::CallRejected(err) => {
let code = err.reject_code().map(|code| code as i32).ok();
let message = err.reject_message();
CreateCanisterAndInstallCodeError {
code,
description: format!("{method_name} was rejected: {message}"),
}
}
CallFailed::CallPerformFailed(_) => CreateCanisterAndInstallCodeError {
code: None,
description: format!("{method_name} failed: ic0.call_perform returned non-zero"),
},
CallFailed::InsufficientLiquidCycleBalance(err) => CreateCanisterAndInstallCodeError {
code: None,
description: format!(
"{method_name} failed: insufficient cycles \
(available: {}, required: {})",
err.available, err.required,
),
},
}
}
53 changes: 52 additions & 1 deletion rs/nns/handlers/root/interface/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use candid::CandidType;
use ic_base_types::PrincipalId;
use ic_base_types::{CanisterId, PrincipalId};
use ic_nervous_system_clients::update_settings::CanisterSettings;
use serde::Deserialize;

Expand Down Expand Up @@ -94,3 +94,54 @@ pub struct UpdateCanisterSettingsError {
pub code: Option<i32>,
pub description: String,
}

/// Request to create a new canister on a specified subnet and install code into
/// it. The canister is created by NNS Root, which becomes a controller.
#[derive(Clone, PartialEq, Debug, CandidType, Deserialize)]
pub struct CreateCanisterAndInstallCodeRequest {
/// The subnet where the canister will be created.
pub host_subnet_id: PrincipalId,

/// Settings for the new canister. If controllers is not specified, Root
/// will be the sole controller.
pub canister_settings: Option<CanisterSettings>,

/// The WASM module to install.
pub wasm_module: Vec<u8>,

/// The argument to pass to the canister's install handler.
pub install_arg: Vec<u8>,
}

#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize)]
pub enum CreateCanisterAndInstallCodeResponse {
Ok(CreateCanisterAndInstallCodeOk),
Err(CreateCanisterAndInstallCodeError),
}

#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize)]
pub struct CreateCanisterAndInstallCodeOk {
/// The ID of the newly created canister.
pub canister_id: PrincipalId,
}

#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize)]
pub struct CreateCanisterAndInstallCodeError {
pub code: Option<i32>,
pub description: String,
}

impl From<Result<CanisterId, CreateCanisterAndInstallCodeError>>
for CreateCanisterAndInstallCodeResponse
{
fn from(result: Result<CanisterId, CreateCanisterAndInstallCodeError>) -> Self {
match result {
Ok(canister_id) => {
CreateCanisterAndInstallCodeResponse::Ok(CreateCanisterAndInstallCodeOk {
canister_id: canister_id.get(),
})
}
Err(err) => CreateCanisterAndInstallCodeResponse::Err(err),
}
}
}
6 changes: 6 additions & 0 deletions rs/nns/handlers/root/unreleased_changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ on the process that this file is part of, see

## Added

* create_canister_and_install_code method. This is only callable by the Governance
canister though, so this is not really a "new feature" in the sense that others
can call this directly, but it is a new capability of this canister that will
nevertheless indirectly be of use to others (outside of NNS). That will happen
via an upcoming new proposal type (with the same name).

## Changed

## Deprecated
Expand Down
Loading