Skip to content

Commit

Permalink
feat: Add support for reserved_cycles and reserved_cycles_limit (#475)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericswanson-dfinity committed Sep 29, 2023
1 parent 1fef402 commit f73babd
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ic-ref.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion ic-utils/src/interfaces/management_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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<Nat>,
}

impl std::fmt::Display for StatusCallResult {
Expand Down
91 changes: 91 additions & 0 deletions ic-utils/src/interfaces/management_canister/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReservedCyclesLimit> 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<Self, Self::Error> {
#[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() {
Expand Down Expand Up @@ -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();
}
89 changes: 86 additions & 3 deletions ic-utils/src/interfaces/management_canister/builders.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<Nat>,

/// 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<Nat>,
}

/// A builder for a `create_canister` call.
Expand All @@ -47,6 +62,7 @@ pub struct CreateCanisterBuilder<'agent, 'canister: 'agent> {
compute_allocation: Option<Result<ComputeAllocation, AgentError>>,
memory_allocation: Option<Result<MemoryAllocation, AgentError>>,
freezing_threshold: Option<Result<FreezingThreshold, AgentError>>,
reserved_cycles_limit: Option<Result<ReservedCyclesLimit, AgentError>>,
is_provisional_create: bool,
amount: Option<u128>,
specified_id: Option<Principal>,
Expand All @@ -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,
Expand Down Expand Up @@ -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<C, E>(self, limit: C) -> Self
where
E: std::fmt::Display,
C: TryInto<ReservedCyclesLimit, Error = E>,
{
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<E, C>(self, limit: Option<C>) -> Self
where
E: std::fmt::Display,
C: TryInto<ReservedCyclesLimit, Error = E>,
{
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<impl 'agent + AsyncCall<(Principal,)>, AgentError> {
Expand All @@ -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 {
Expand All @@ -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,
};
Expand All @@ -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)
};
Expand Down Expand Up @@ -466,6 +516,7 @@ pub struct UpdateCanisterBuilder<'agent, 'canister: 'agent> {
compute_allocation: Option<Result<ComputeAllocation, AgentError>>,
memory_allocation: Option<Result<MemoryAllocation, AgentError>>,
freezing_threshold: Option<Result<FreezingThreshold, AgentError>>,
reserved_cycles_limit: Option<Result<ReservedCyclesLimit, AgentError>>,
}

impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
Expand All @@ -478,6 +529,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
compute_allocation: None,
memory_allocation: None,
freezing_threshold: None,
reserved_cycles_limit: None,
}
}

Expand Down Expand Up @@ -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<C, E>(self, limit: C) -> Self
where
E: std::fmt::Display,
C: TryInto<ReservedCyclesLimit, Error = E>,
{
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<E, C>(self, limit: Option<C>) -> Self
where
E: std::fmt::Display,
C: TryInto<ReservedCyclesLimit, Error = E>,
{
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<impl 'agent + AsyncCall<()>, AgentError> {
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions ic-utils/src/interfaces/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
Loading

0 comments on commit f73babd

Please sign in to comment.