diff --git a/.github/workflows/ic-ref.yml b/.github/workflows/ic-ref.yml index 2afb36ea..a292aea2 100644 --- a/.github/workflows/ic-ref.yml +++ b/.github/workflows/ic-ref.yml @@ -31,7 +31,7 @@ jobs: - name: Install dfx uses: dfinity/setup-dfx@main with: - dfx-version: "0.15.0" + dfx-version: "0.15.1-beta.0" - name: Cargo cache uses: actions/cache@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e0e0ac..8b59ac82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* Added `reserved_cycles_limit` to canister creation and canister setting update options. +* Added `reserved_cycles` and `reserved_cycles_limit` to canister status call result. + ## [0.28.0] - 2023-09-21 * Added `DelegatedIdentity`, an `Identity` implementation for consuming delegations such as those from Internet Identity. diff --git a/ic-utils/src/interfaces/management_canister.rs b/ic-utils/src/interfaces/management_canister.rs index f56d2e8c..408b7a8c 100644 --- a/ic-utils/src/interfaces/management_canister.rs +++ b/ic-utils/src/interfaces/management_canister.rs @@ -74,7 +74,7 @@ impl<'agent> ManagementCanister<'agent> { /// The complete canister status information of a canister. This includes /// the CanisterStatus, a hash of the module installed on the canister (None if nothing installed), -/// the contoller of the canister, the canisters memory size, and its balance in cycles. +/// the controller of the canister, the canister's memory size, and its balance in cycles. #[derive(Clone, Debug, Deserialize, CandidType)] pub struct StatusCallResult { /// The status of the canister. @@ -87,6 +87,8 @@ pub struct StatusCallResult { pub memory_size: Nat, /// The canister's cycle balance. pub cycles: Nat, + /// The canister's reserved cycles balance. + pub reserved_cycles: Nat, } /// The concrete settings of a canister. @@ -100,6 +102,8 @@ pub struct DefiniteCanisterSettings { pub memory_allocation: Nat, /// The IC will freeze a canister protectively if it will likely run out of cycles before this amount of time, in seconds (up to `u64::MAX`), has passed. pub freezing_threshold: Nat, + /// The upper limit of the canister's reserved cycles balance. + pub reserved_cycles_limit: Option, } impl std::fmt::Display for StatusCallResult { diff --git a/ic-utils/src/interfaces/management_canister/attributes.rs b/ic-utils/src/interfaces/management_canister/attributes.rs index 9b40af5c..2b2edf68 100644 --- a/ic-utils/src/interfaces/management_canister/attributes.rs +++ b/ic-utils/src/interfaces/management_canister/attributes.rs @@ -147,6 +147,66 @@ try_from_freezing_threshold_decl!(i64); try_from_freezing_threshold_decl!(i128); try_from_freezing_threshold_decl!(u128); +/// An error encountered when attempting to construct a [`ReservedCyclesLimit`]. +#[derive(Error, Debug)] +pub enum ReservedCyclesLimitError { + /// The provided value was not in the range [0, 2^128-1]. + #[error("ReservedCyclesLimit must be between 0 and 2^128-1, inclusively. Got {0}.")] + InvalidReservedCyclesLimit(i128), +} + +/// A reserved cycles limit for a canister. Can be anywhere from 0 to 2^128-1 inclusive. +/// +/// This represents the upper limit of reserved_cycles for the canister. +/// +/// 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 reserved cycles limit of 0 disables the reservation mechanism for the canister. +/// If so disabled, the canister will trap when it tries to allocate storage, if the subnet's usage exceeds 450 GiB. +#[derive(Copy, Clone, Debug)] +pub struct ReservedCyclesLimit(u128); + +impl std::convert::From for u128 { + fn from(reserved_cycles_limit: ReservedCyclesLimit) -> Self { + reserved_cycles_limit.0 + } +} + +#[allow(unused_comparisons)] +macro_rules! try_from_reserved_cycles_limit_decl { + ( $t: ty ) => { + impl std::convert::TryFrom<$t> for ReservedCyclesLimit { + type Error = ReservedCyclesLimitError; + + fn try_from(value: $t) -> Result { + #[allow(unused_comparisons)] + if value < 0 { + Err(ReservedCyclesLimitError::InvalidReservedCyclesLimit( + value as i128, + )) + } else { + Ok(Self(value as u128)) + } + } + } + }; +} + +try_from_reserved_cycles_limit_decl!(u8); +try_from_reserved_cycles_limit_decl!(u16); +try_from_reserved_cycles_limit_decl!(u32); +try_from_reserved_cycles_limit_decl!(u64); +try_from_reserved_cycles_limit_decl!(i8); +try_from_reserved_cycles_limit_decl!(i16); +try_from_reserved_cycles_limit_decl!(i32); +try_from_reserved_cycles_limit_decl!(i64); +try_from_reserved_cycles_limit_decl!(i128); +try_from_reserved_cycles_limit_decl!(u128); + #[test] #[allow(clippy::useless_conversion)] fn can_convert_compute_allocation() { @@ -205,3 +265,34 @@ fn can_convert_freezing_threshold() { let ft = FreezingThreshold(100); let _ft_ft: FreezingThreshold = FreezingThreshold::try_from(ft).unwrap(); } + +#[test] +#[allow(clippy::useless_conversion)] +fn can_convert_reserved_cycles_limit() { + use std::convert::{TryFrom, TryInto}; + + // This is more of a compiler test than an actual test. + let _ft_u8: ReservedCyclesLimit = 1u8.try_into().unwrap(); + let _ft_u16: ReservedCyclesLimit = 1u16.try_into().unwrap(); + let _ft_u32: ReservedCyclesLimit = 1u32.try_into().unwrap(); + let _ft_u64: ReservedCyclesLimit = 1u64.try_into().unwrap(); + let _ft_i8: ReservedCyclesLimit = 1i8.try_into().unwrap(); + let _ft_i16: ReservedCyclesLimit = 1i16.try_into().unwrap(); + let _ft_i32: ReservedCyclesLimit = 1i32.try_into().unwrap(); + let _ft_i64: ReservedCyclesLimit = 1i64.try_into().unwrap(); + let _ft_u128: ReservedCyclesLimit = 1i128.try_into().unwrap(); + let _ft_i128: ReservedCyclesLimit = 1u128.try_into().unwrap(); + + assert!(matches!( + ReservedCyclesLimit::try_from(-4).unwrap_err(), + ReservedCyclesLimitError::InvalidReservedCyclesLimit(-4) + )); + + assert_eq!( + ReservedCyclesLimit::try_from(2u128.pow(127) + 6).unwrap().0, + 170141183460469231731687303715884105734u128 + ); + + let ft = ReservedCyclesLimit(100); + let _ft_ft: ReservedCyclesLimit = ReservedCyclesLimit::try_from(ft).unwrap(); +} diff --git a/ic-utils/src/interfaces/management_canister/builders.rs b/ic-utils/src/interfaces/management_canister/builders.rs index 62438ce5..618e72a7 100644 --- a/ic-utils/src/interfaces/management_canister/builders.rs +++ b/ic-utils/src/interfaces/management_canister/builders.rs @@ -1,15 +1,16 @@ //! Builder interfaces for some method calls of the management canister. +pub use super::attributes::{ + ComputeAllocation, FreezingThreshold, MemoryAllocation, ReservedCyclesLimit, +}; use crate::{ call::AsyncCall, canister::Argument, interfaces::management_canister::MgmtMethod, Canister, }; use async_trait::async_trait; use candid::{CandidType, Deserialize, Nat}; use ic_agent::{export::Principal, AgentError, RequestId}; -use std::str::FromStr; - -pub use super::attributes::{ComputeAllocation, FreezingThreshold, MemoryAllocation}; use std::convert::{From, TryInto}; +use std::str::FromStr; /// The set of possible canister settings. Similar to [`DefiniteCanisterSettings`](super::DefiniteCanisterSettings), /// but all the fields are optional. @@ -36,6 +37,20 @@ pub struct CanisterSettings { /// /// If unspecified and a canister is being created with these settings, defaults to 2592000, i.e. ~30 days. pub freezing_threshold: Option, + + /// The upper limit of reserved_cycles for the canister. + /// + /// 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. + /// + /// If unspecified and a canister is being created with these settings, defaults to 5T cycles. + /// + /// If set to 0, disables the reservation mechanism for the canister. + /// Doing so will cause the canister to trap when it tries to allocate storage, if the subnet's usage exceeds 450 GiB. + pub reserved_cycles_limit: Option, } /// A builder for a `create_canister` call. @@ -47,6 +62,7 @@ pub struct CreateCanisterBuilder<'agent, 'canister: 'agent> { compute_allocation: Option>, memory_allocation: Option>, freezing_threshold: Option>, + reserved_cycles_limit: Option>, is_provisional_create: bool, amount: Option, specified_id: Option, @@ -62,6 +78,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> { compute_allocation: None, memory_allocation: None, freezing_threshold: None, + reserved_cycles_limit: None, is_provisional_create: false, amount: None, specified_id: None, @@ -224,6 +241,32 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> { self.with_optional_freezing_threshold(Some(freezing_threshold)) } + /// Pass in a reserved cycles limit value for the canister. + pub fn with_reserved_cycles_limit(self, limit: C) -> Self + where + E: std::fmt::Display, + C: TryInto, + { + self.with_optional_reserved_cycles_limit(Some(limit)) + } + + /// Pass in a reserved cycles limit optional value for the canister. If this is [None], + /// it will create the canister with the default limit. + pub fn with_optional_reserved_cycles_limit(self, limit: Option) -> Self + where + E: std::fmt::Display, + C: TryInto, + { + Self { + reserved_cycles_limit: limit.map(|limit| { + limit + .try_into() + .map_err(|e| AgentError::MessageError(format!("{}", e))) + }), + ..self + } + } + /// Create an [AsyncCall] implementation that, when called, will create a /// canister. pub fn build(self) -> Result, AgentError> { @@ -247,6 +290,11 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> { Some(Ok(x)) => Some(Nat::from(u64::from(x))), None => None, }; + let reserved_cycles_limit = match self.reserved_cycles_limit { + Some(Err(x)) => return Err(AgentError::MessageError(format!("{}", x))), + Some(Ok(x)) => Some(Nat::from(u128::from(x))), + None => None, + }; #[derive(Deserialize, CandidType)] struct Out { @@ -267,6 +315,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> { compute_allocation, memory_allocation, freezing_threshold, + reserved_cycles_limit, }, specified_id: self.specified_id, }; @@ -282,6 +331,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> { compute_allocation, memory_allocation, freezing_threshold, + reserved_cycles_limit, }) .with_effective_canister_id(self.effective_canister_id) }; @@ -466,6 +516,7 @@ pub struct UpdateCanisterBuilder<'agent, 'canister: 'agent> { compute_allocation: Option>, memory_allocation: Option>, freezing_threshold: Option>, + reserved_cycles_limit: Option>, } impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> { @@ -478,6 +529,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> { compute_allocation: None, memory_allocation: None, freezing_threshold: None, + reserved_cycles_limit: None, } } @@ -595,6 +647,31 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> { self.with_optional_freezing_threshold(Some(freezing_threshold)) } + /// Pass in a reserved cycles limit value for the canister. + pub fn with_reserved_cycles_limit(self, limit: C) -> Self + where + E: std::fmt::Display, + C: TryInto, + { + self.with_optional_reserved_cycles_limit(Some(limit)) + } + + /// Pass in a reserved cycles limit optional value for the canister. + /// If this is [None], leaves the reserved cycles limit unchanged. + pub fn with_optional_reserved_cycles_limit(self, limit: Option) -> Self + where + E: std::fmt::Display, + C: TryInto, + { + Self { + reserved_cycles_limit: limit.map(|ma| { + ma.try_into() + .map_err(|e| AgentError::MessageError(format!("{}", e))) + }), + ..self + } + } + /// Create an [AsyncCall] implementation that, when called, will update a /// canisters settings. pub fn build(self) -> Result, AgentError> { @@ -624,6 +701,11 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> { Some(Ok(x)) => Some(Nat::from(u64::from(x))), None => None, }; + let reserved_cycles_limit = match self.reserved_cycles_limit { + Some(Err(x)) => return Err(AgentError::MessageError(format!("{}", x))), + Some(Ok(x)) => Some(Nat::from(u128::from(x))), + None => None, + }; Ok(self .canister @@ -635,6 +717,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> { compute_allocation, memory_allocation, freezing_threshold, + reserved_cycles_limit, }, }) .with_effective_canister_id(self.canister_id) diff --git a/ic-utils/src/interfaces/wallet.rs b/ic-utils/src/interfaces/wallet.rs index 87b5cbb6..c3de2076 100644 --- a/ic-utils/src/interfaces/wallet.rs +++ b/ic-utils/src/interfaces/wallet.rs @@ -685,6 +685,7 @@ impl<'agent> WalletCanister<'agent> { compute_allocation: compute_allocation.map(u8::from).map(Nat::from), memory_allocation: memory_allocation.map(u64::from).map(Nat::from), freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from), + reserved_cycles_limit: None, }; self.update("wallet_create_canister") @@ -713,6 +714,7 @@ impl<'agent> WalletCanister<'agent> { compute_allocation: compute_allocation.map(u8::from).map(Nat::from), memory_allocation: memory_allocation.map(u64::from).map(Nat::from), freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from), + reserved_cycles_limit: None, }; self.update("wallet_create_canister128") @@ -722,6 +724,10 @@ impl<'agent> WalletCanister<'agent> { } /// Create a canister through the wallet. + /// + /// This method does not have a `reserved_cycles_limit` parameter, + /// as the wallet does not support the setting. If you need to create a canister + /// with a `reserved_cycles_limit` set, use the management canister. pub async fn wallet_create_canister<'canister: 'agent>( &'canister self, cycles: u128, @@ -831,6 +837,7 @@ impl<'agent> WalletCanister<'agent> { compute_allocation: compute_allocation.map(u8::from).map(Nat::from), memory_allocation: memory_allocation.map(u64::from).map(Nat::from), freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from), + reserved_cycles_limit: None, }; self.update("wallet_create_wallet") @@ -859,6 +866,7 @@ impl<'agent> WalletCanister<'agent> { compute_allocation: compute_allocation.map(u8::from).map(Nat::from), memory_allocation: memory_allocation.map(u64::from).map(Nat::from), freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from), + reserved_cycles_limit: None, }; self.update("wallet_create_wallet128") diff --git a/ref-tests/tests/ic-ref.rs b/ref-tests/tests/ic-ref.rs index 963c63c8..d36bc286 100644 --- a/ref-tests/tests/ic-ref.rs +++ b/ref-tests/tests/ic-ref.rs @@ -633,6 +633,7 @@ mod management_canister { compute_allocation: None, memory_allocation: None, freezing_threshold: None, + reserved_cycles_limit: None, }, }; @@ -867,6 +868,7 @@ mod extras { .with_compute_allocation(1_u64) .with_memory_allocation(1024 * 1024_u64) .with_freezing_threshold(1_000_000_u64) + .with_reserved_cycles_limit(2_500_800_000_000u128) .call_and_wait() .await?; @@ -880,6 +882,10 @@ mod extras { result.0.settings.freezing_threshold, Nat::from(1_000_000_u64) ); + assert_eq!( + result.0.settings.reserved_cycles_limit, + Some(Nat::from(2_500_800_000_000u128)) + ); Ok(()) }) @@ -952,6 +958,78 @@ mod extras { }) } + #[ignore] + #[test] + fn create_with_reserved_cycles_limit() { + with_agent(|agent| async move { + let ic00 = ManagementCanister::create(&agent); + + let (canister_id,) = ic00 + .create_canister() + .as_provisional_create_with_amount(None) + .with_effective_canister_id(get_effective_canister_id()) + .with_reserved_cycles_limit(2u128.pow(70)) + .call_and_wait() + .await + .unwrap(); + + let result = ic00.canister_status(&canister_id).call_and_wait().await?; + assert_eq!( + result.0.settings.reserved_cycles_limit, + Some(Nat::from(2u128.pow(70))) + ); + + Ok(()) + }) + } + + #[ignore] + #[test] + fn update_reserved_cycles_limit() { + with_agent(|agent| async move { + let ic00 = ManagementCanister::create(&agent); + + let (canister_id,) = ic00 + .create_canister() + .as_provisional_create_with_amount(Some(20_000_000_000_000_u128)) + .with_effective_canister_id(get_effective_canister_id()) + .with_reserved_cycles_limit(2_500_800_000_000u128) + .call_and_wait() + .await?; + + let result = ic00.canister_status(&canister_id).call_and_wait().await?; + assert_eq!( + result.0.settings.reserved_cycles_limit, + Some(Nat::from(2_500_800_000_000u128)) + ); + + ic00.update_settings(&canister_id) + .with_reserved_cycles_limit(3_400_200_000_000u128) + .call_and_wait() + .await?; + + let result = ic00.canister_status(&canister_id).call_and_wait().await?; + assert_eq!( + result.0.settings.reserved_cycles_limit, + Some(Nat::from(3_400_200_000_000u128)) + ); + + let no_change: Option = None; + ic00.update_settings(&canister_id) + .with_optional_reserved_cycles_limit(no_change) + .call_and_wait() + .await?; + + let result = ic00.canister_status(&canister_id).call_and_wait().await?; + assert_eq!( + result.0.settings.reserved_cycles_limit, + Some(Nat::from(3_400_200_000_000u128)) + ); + + Ok(()) + }) + } + #[ignore] #[test] fn specified_id() { diff --git a/ref-tests/tests/integration.rs b/ref-tests/tests/integration.rs index 1bf9b015..1c4b20b4 100644 --- a/ref-tests/tests/integration.rs +++ b/ref-tests/tests/integration.rs @@ -334,6 +334,7 @@ fn wallet_create_wallet() { compute_allocation: None, memory_allocation: None, freezing_threshold: None, + reserved_cycles_limit: None, }, }; let args = Argument::from_candid((create_args,));