use crate::{
cdk::{
candid::{CandidType, Principal},
types::Cycles,
},
ids::CanisterRole,
};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
mod defaults {
use super::Cycles;
use crate::cdk::types::TC;
pub const fn initial_cycles() -> Cycles {
Cycles::new(5_000_000_000_000)
}
pub const fn topup_threshold() -> Cycles {
Cycles::new(10 * TC)
}
pub const fn topup_amount() -> Cycles {
Cycles::new(5 * TC)
}
pub const fn cycles_funding_max_per_request() -> Cycles {
Cycles::new(crate::domain::policy::pure::cycles_funding::DEFAULT_MAX_PER_REQUEST)
}
pub const fn cycles_funding_max_per_child() -> Cycles {
Cycles::new(crate::domain::policy::pure::cycles_funding::DEFAULT_MAX_PER_CHILD)
}
pub const fn cycles_funding_cooldown_secs() -> u64 {
crate::domain::policy::pure::cycles_funding::DEFAULT_COOLDOWN_SECS
}
pub const fn component_maximum_children() -> u32 {
super::DEFAULT_COMPONENT_MAXIMUM_CHILDREN
}
pub const fn component_maximum_registry_bytes() -> u64 {
super::DEFAULT_COMPONENT_MAXIMUM_REGISTRY_BYTES
}
pub const fn component_cycles_funding_window_secs() -> u64 {
super::DEFAULT_COMPONENT_CYCLES_FUNDING_WINDOW_SECS
}
pub const fn component_cycles_funding_maximum_cycles() -> Cycles {
Cycles::new(super::DEFAULT_COMPONENT_CYCLES_FUNDING_MAXIMUM_CYCLES)
}
}
pub const DEFAULT_COMPONENT_MAXIMUM_CHILDREN: u32 = 4_096;
pub const DEFAULT_COMPONENT_MAXIMUM_REGISTRY_BYTES: u64 = 2_097_152;
pub const DEFAULT_COMPONENT_CYCLES_FUNDING_WINDOW_SECS: u64 = 3_600;
pub const DEFAULT_COMPONENT_CYCLES_FUNDING_MAXIMUM_CYCLES: u128 = 1_000_000_000_000_000;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ComponentSpecConfig {
pub component_role: CanisterRole,
pub maximum_instances: u32,
#[serde(default)]
pub limits: ComponentLimitsConfig,
#[serde(
default = "defaults::initial_cycles",
deserialize_with = "Cycles::from_config"
)]
pub initial_cycles: Cycles,
#[serde(default)]
pub topup: Option<TopupPolicy>,
#[serde(default)]
pub cycles_funding: CyclesFundingPolicyConfig,
#[serde(default)]
pub scaling: Option<ScalingConfig>,
#[serde(default)]
pub sharding: Option<ShardingConfig>,
#[serde(default)]
pub binding: Option<BindingConfig>,
#[serde(default)]
pub auth: CanisterAuthConfig,
#[serde(default)]
pub standards: StandardsCanisterConfig,
#[serde(default)]
pub diagnostics: DiagnosticsCanisterConfig,
#[serde(default)]
pub metrics: MetricsCanisterConfig,
#[serde(default)]
pub children: BTreeMap<CanisterRole, ComponentChildConfig>,
}
impl ComponentSpecConfig {
#[must_use]
pub fn get_canister(&self, role: &CanisterRole) -> Option<CanisterConfig> {
if role == &self.component_role {
return Some(self.component_canister_config());
}
self.children
.get(role)
.map(ComponentChildConfig::canister_config)
}
#[must_use]
pub fn auto_create_roles(&self) -> BTreeSet<CanisterRole> {
BTreeSet::from([self.component_role.clone()])
}
#[must_use]
pub fn component_directory_roles(&self) -> BTreeSet<CanisterRole> {
self.auto_create_roles()
}
pub fn roles(&self) -> impl Iterator<Item = &CanisterRole> {
std::iter::once(&self.component_role).chain(self.children.keys())
}
pub fn canister_configs(&self) -> impl Iterator<Item = (&CanisterRole, CanisterConfig)> + '_ {
std::iter::once((&self.component_role, self.component_canister_config())).chain(
self.children
.iter()
.map(|(role, child)| (role, child.canister_config())),
)
}
#[must_use]
pub fn component_canister_config(&self) -> CanisterConfig {
CanisterConfig {
kind: CanisterKind::Service,
initial_cycles: self.initial_cycles.clone(),
topup: self.topup.clone(),
icp_refill: None,
cycles_funding: self.cycles_funding.clone(),
scaling: self.scaling.clone(),
sharding: self.sharding.clone(),
binding: self.binding.clone(),
auth: self.auth.clone(),
standards: self.standards.clone(),
diagnostics: self.diagnostics,
metrics: self.metrics,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ComponentLimitsConfig {
#[serde(default = "defaults::component_maximum_children")]
pub maximum_children: u32,
#[serde(default = "defaults::component_maximum_registry_bytes")]
pub maximum_registry_bytes: u64,
#[serde(default)]
pub cycles_funding: CyclesFundingBudgetConfig,
}
impl Default for ComponentLimitsConfig {
fn default() -> Self {
Self {
maximum_children: defaults::component_maximum_children(),
maximum_registry_bytes: defaults::component_maximum_registry_bytes(),
cycles_funding: CyclesFundingBudgetConfig::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CyclesFundingBudgetConfig {
#[serde(default = "defaults::component_cycles_funding_window_secs")]
pub window_secs: u64,
#[serde(
default = "defaults::component_cycles_funding_maximum_cycles",
deserialize_with = "Cycles::from_config"
)]
pub maximum_cycles: Cycles,
}
impl Default for CyclesFundingBudgetConfig {
fn default() -> Self {
Self {
window_secs: defaults::component_cycles_funding_window_secs(),
maximum_cycles: defaults::component_cycles_funding_maximum_cycles(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ComponentChildConfig {
pub kind: ComponentChildKind,
pub maximum_instances: u32,
#[serde(
default = "defaults::initial_cycles",
deserialize_with = "Cycles::from_config"
)]
pub initial_cycles: Cycles,
#[serde(default)]
pub topup: Option<TopupPolicy>,
#[serde(default)]
pub cycles_funding: CyclesFundingPolicyConfig,
#[serde(default)]
pub auth: CanisterAuthConfig,
#[serde(default)]
pub standards: StandardsCanisterConfig,
#[serde(default)]
pub diagnostics: DiagnosticsCanisterConfig,
#[serde(default)]
pub metrics: MetricsCanisterConfig,
}
impl ComponentChildConfig {
#[must_use]
pub fn canister_config(&self) -> CanisterConfig {
CanisterConfig {
kind: self.kind.into(),
initial_cycles: self.initial_cycles.clone(),
topup: self.topup.clone(),
icp_refill: None,
cycles_funding: self.cycles_funding.clone(),
scaling: None,
sharding: None,
binding: None,
auth: self.auth.clone(),
standards: self.standards.clone(),
diagnostics: self.diagnostics,
metrics: self.metrics,
}
}
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum ComponentChildKind {
#[serde(rename = "singleton")]
Singleton,
#[serde(rename = "replica")]
Replica,
#[serde(rename = "shard")]
Shard,
#[serde(rename = "instance")]
Instance,
}
impl fmt::Display for ComponentChildKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Singleton => "singleton",
Self::Replica => "replica",
Self::Shard => "shard",
Self::Instance => "instance",
};
formatter.write_str(label)
}
}
impl From<ComponentChildKind> for CanisterKind {
fn from(kind: ComponentChildKind) -> Self {
match kind {
ComponentChildKind::Singleton => Self::Singleton,
ComponentChildKind::Replica => Self::Replica,
ComponentChildKind::Shard => Self::Shard,
ComponentChildKind::Instance => Self::Instance,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PoolImport {
#[serde(default)]
pub initial: Option<u16>,
#[serde(default)]
pub local: Vec<Principal>,
#[serde(default)]
pub ic: Vec<Principal>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CanisterPool {
pub minimum_size: u8,
#[serde(default)]
pub import: PoolImport,
}
pub fn implicit_wasm_store_canister_config() -> CanisterConfig {
CanisterConfig {
kind: CanisterKind::Singleton,
initial_cycles: defaults::initial_cycles(),
topup: None,
icp_refill: None,
cycles_funding: CyclesFundingPolicyConfig::default(),
scaling: None,
sharding: None,
binding: None,
auth: CanisterAuthConfig::default(),
standards: StandardsCanisterConfig::default(),
diagnostics: DiagnosticsCanisterConfig::default(),
metrics: MetricsCanisterConfig::default(),
}
}
#[must_use]
pub fn implicit_root_canister_config() -> CanisterConfig {
CanisterConfig {
kind: CanisterKind::Root,
initial_cycles: defaults::initial_cycles(),
topup: None,
icp_refill: None,
cycles_funding: CyclesFundingPolicyConfig::default(),
scaling: None,
sharding: None,
binding: None,
auth: CanisterAuthConfig::default(),
standards: StandardsCanisterConfig::default(),
diagnostics: DiagnosticsCanisterConfig::default(),
metrics: MetricsCanisterConfig::default(),
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CanisterAuthConfig {
#[serde(default)]
pub delegated_token_issuer: bool,
#[serde(default)]
pub delegated_token_verifier: bool,
#[serde(default)]
pub role_attestation_cache: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct StandardsCanisterConfig {
#[serde(default)]
pub icrc21: bool,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticsCanisterConfig {
#[serde(default)]
pub memory_ledger: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CanisterConfig {
pub kind: CanisterKind,
#[serde(
default = "defaults::initial_cycles",
deserialize_with = "Cycles::from_config"
)]
pub initial_cycles: Cycles,
#[serde(default)]
pub topup: Option<TopupPolicy>,
#[serde(default)]
pub icp_refill: Option<IcpRefillPolicy>,
#[serde(default)]
pub cycles_funding: CyclesFundingPolicyConfig,
#[serde(default)]
pub scaling: Option<ScalingConfig>,
#[serde(default)]
pub sharding: Option<ShardingConfig>,
#[serde(default)]
pub binding: Option<BindingConfig>,
#[serde(default)]
pub auth: CanisterAuthConfig,
#[serde(default)]
pub standards: StandardsCanisterConfig,
#[serde(default)]
pub diagnostics: DiagnosticsCanisterConfig,
#[serde(default)]
pub metrics: MetricsCanisterConfig,
}
impl CanisterConfig {
#[must_use]
pub fn resolved_metrics_profile(&self, role: &CanisterRole) -> MetricsProfile {
if let Some(profile) = self.metrics.profile {
return profile;
}
if self.kind == CanisterKind::Root || role.is_root() {
return MetricsProfile::Root;
}
if role.is_wasm_store() {
return MetricsProfile::Storage;
}
if self.scaling.is_some() || self.sharding.is_some() || self.binding.is_some() {
return MetricsProfile::Hub;
}
MetricsProfile::Leaf
}
#[must_use]
pub fn role_bearing_child_roles(&self) -> Vec<&CanisterRole> {
let scaling_roles = self
.scaling
.iter()
.flat_map(|scaling| scaling.pools.values().map(|pool| &pool.canister_role));
let sharding_roles = self
.sharding
.iter()
.flat_map(|sharding| sharding.pools.values().map(|pool| &pool.canister_role));
let binding_roles = self
.binding
.iter()
.flat_map(|binding| binding.pools.values().map(|pool| &pool.canister_role));
scaling_roles
.chain(sharding_roles)
.chain(binding_roles)
.collect()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CyclesFundingPolicyConfig {
#[serde(
default = "defaults::cycles_funding_max_per_request",
deserialize_with = "Cycles::from_config"
)]
pub max_per_request: Cycles,
#[serde(
default = "defaults::cycles_funding_max_per_child",
deserialize_with = "Cycles::from_config"
)]
pub max_per_child: Cycles,
#[serde(default = "defaults::cycles_funding_cooldown_secs")]
pub cooldown_secs: u64,
}
impl Default for CyclesFundingPolicyConfig {
fn default() -> Self {
Self {
max_per_request: defaults::cycles_funding_max_per_request(),
max_per_child: defaults::cycles_funding_max_per_child(),
cooldown_secs: defaults::cycles_funding_cooldown_secs(),
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MetricsCanisterConfig {
#[serde(default)]
pub profile: Option<MetricsProfile>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetricsProfile {
Leaf,
Hub,
Storage,
Root,
Full,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CanisterKind {
Root,
Service,
Singleton,
Replica,
Shard,
Instance,
}
impl fmt::Display for CanisterKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Root => "root",
Self::Service => "service",
Self::Singleton => "singleton",
Self::Replica => "replica",
Self::Shard => "shard",
Self::Instance => "instance",
};
f.write_str(label)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TopupPolicy {
#[serde(
default = "defaults::topup_threshold",
deserialize_with = "Cycles::from_config"
)]
pub threshold: Cycles,
#[serde(
default = "defaults::topup_amount",
deserialize_with = "Cycles::from_config"
)]
pub amount: Cycles,
}
impl Default for TopupPolicy {
fn default() -> Self {
Self {
threshold: defaults::topup_threshold(),
amount: defaults::topup_amount(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct IcpRefillPolicy {
pub max_refill_e8s_per_call: u64,
#[serde(default)]
pub min_xdr_permyriad_per_icp: Option<u64>,
#[serde(default)]
pub ledger_canister_id: Option<Principal>,
#[serde(default)]
pub cmc_canister_id: Option<Principal>,
#[serde(default)]
pub allow_ic_system_canister_overrides: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ScalingConfig {
#[serde(default)]
pub pools: BTreeMap<String, ScalePool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ScalePool {
pub canister_role: CanisterRole,
#[serde(default)]
pub policy: ScalePoolPolicy,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, default)]
pub struct ScalePoolPolicy {
pub initial_workers: u32,
pub min_workers: u32,
pub max_workers: u32,
}
impl Default for ScalePoolPolicy {
fn default() -> Self {
Self {
initial_workers: 1,
min_workers: 1,
max_workers: 32,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ShardingConfig {
#[serde(default)]
pub pools: BTreeMap<String, ShardPool>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BindingConfig {
#[serde(default)]
pub pools: BTreeMap<String, BindingPool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BindingPool {
pub canister_role: CanisterRole,
pub key_name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ShardPool {
pub canister_role: CanisterRole,
#[serde(default)]
pub policy: ShardPoolPolicy,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, default)]
pub struct ShardPoolPolicy {
pub capacity: u32,
pub initial_shards: u32,
pub max_shards: u32,
}
impl Default for ShardPoolPolicy {
fn default() -> Self {
Self {
capacity: 1_000,
initial_shards: 1,
max_shards: 4,
}
}
}
#[cfg(test)]
mod tests;