From 234552b907c5e51c443f62fa53a340aac587c682 Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Mon, 2 Oct 2023 05:21:31 -0700 Subject: [PATCH] feat: add support for reserved_cycles and reserved_cycles_limit (#3404) Adds support for reserved cycles: - `dfx canister status` reports reserved cycles balance and limit - Added `--reserved-cycles-limit` option to `dfx canister create` and `dfx canister update-settings` - Added `reserved_cycles_limit` option for initialization_values that dfx deploy uses when creating canisters. Fixes https://dfinity.atlassian.net/browse/SDK-1243 --- CHANGELOG.md | 10 ++++++ Cargo.lock | 32 +++++++++---------- Cargo.toml | 18 +++++------ docs/cli-reference/dfx-canister.md | 2 ++ docs/dfx-json-schema.json | 14 +++++++- e2e/tests-dfx/create.bash | 15 +++++++++ e2e/tests-dfx/deploy.bash | 13 ++++++++ e2e/tests-dfx/update_settings.bash | 11 +++++++ src/dfx-core/src/config/model/dfinity.rs | 27 +++++++++++++++- src/dfx-core/src/error/dfx_config.rs | 6 ++++ src/dfx/src/commands/canister/create.rs | 32 ++++++++++++++++++- src/dfx/src/commands/canister/delete.rs | 6 +++- src/dfx/src/commands/canister/status.rs | 10 +++++- .../src/commands/canister/update_settings.rs | 28 +++++++++++++++- src/dfx/src/lib/ic_attributes/mod.rs | 24 +++++++++++++- src/dfx/src/lib/migrate.rs | 1 + .../operations/canister/create_canister.rs | 5 +++ .../operations/canister/deploy_canisters.rs | 9 +++++- src/dfx/src/lib/operations/canister/mod.rs | 4 +++ src/dfx/src/util/clap/parsers.rs | 6 ++++ 20 files changed, 240 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 703820bd83..065c77b180 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ # UNRELEASED +### feat: Added support for reserved_cycles and reserved_cycles_limit + +`dfx canister status` will now display the reserved cycles balance and reserved cycles limit for a canister. + +Added command-line options: + - `dfx canister create --reserved-cycles-limit ` + - `dfx canister update-settings --reserved-cycles-limit ` + +In addition, `dfx deploy` will set `reserved_cycles_limit` when creating canisters if specified in `canisters..initialization_values.reserved_cycles_limit` in dfx.json. + ### feat: emit management canister idl when imported by Motoko canister `import management "ic:aaaaa-aa;` diff --git a/Cargo.lock b/Cargo.lock index cae09245f4..36caf124d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1362,7 +1362,7 @@ dependencies = [ "ic-agent", "ic-asset", "ic-identity-hsm", - "ic-utils 0.28.0", + "ic-utils 0.29.0", "ic-wasm", "indicatif", "itertools 0.10.5", @@ -1431,7 +1431,7 @@ dependencies = [ "humantime-serde", "ic-agent", "ic-identity-hsm", - "ic-utils 0.28.0", + "ic-utils 0.29.0", "k256 0.11.6", "keyring", "lazy_static", @@ -2386,8 +2386,8 @@ dependencies = [ [[package]] name = "ic-agent" -version = "0.28.0" -source = "git+https://github.com/dfinity/agent-rs.git?rev=9c4db330d96938d95eda69bbf5878db91aae0aa1#9c4db330d96938d95eda69bbf5878db91aae0aa1" +version = "0.29.0" +source = "git+https://github.com/dfinity/agent-rs.git?rev=b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68#b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68" dependencies = [ "backoff", "candid 0.9.6", @@ -2395,7 +2395,7 @@ dependencies = [ "hex", "http", "http-body", - "ic-certification 0.27.0", + "ic-certification 1.2.0", "ic-transport-types", "ic-verify-bls-signature", "k256 0.13.1", @@ -2433,7 +2433,7 @@ dependencies = [ "globset", "hex", "ic-agent", - "ic-utils 0.28.0", + "ic-utils 0.29.0", "itertools 0.10.5", "json5", "mime", @@ -2532,9 +2532,9 @@ dependencies = [ [[package]] name = "ic-certification" -version = "0.27.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1120544357d4d2f7540dd5290b952c6305afe24c9620d423e2239560adca2535" +checksum = "a59dc342d11b2067e19d0f146bdec3674de921303ffc762f114d201ebbe0e68a" dependencies = [ "hex", "serde", @@ -2752,8 +2752,8 @@ dependencies = [ [[package]] name = "ic-identity-hsm" -version = "0.28.0" -source = "git+https://github.com/dfinity/agent-rs.git?rev=9c4db330d96938d95eda69bbf5878db91aae0aa1#9c4db330d96938d95eda69bbf5878db91aae0aa1" +version = "0.29.0" +source = "git+https://github.com/dfinity/agent-rs.git?rev=b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68#b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68" dependencies = [ "hex", "ic-agent", @@ -2848,12 +2848,12 @@ dependencies = [ [[package]] name = "ic-transport-types" -version = "0.28.0" -source = "git+https://github.com/dfinity/agent-rs.git?rev=9c4db330d96938d95eda69bbf5878db91aae0aa1#9c4db330d96938d95eda69bbf5878db91aae0aa1" +version = "0.29.0" +source = "git+https://github.com/dfinity/agent-rs.git?rev=b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68#b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68" dependencies = [ "candid 0.9.6", "hex", - "ic-certification 0.27.0", + "ic-certification 1.2.0", "leb128", "serde", "serde_bytes", @@ -2923,8 +2923,8 @@ dependencies = [ [[package]] name = "ic-utils" -version = "0.28.0" -source = "git+https://github.com/dfinity/agent-rs.git?rev=9c4db330d96938d95eda69bbf5878db91aae0aa1#9c4db330d96938d95eda69bbf5878db91aae0aa1" +version = "0.29.0" +source = "git+https://github.com/dfinity/agent-rs.git?rev=b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68#b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68" dependencies = [ "async-trait", "candid 0.9.6", @@ -3000,7 +3000,7 @@ dependencies = [ "humantime", "ic-agent", "ic-asset", - "ic-utils 0.28.0", + "ic-utils 0.29.0", "libflate", "num-traits", "pem 1.1.1", diff --git a/Cargo.toml b/Cargo.toml index e0bff1eb37..4cae352f04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,11 @@ license = "Apache-2.0" [workspace.dependencies] candid = "0.9.0" -ic-agent = "0.28.0" +ic-agent = "0.29.0" ic-asset = { path = "src/canisters/frontend/ic-asset" } ic-cdk = "0.10.0" -ic-identity-hsm = "0.28.0" -ic-utils = "0.28.0" +ic-identity-hsm = "0.29.0" +ic-utils = "0.29.0" aes-gcm = "0.9.4" anyhow = "1.0.56" @@ -69,19 +69,19 @@ url = "2.1.0" walkdir = "2.3.2" [patch.crates-io.ic-agent] -version = "0.28.0" +version = "0.29.0" git = "https://github.com/dfinity/agent-rs.git" -rev = "9c4db330d96938d95eda69bbf5878db91aae0aa1" +rev = "b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68" [patch.crates-io.ic-identity-hsm] -version = "0.28.0" +version = "0.29.0" git = "https://github.com/dfinity/agent-rs.git" -rev = "9c4db330d96938d95eda69bbf5878db91aae0aa1" +rev = "b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68" [patch.crates-io.ic-utils] -version = "0.28.0" +version = "0.29.0" git = "https://github.com/dfinity/agent-rs.git" -rev = "9c4db330d96938d95eda69bbf5878db91aae0aa1" +rev = "b91b85b7b6ba6bfaf9dfd616b7c7c8f966bf8f68" [profile.release] panic = 'abort' diff --git a/docs/cli-reference/dfx-canister.md b/docs/cli-reference/dfx-canister.md index 559ef2549f..555b83df78 100644 --- a/docs/cli-reference/dfx-canister.md +++ b/docs/cli-reference/dfx-canister.md @@ -216,6 +216,7 @@ You can use the following options with the `dfx canister create` command. | `-c`, `--compute-allocation ` | Specifies the canister's compute allocation. This should be a percent in the range [0..100]. | | `--controller ` | Specifies the identity name or the principal of the new controller. | | `--memory-allocation ` | Specifies how much memory the canister is allowed to use in total. This should be a value in the range [0..12 GiB]. A setting of 0 means the canister will have access to memory on a “best-effort” basis: It will only be charged for the memory it uses, but at any point in time may stop running if it tries to allocate more memory when there isn’t space available on the subnet. | +| `--reserved-cycles-limit ` | Specifies the upper limit for the canister's reserved cycles. | | `--no-wallet` | Performs the call with the user Identity as the Sender of messages. Bypasses the Wallet canister. Enabled by default. | | `--with-cycles ` | Specifies the initial cycle balance to deposit into the newly created canister. The specified amount needs to take the canister create fee into account. This amount is deducted from the wallet's cycle balance. | | `--specified-id ` | Attempts to create the canister with this Canister ID | @@ -921,6 +922,7 @@ You can specify the following options for the `dfx canister update-settings` com | `-c`, `--compute-allocation ` | Specifies the canister's compute allocation. This should be a percent in the range [0..100]. | | `--set-controller ` | Specifies the identity name or the principal of the new controller. Can be specified more than once, indicating the canister will have multiple controllers. If any controllers are set with this parameter, any other controllers will be removed. | | `--memory-allocation ` | Specifies how much memory the canister is allowed to use in total. This should be a value in the range [0..12 GiB]. A setting of 0 means the canister will have access to memory on a “best-effort” basis: It will only be charged for the memory it uses, but at any point in time may stop running if it tries to allocate more memory when there isn’t space available on the subnet. | +| `--reserved-cycles-limit ` | Specifies the upper limit of the canister's reserved cycles. | | `--remove-controller ` | Removes a principal from the list of controllers of the canister. | | `--freezing-threshold ` | Set the [freezing threshold](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-create_canister) in seconds for a canister. This should be a value in the range [0..2^64^-1]. Very long thresholds require the `--confirm-very-long-freezing-threshold` flag. | | `-y`, `--yes` | Skips yes/no checks by answering 'yes'. Such checks can result in loss of control, so this is not recommended outside of CI. | diff --git a/docs/dfx-json-schema.json b/docs/dfx-json-schema.json index 8d38220a0a..186ca4c6f7 100644 --- a/docs/dfx-json-schema.json +++ b/docs/dfx-json-schema.json @@ -372,7 +372,8 @@ "default": { "compute_allocation": null, "freezing_threshold": null, - "memory_allocation": null + "memory_allocation": null, + "reserved_cycles_limit": null }, "allOf": [ { @@ -848,6 +849,17 @@ ], "format": "uint64", "minimum": 0.0 + }, + "reserved_cycles_limit": { + "title": "Reserved Cycles Limit", + "description": "Specifies the upper limit of the canister's reserved cycles balance.\n\nReserved cycles are cycles that the system sets aside for future use by the canister. If a subnet's storage exceeds 450 GiB, then every time a canister allocates new storage bytes, the system sets aside some amount of cycles from the main balance of the canister. These reserved cycles will be used to cover future payments for the newly allocated bytes. The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is.\n\nA setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds 450 GiB.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint128", + "minimum": 0.0 } } }, diff --git a/e2e/tests-dfx/create.bash b/e2e/tests-dfx/create.bash index a7f04b3ed4..bd16462e84 100644 --- a/e2e/tests-dfx/create.bash +++ b/e2e/tests-dfx/create.bash @@ -14,6 +14,21 @@ teardown() { standard_teardown } +@test "create with reserved cycles limit" { + dfx_start + + assert_command_fail dfx canister create e2e_project_backend --reserved-cycles-limit 470000 + assert_contains "Cannot create a canister using a wallet if the reserved_cycles_limit is set. Please create with --no-wallet or use dfx canister update-settings instead." + + assert_command dfx canister create e2e_project_frontend --no-wallet + assert_command dfx canister status e2e_project_frontend + assert_contains "Reserved Cycles Limit: 5_000_000_000_000 Cycles" + + assert_command dfx canister create e2e_project_backend --reserved-cycles-limit 470000 --no-wallet + assert_command dfx canister status e2e_project_backend + assert_contains "Reserved Cycles Limit: 470_000 Cycles" +} + @test "create succeeds on default project" { dfx_start assert_command dfx canister create --all diff --git a/e2e/tests-dfx/deploy.bash b/e2e/tests-dfx/deploy.bash index 88aa5f05fe..87a69c4a32 100644 --- a/e2e/tests-dfx/deploy.bash +++ b/e2e/tests-dfx/deploy.bash @@ -14,6 +14,19 @@ teardown() { standard_teardown } +@test "deploy with reserved cycles limit" { + dfx_start + cat dfx.json + jq '.canisters.hello_backend.initialization_values.reserved_cycles_limit=860000' dfx.json | sponge dfx.json + assert_command_fail dfx deploy + assert_contains "Cannot create a canister using a wallet if the reserved_cycles_limit is set. Please create with --no-wallet or use dfx canister update-settings instead." + + assert_command dfx deploy --no-wallet + + assert_command dfx canister status hello_backend + assert_contains "Reserved Cycles Limit: 860_000 Cycles" +} + @test "deploy --upgrade-unchanged upgrades even if the .wasm did not change" { dfx_start assert_command dfx deploy diff --git a/e2e/tests-dfx/update_settings.bash b/e2e/tests-dfx/update_settings.bash index 8ad100f00b..54f529948d 100644 --- a/e2e/tests-dfx/update_settings.bash +++ b/e2e/tests-dfx/update_settings.bash @@ -14,6 +14,17 @@ teardown() { standard_teardown } +@test "set reserved cycles limit" { + dfx_start + assert_command dfx deploy hello_backend + assert_command dfx canister status hello_backend + assert_contains "Reserved Cycles Limit: 5_000_000_000_000 Cycles" + + assert_command dfx canister update-settings hello_backend --reserved-cycles-limit 650000 + assert_command dfx canister status hello_backend + assert_contains "Reserved Cycles Limit: 650_000 Cycles" +} + @test "set freezing threshold" { dfx_start assert_command dfx deploy hello_backend diff --git a/src/dfx-core/src/config/model/dfinity.rs b/src/dfx-core/src/config/model/dfinity.rs index 5e233544f5..e67d9a7c38 100644 --- a/src/dfx-core/src/config/model/dfinity.rs +++ b/src/dfx-core/src/config/model/dfinity.rs @@ -10,10 +10,11 @@ use crate::error::dfx_config::GetFreezingThresholdError::GetFreezingThresholdFai use crate::error::dfx_config::GetMemoryAllocationError::GetMemoryAllocationFailed; use crate::error::dfx_config::GetPullCanistersError::PullCanistersSameId; use crate::error::dfx_config::GetRemoteCanisterIdError::GetRemoteCanisterIdFailed; +use crate::error::dfx_config::GetReservedCyclesLimitError::GetReservedCyclesLimitFailed; use crate::error::dfx_config::{ AddDependenciesError, GetCanisterConfigError, GetCanisterNamesWithDependenciesError, GetComputeAllocationError, GetFreezingThresholdError, GetMemoryAllocationError, - GetPullCanistersError, GetRemoteCanisterIdError, + GetPullCanistersError, GetRemoteCanisterIdError, GetReservedCyclesLimitError, }; use crate::error::load_dfx_config::LoadDfxConfigError; use crate::error::load_dfx_config::LoadDfxConfigError::{ @@ -347,6 +348,19 @@ pub struct InitializationValues { #[serde(with = "humantime_serde")] #[schemars(with = "Option")] pub freezing_threshold: Option, + + /// # Reserved Cycles Limit + /// Specifies the upper limit of the canister's reserved cycles balance. + /// + /// Reserved cycles are cycles that the system sets aside for future use by the canister. + /// If a subnet's storage exceeds 450 GiB, then every time a canister allocates new storage bytes, + /// the system sets aside some amount of cycles from the main balance of the canister. + /// These reserved cycles will be used to cover future payments for the newly allocated bytes. + /// The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is. + /// + /// A setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds 450 GiB. + #[schemars(with = "Option")] + pub reserved_cycles_limit: Option, } /// # Declarations Configuration @@ -836,6 +850,17 @@ impl ConfigInterface { .freezing_threshold) } + pub fn get_reserved_cycles_limit( + &self, + canister_name: &str, + ) -> Result, GetReservedCyclesLimitError> { + Ok(self + .get_canister_config(canister_name) + .map_err(|e| GetReservedCyclesLimitFailed(canister_name.to_string(), e))? + .initialization_values + .reserved_cycles_limit) + } + fn get_canister_config( &self, canister_name: &str, diff --git a/src/dfx-core/src/error/dfx_config.rs b/src/dfx-core/src/error/dfx_config.rs index 062ff19a1c..943484f11b 100644 --- a/src/dfx-core/src/error/dfx_config.rs +++ b/src/dfx-core/src/error/dfx_config.rs @@ -40,6 +40,12 @@ pub enum GetFreezingThresholdError { GetFreezingThresholdFailed(String, GetCanisterConfigError), } +#[derive(Error, Debug)] +pub enum GetReservedCyclesLimitError { + #[error("Failed to get reserved cycles limit for canister '{0}': {1}")] + GetReservedCyclesLimitFailed(String, GetCanisterConfigError), +} + #[derive(Error, Debug)] pub enum GetMemoryAllocationError { #[error("Failed to get memory allocation for canister '{0}': {1}")] diff --git a/src/dfx/src/commands/canister/create.rs b/src/dfx/src/commands/canister/create.rs index ee152f6aac..7793120e69 100644 --- a/src/dfx/src/commands/canister/create.rs +++ b/src/dfx/src/commands/canister/create.rs @@ -2,7 +2,8 @@ use crate::lib::deps::get_pull_canisters_in_config; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; use crate::lib::ic_attributes::{ - get_compute_allocation, get_freezing_threshold, get_memory_allocation, CanisterSettings, + get_compute_allocation, get_freezing_threshold, get_memory_allocation, + get_reserved_cycles_limit, CanisterSettings, }; use crate::lib::identity::wallet::get_or_create_wallet_canister; use crate::lib::operations::canister::create_canister; @@ -10,6 +11,7 @@ use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::clap::parsers::cycle_amount_parser; use crate::util::clap::parsers::{ compute_allocation_parser, freezing_threshold_parser, memory_allocation_parser, + reserved_cycles_limit_parser, }; use anyhow::{bail, Context}; use byte_unit::Byte; @@ -62,6 +64,18 @@ pub struct CanisterCreateOpts { #[arg(long, value_parser = freezing_threshold_parser, hide = true)] freezing_threshold: Option, + /// Specifies the upper limit of the canister's reserved cycles balance. + /// + /// Reserved cycles are cycles that the system sets aside for future use by the canister. + /// If a subnet's storage exceeds 450 GiB, then every time a canister allocates new storage bytes, + /// the system sets aside some amount of cycles from the main balance of the canister. + /// These reserved cycles will be used to cover future payments for the newly allocated bytes. + /// The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is. + /// + /// A setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds 450 GiB. + #[arg(long, value_parser = reserved_cycles_limit_parser, hide = true)] + reserved_cycles_limit: Option, + /// Performs the call with the user Identity as the Sender of messages. /// Bypasses the Wallet canister. #[arg(long)] @@ -160,6 +174,12 @@ pub async fn exec( Some(canister_name), ) .with_context(|| format!("Failed to read freezing threshold of {}.", canister_name))?; + let reserved_cycles_limit = get_reserved_cycles_limit( + opts.reserved_cycles_limit, + Some(config_interface), + Some(canister_name), + ) + .with_context(|| format!("Failed to read reserved cycles limit of {}.", canister_name))?; create_canister( env, canister_name, @@ -171,6 +191,7 @@ pub async fn exec( compute_allocation, memory_allocation, freezing_threshold, + reserved_cycles_limit, }, ) .await?; @@ -219,6 +240,14 @@ pub async fn exec( .with_context(|| { format!("Failed to read freezing threshold of {}.", canister_name) })?; + let reserved_cycles_limit = get_reserved_cycles_limit( + opts.reserved_cycles_limit, + Some(config_interface), + Some(canister_name), + ) + .with_context(|| { + format!("Failed to read reserved cycles limit of {}.", canister_name) + })?; create_canister( env, canister_name, @@ -230,6 +259,7 @@ pub async fn exec( compute_allocation, memory_allocation, freezing_threshold, + reserved_cycles_limit, }, ) .await?; diff --git a/src/dfx/src/commands/canister/delete.rs b/src/dfx/src/commands/canister/delete.rs index 1f4e5eff29..88c2b0de58 100644 --- a/src/dfx/src/commands/canister/delete.rs +++ b/src/dfx/src/commands/canister/delete.rs @@ -19,7 +19,7 @@ use dfx_core::identity::CallSender; use fn_error_context::context; use ic_utils::call::AsyncCall; use ic_utils::interfaces::management_canister::attributes::{ - ComputeAllocation, FreezingThreshold, MemoryAllocation, + ComputeAllocation, FreezingThreshold, MemoryAllocation, ReservedCyclesLimit, }; use ic_utils::interfaces::management_canister::builders::InstallMode; use ic_utils::interfaces::management_canister::CanisterStatus; @@ -36,6 +36,7 @@ const DANK_PRINCIPAL: Principal = // "Couldn't send message" when deleting a canister: increase WITHDRAWAL_COST const WITHDRAWAL_COST: u128 = 10_606_030_000; // 5% higher than a value observed ok locally const MAX_MEMORY_ALLOCATION: u64 = 8589934592; +const DEFAULT_RESERVED_CYCLES_LIMIT: u128 = 5_000_000_000_000; /// Deletes a currently stopped canister. #[derive(Parser)] @@ -161,6 +162,9 @@ async fn delete_canister( compute_allocation: Some(ComputeAllocation::try_from(0u8).unwrap()), memory_allocation: Some(MemoryAllocation::try_from(MAX_MEMORY_ALLOCATION).unwrap()), freezing_threshold: Some(FreezingThreshold::try_from(0u8).unwrap()), + reserved_cycles_limit: Some( + ReservedCyclesLimit::try_from(DEFAULT_RESERVED_CYCLES_LIMIT).unwrap(), + ), }; info!(log, "Setting the controller to identity principal."); update_settings(env, canister_id, settings, call_sender).await?; diff --git a/src/dfx/src/commands/canister/status.rs b/src/dfx/src/commands/canister/status.rs index 0d08ac04c9..bfa2a4787d 100644 --- a/src/dfx/src/commands/canister/status.rs +++ b/src/dfx/src/commands/canister/status.rs @@ -41,7 +41,13 @@ async fn canister_status( .collect(); controllers.sort(); - info!(log, "Canister status call result for {}.\nStatus: {}\nControllers: {}\nMemory allocation: {}\nCompute allocation: {}\nFreezing threshold: {}\nMemory Size: {:?}\nBalance: {} Cycles\nModule hash: {}", + let reserved_cycles_limit = if let Some(limit) = status.settings.reserved_cycles_limit { + format!("{} Cycles", limit) + } else { + "Not Set".to_string() + }; + + info!(log, "Canister status call result for {}.\nStatus: {}\nControllers: {}\nMemory allocation: {}\nCompute allocation: {}\nFreezing threshold: {}\nMemory Size: {:?}\nBalance: {} Cycles\nReserved: {} Cycles\nReserved Cycles Limit: {}\nModule hash: {}", canister, status.status, controllers.join(" "), @@ -50,6 +56,8 @@ async fn canister_status( status.settings.freezing_threshold, status.memory_size, status.cycles, + status.reserved_cycles, + reserved_cycles_limit, status.module_hash.map_or_else(|| "None".to_string(), |v| format!("0x{}", hex::encode(v))) ); Ok(()) diff --git a/src/dfx/src/commands/canister/update_settings.rs b/src/dfx/src/commands/canister/update_settings.rs index b19f846991..b06fae6ae9 100644 --- a/src/dfx/src/commands/canister/update_settings.rs +++ b/src/dfx/src/commands/canister/update_settings.rs @@ -2,12 +2,14 @@ use crate::lib::diagnosis::DiagnosedError; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; use crate::lib::ic_attributes::{ - get_compute_allocation, get_freezing_threshold, get_memory_allocation, CanisterSettings, + get_compute_allocation, get_freezing_threshold, get_memory_allocation, + get_reserved_cycles_limit, CanisterSettings, }; use crate::lib::operations::canister::{get_canister_status, update_settings}; use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::clap::parsers::{ compute_allocation_parser, freezing_threshold_parser, memory_allocation_parser, + reserved_cycles_limit_parser, }; use anyhow::{bail, Context}; use byte_unit::Byte; @@ -62,6 +64,18 @@ pub struct UpdateSettingsOpts { #[arg(long, value_parser = freezing_threshold_parser)] freezing_threshold: Option, + /// Sets the upper limit of the canister's reserved cycles balance. + /// + /// Reserved cycles are cycles that the system sets aside for future use by the canister. + /// If a subnet's storage exceeds 450 GiB, then every time a canister allocates new storage bytes, + /// the system sets aside some amount of cycles from the main balance of the canister. + /// These reserved cycles will be used to cover future payments for the newly allocated bytes. + /// The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is. + /// + /// A setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds 450 GiB. + #[arg(long, value_parser = reserved_cycles_limit_parser)] + reserved_cycles_limit: Option, + /// Freezing thresholds above ~1.5 years require this flag as confirmation. #[arg(long)] confirm_very_long_freezing_threshold: bool, @@ -122,6 +136,8 @@ pub async fn exec( get_memory_allocation(opts.memory_allocation, config_interface, canister_name)?; let freezing_threshold = get_freezing_threshold(opts.freezing_threshold, config_interface, canister_name)?; + let reserved_cycles_limit = + get_reserved_cycles_limit(opts.reserved_cycles_limit, config_interface, canister_name)?; if let Some(added) = &opts.add_controller { let status = get_canister_status(env, canister_id, call_sender).await?; let mut existing_controllers = status.settings.controllers; @@ -153,6 +169,7 @@ pub async fn exec( compute_allocation, memory_allocation, freezing_threshold, + reserved_cycles_limit, }; update_settings(env, canister_id, settings, call_sender).await?; display_controller_update(&opts, canister_name_or_id); @@ -188,6 +205,14 @@ pub async fn exec( .with_context(|| { format!("Failed to get freezing threshold for {}.", canister_name) })?; + let reserved_cycles_limit = get_reserved_cycles_limit( + opts.reserved_cycles_limit, + Some(config_interface), + Some(canister_name), + ) + .with_context(|| { + format!("Failed to get reserved cycles limit for {}.", canister_name) + })?; if let Some(added) = &opts.add_controller { let status = get_canister_status(env, canister_id, call_sender).await?; let mut existing_controllers = status.settings.controllers; @@ -219,6 +244,7 @@ pub async fn exec( compute_allocation, memory_allocation, freezing_threshold, + reserved_cycles_limit, }; update_settings(env, canister_id, settings, call_sender).await?; display_controller_update(&opts, canister_name); diff --git a/src/dfx/src/lib/ic_attributes/mod.rs b/src/dfx/src/lib/ic_attributes/mod.rs index 77e0cea05f..09d495338e 100644 --- a/src/dfx/src/lib/ic_attributes/mod.rs +++ b/src/dfx/src/lib/ic_attributes/mod.rs @@ -5,7 +5,7 @@ use candid::Principal; use dfx_core::config::model::dfinity::ConfigInterface; use fn_error_context::context; use ic_utils::interfaces::management_canister::attributes::{ - ComputeAllocation, FreezingThreshold, MemoryAllocation, + ComputeAllocation, FreezingThreshold, MemoryAllocation, ReservedCyclesLimit, }; use std::convert::TryFrom; @@ -15,6 +15,7 @@ pub struct CanisterSettings { pub compute_allocation: Option, pub memory_allocation: Option, pub freezing_threshold: Option, + pub reserved_cycles_limit: Option, } #[context("Failed to get compute allocation.")] @@ -80,3 +81,24 @@ pub fn get_freezing_threshold( }) .transpose() } + +#[context("Failed to get reserved cycles limit")] +pub fn get_reserved_cycles_limit( + reserved_cycles_limit: Option, + config_interface: Option<&ConfigInterface>, + canister_name: Option<&str>, +) -> DfxResult> { + let reserved_cycles_limit = match (reserved_cycles_limit, config_interface, canister_name) { + (Some(reserved_cycles_limit), _, _) => Some(reserved_cycles_limit), + (None, Some(config_interface), Some(canister_name)) => { + config_interface.get_reserved_cycles_limit(canister_name)? + } + _ => None, + }; + reserved_cycles_limit + .map(|arg| { + ReservedCyclesLimit::try_from(arg) + .context("Must be a limit between 0 and 2^128-1 inclusive.") + }) + .transpose() +} diff --git a/src/dfx/src/lib/migrate.rs b/src/dfx/src/lib/migrate.rs index 62845ab4e0..18d7710868 100644 --- a/src/dfx/src/lib/migrate.rs +++ b/src/dfx/src/lib/migrate.rs @@ -114,6 +114,7 @@ async fn migrate_canister( compute_allocation: None, freezing_threshold: None, memory_allocation: None, + reserved_cycles_limit: None, }, },)), 0, diff --git a/src/dfx/src/lib/operations/canister/create_canister.rs b/src/dfx/src/lib/operations/canister/create_canister.rs index 1d6ee09171..c093613da6 100644 --- a/src/dfx/src/lib/operations/canister/create_canister.rs +++ b/src/dfx/src/lib/operations/canister/create_canister.rs @@ -121,6 +121,7 @@ async fn create_with_management_canister( .with_optional_compute_allocation(settings.compute_allocation) .with_optional_memory_allocation(settings.memory_allocation) .with_optional_freezing_threshold(settings.freezing_threshold) + .with_optional_reserved_cycles_limit(settings.reserved_cycles_limit) .call_and_wait() .await; const NEEDS_WALLET: &str = "In order to create a canister on this network, you must use a wallet in order to allocate cycles to the new canister. \ @@ -152,6 +153,10 @@ async fn create_with_wallet( ) -> DfxResult { let wallet = build_wallet_canister(*wallet_id, agent).await?; let cycles = with_cycles.unwrap_or(CANISTER_CREATE_FEE + CANISTER_INITIAL_CYCLE_BALANCE); + if settings.reserved_cycles_limit.is_some() { + bail!( + "Cannot create a canister using a wallet if the reserved_cycles_limit is set. Please create with --no-wallet or use dfx canister update-settings instead.") + } match wallet .wallet_create_canister( cycles, diff --git a/src/dfx/src/lib/operations/canister/deploy_canisters.rs b/src/dfx/src/lib/operations/canister/deploy_canisters.rs index 7984f6cb76..fb1fccb2d1 100644 --- a/src/dfx/src/lib/operations/canister/deploy_canisters.rs +++ b/src/dfx/src/lib/operations/canister/deploy_canisters.rs @@ -20,7 +20,7 @@ use dfx_core::config::model::dfinity::Config; use dfx_core::identity::CallSender; use fn_error_context::context; use ic_utils::interfaces::management_canister::attributes::{ - ComputeAllocation, FreezingThreshold, MemoryAllocation, + ComputeAllocation, FreezingThreshold, MemoryAllocation, ReservedCyclesLimit, }; use ic_utils::interfaces::management_canister::builders::InstallMode; use slog::info; @@ -242,6 +242,12 @@ async fn register_canisters( FreezingThreshold::try_from(arg.as_secs()) .expect("Freezing threshold must be between 0 and 2^64-1, inclusively.") }); + let reserved_cycles_limit = config_interface + .get_reserved_cycles_limit(canister_name)? + .map(|arg| { + ReservedCyclesLimit::try_from(arg) + .expect("Reserved cycles limit must be between 0 and 2^128-1, inclusively.") + }); let controllers = None; create_canister( env, @@ -254,6 +260,7 @@ async fn register_canisters( compute_allocation, memory_allocation, freezing_threshold, + reserved_cycles_limit, }, ) .await?; diff --git a/src/dfx/src/lib/operations/canister/mod.rs b/src/dfx/src/lib/operations/canister/mod.rs index 1107c293b3..767182a021 100644 --- a/src/dfx/src/lib/operations/canister/mod.rs +++ b/src/dfx/src/lib/operations/canister/mod.rs @@ -183,6 +183,10 @@ pub async fn update_settings( .freezing_threshold .map(u64::from) .map(candid::Nat::from), + reserved_cycles_limit: settings + .reserved_cycles_limit + .map(u128::from) + .map(candid::Nat::from), }, }, call_sender, diff --git a/src/dfx/src/util/clap/parsers.rs b/src/dfx/src/util/clap/parsers.rs index e8b5f8360c..290e3da662 100644 --- a/src/dfx/src/util/clap/parsers.rs +++ b/src/dfx/src/util/clap/parsers.rs @@ -84,6 +84,12 @@ pub fn freezing_threshold_parser(freezing_threshold: &str) -> Result Result { + reserved_cycles_limit + .parse::() + .map_err(|_| "Must be a value between 0 and 2^128-1 inclusive".to_string()) +} + /// Validate a String can be a valid project name. /// A project name is valid if it starts with a letter, and is alphanumeric (with hyphens). /// It cannot end with a dash.