pub(in crate::fleet_ensure) mod allowance;
pub(in crate::fleet_ensure) mod live_binding;
pub(in crate::fleet_ensure) mod local_demand;
pub(super) mod planning;
pub(in crate::fleet_ensure) mod recursive;
pub(in crate::fleet_ensure) mod relay_quote;
#[cfg(test)]
mod tests;
#[cfg(test)]
pub(in crate::fleet_ensure) use planning::tests::{qualify, qualify_creation};
#[cfg(test)]
pub(in crate::fleet_ensure) use tests::{funding_binding, hub_config, hub_source};
use super::{EnsurePolicyError, initial_role_instances};
use crate::fleet_ensure::{
model::{DesiredFleet, DesiredFleetBootstrapRoot},
view::startup_funding::{StartupComponentFunding, StartupRoleFunding},
};
use canic_core::{
bootstrap::compiled::ConfigModel, control_plane_support::config::ComponentSpec,
ids::CanisterRole,
};
use std::collections::BTreeMap;
pub(in crate::fleet_ensure) fn minimum_root_cycles(
request_threshold: u128,
child_grants: u128,
) -> Result<u128, EnsurePolicyError> {
use canic_core::control_plane_support::policy::deployment::MINIMUM_DEPLOYMENT_RESERVE_CYCLES;
add(
add(request_threshold, 1)?.max(MINIMUM_DEPLOYMENT_RESERVE_CYCLES),
child_grants,
)
}
pub(in crate::fleet_ensure) fn recovery_minimum_cycles(
desired: &DesiredFleet,
root: &str,
configured_minimum: u128,
) -> Result<u128, EnsurePolicyError> {
let threshold = if let Some(bootstrap) = &desired.bootstrap {
let mut matches = bootstrap.roots.iter().filter(|entry| entry.root == root);
let selected = matches.next().ok_or_else(|| {
invalid_topology("native recovery requires the selected Root's funding authority")
})?;
if matches.next().is_some() {
return Err(invalid_topology(
"native recovery requires one Root funding authority",
));
}
selected.funding.root_funding.request_threshold.to_u128()
} else {
0
};
Ok(configured_minimum.max(minimum_root_cycles(threshold, 0)?))
}
pub(in crate::fleet_ensure) fn root_components(
config: &ConfigModel,
desired: &DesiredFleet,
root: &DesiredFleetBootstrapRoot,
) -> Result<Vec<StartupComponentFunding>, EnsurePolicyError> {
let bootstrap = desired
.bootstrap
.as_ref()
.ok_or_else(|| invalid_topology("startup funding requires bootstrap"))?;
let protocol = desired
.protocol
.as_ref()
.ok_or_else(|| invalid_topology("startup funding requires protocol"))?;
let mut components = Vec::new();
for placement in protocol
.component_group_placements
.iter()
.filter(|placement| placement.root == root.root)
{
let deployment = bootstrap
.component_deployment_configuration
.deployment_topology
.component_group_deployments
.iter()
.find(|deployment| deployment.deployment.as_str() == placement.deployment)
.ok_or_else(|| invalid_topology("startup funding requires a configured deployment"))?;
for member in &deployment.members {
let spec = bootstrap
.component_deployment_configuration
.component_topology
.component_specs
.iter()
.find(|spec| spec.component_spec == member.component_spec)
.ok_or_else(|| {
invalid_topology("startup funding requires a configured Component Spec")
})?;
components.push(component(
config,
spec,
root.limits.canister_pool.canister_cycles.to_u128(),
&placement.deployment,
placement.ordinal,
)?);
}
}
Ok(components)
}
pub(in crate::fleet_ensure) fn component(
config: &ConfigModel,
spec: &ComponentSpec,
pool_balance: u128,
deployment: &str,
ordinal: u32,
) -> Result<StartupComponentFunding, EnsurePolicyError> {
let roles = compile_roles(config, spec, pool_balance)?;
let root_grant_cycles = roles[&spec.component_role].parent_grants_per_instance_cycles;
let descendant_grants_cycles = roles
.values()
.filter(|role| role.role != spec.component_role)
.try_fold(0, |total, role| {
add(
total,
multiply(
role.parent_grants_per_instance_cycles,
u128::from(role.instances),
)?,
)
})?;
Ok(StartupComponentFunding {
component_spec: spec.component_spec.clone(),
deployment: deployment.to_owned(),
descendant_grants_cycles,
exceeds_window_budget: descendant_grants_cycles
> spec.limits.cycles_funding.maximum_cycles.to_u128(),
funding_budget: spec.limits.cycles_funding.clone(),
ordinal,
roles: roles.into_values().collect(),
root_grant_cycles,
})
}
fn compile_roles(
config: &ConfigModel,
spec: &ComponentSpec,
pool_balance: u128,
) -> Result<BTreeMap<CanisterRole, StartupRoleFunding>, EnsurePolicyError> {
let source = config
.component_specs
.get(&spec.component_spec)
.ok_or_else(|| {
invalid_topology("startup funding has no matching configured Component Spec")
})?;
let mut instances = initial_role_instances(spec)?;
instances.retain(|_, count| *count > 0);
let mut roles = BTreeMap::<CanisterRole, StartupRoleFunding>::new();
while roles.len() < instances.len() {
let before = roles.len();
for (role, count) in &instances {
if roles.contains_key(role) {
continue;
}
let Some(outgoing) = outgoing_grants(spec, role, &roles)? else {
continue;
};
let policy = source.get_canister(role).ok_or_else(|| {
invalid_topology("startup funding has no matching configured role")
})?;
let threshold = policy.topup.as_ref().map(|topup| topup.threshold.to_u128());
let grant = policy.topup.as_ref().map_or(0, |topup| {
topup
.amount
.to_u128()
.min(policy.cycles_funding.max_per_request.to_u128())
});
let demand = grant_demand(
pool_balance,
outgoing,
threshold,
grant,
policy.cycles_funding.max_per_child.to_u128(),
)?;
roles.insert(
role.clone(),
StartupRoleFunding {
cooldown_secs: policy.cycles_funding.cooldown_secs,
effective_grant_cycles: grant,
grants_per_instance: demand.count,
initial_balance_cycles: pool_balance,
instances: *count,
maximum_per_child_cycles: policy.cycles_funding.max_per_child.to_u128(),
outgoing_grants_per_instance_cycles: outgoing,
parent_grants_per_instance_cycles: demand.cycles,
role: role.clone(),
threshold_cycles: threshold,
unfunded_per_instance_cycles: demand.unfunded,
},
);
}
if roles.len() == before {
return Err(invalid_topology(
"startup funding role graph did not converge",
));
}
}
Ok(roles)
}
fn outgoing_grants(
spec: &ComponentSpec,
role: &CanisterRole,
roles: &BTreeMap<CanisterRole, StartupRoleFunding>,
) -> Result<Option<u128>, EnsurePolicyError> {
let mut outgoing = 0_u128;
for edge in spec
.spawn_grants
.iter()
.filter(|edge| &edge.parent_role == role && edge.initial_instances_per_parent > 0)
{
let Some(child) = roles.get(&edge.child_role) else {
return Ok(None);
};
outgoing = add(
outgoing,
multiply(
child.parent_grants_per_instance_cycles,
u128::from(edge.initial_instances_per_parent),
)?,
)?;
}
Ok(Some(outgoing))
}
#[derive(Debug, Eq, PartialEq)]
struct GrantDemand {
count: u128,
cycles: u128,
unfunded: u128,
}
fn grant_demand(
balance: u128,
outgoing: u128,
threshold: Option<u128>,
grant: u128,
lifetime_limit: u128,
) -> Result<GrantDemand, EnsurePolicyError> {
let target = threshold.map_or(Ok(0), |threshold| add(threshold, 1))?;
let required = add(outgoing, target)?.saturating_sub(balance);
if threshold.is_none() || grant == 0 || required == 0 {
return Ok(GrantDemand {
count: 0,
cycles: 0,
unfunded: required,
});
}
let requested = multiply(required.div_ceil(grant), grant)?;
let cycles = requested.min(lifetime_limit);
Ok(GrantDemand {
count: cycles.div_ceil(grant),
cycles,
unfunded: required.saturating_sub(cycles),
})
}
pub(in crate::fleet_ensure) fn add(left: u128, right: u128) -> Result<u128, EnsurePolicyError> {
left.checked_add(right)
.ok_or(EnsurePolicyError::ArithmeticOverflow {
field: "startup funding demand",
})
}
fn multiply(left: u128, right: u128) -> Result<u128, EnsurePolicyError> {
left.checked_mul(right)
.ok_or(EnsurePolicyError::ArithmeticOverflow {
field: "startup funding demand",
})
}
fn invalid_topology(reason: &str) -> EnsurePolicyError {
EnsurePolicyError::EstateFundingTopology {
reason: reason.to_owned(),
}
}