Skip to main content

canic_host/component_topology/
mod.rs

1//! Module: component_topology
2//!
3//! Responsibility: finalize Fleet Subnet Root topology bindings from config and explicit host input.
4//! Does not own: Subnet selection, Canister creation, release sets, Registry commit, or runtime.
5//! Boundary: accepts resolved authority/placement facts and emits validated immutable bindings.
6
7#[cfg(test)]
8mod tests;
9
10use candid::Principal;
11use canic_core::{
12    bootstrap::compiled::{ComponentTopology, ConfigModel},
13    ids::{
14        ComponentSpecAdmission, ComponentSpecId, ComponentTopologyDigest, FleetRegistryAuthority,
15        FleetSubnetRootBinding, FleetSubnetRootFundingAuthority, FleetSubnetRootLimits, SubnetId,
16    },
17};
18use std::collections::BTreeSet;
19use thiserror::Error as ThisError;
20
21///
22/// RootComponentAdmissionInput
23///
24/// Operator-planned positive instance capacity for one Spec on one resolved root.
25///
26
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct RootComponentAdmissionInput {
29    pub component_spec: ComponentSpecId,
30    pub maximum_root_instances: u32,
31}
32
33///
34/// FleetSubnetRootTopologyInput
35///
36/// Exact resolved physical root placement, admissions, and immutable aggregate limits.
37///
38
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct FleetSubnetRootTopologyInput {
41    pub placement_subnet: SubnetId,
42    pub fleet_subnet_root: Principal,
43    pub component_admissions: Vec<RootComponentAdmissionInput>,
44    pub limits: FleetSubnetRootLimits,
45    pub funding: FleetSubnetRootFundingAuthority,
46}
47
48///
49/// PlannedFleetSubnetRootTopologyInput
50///
51/// Exact root placement, admissions, and limits resolved before Canister creation.
52///
53
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct PlannedFleetSubnetRootTopologyInput {
56    pub placement_subnet: SubnetId,
57    pub component_admissions: Vec<RootComponentAdmissionInput>,
58    pub limits: FleetSubnetRootLimits,
59}
60
61///
62/// PlannedFleetSubnetRootTopology
63///
64/// Canonical pre-creation root topology with no fabricated Canister principal.
65///
66
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct PlannedFleetSubnetRootTopology {
69    pub placement_subnet: SubnetId,
70    pub component_admissions: Vec<ComponentSpecAdmission>,
71    pub component_topology_digest: ComponentTopologyDigest,
72    pub limits: FleetSubnetRootLimits,
73}
74
75///
76/// PlannedFleetTopology
77///
78/// Canonical Component Topology and every validated pre-creation root plan.
79///
80
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct PlannedFleetTopology {
83    pub component_topology: ComponentTopology,
84    pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootTopology>,
85}
86
87/// Root-local admitted Component demand checked against one empty-pool target.
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct RootPoolCapacityInput {
90    pub component_admissions: Vec<ComponentSpecAdmission>,
91    pub pool_target_cycles: u128,
92    pub root: String,
93}
94
95/// Exact retained import count checked against one Root's initial pool capacity.
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct RootPoolImportCapacityInput {
98    pub import_count: usize,
99    pub maximum_size: u32,
100    pub root: String,
101}
102
103/// Exact fail-closed mismatch between admitted Component demand and pool capacity.
104#[derive(Debug, Eq, PartialEq, ThisError)]
105pub enum RootPoolCapacityError {
106    #[error(
107        "Root {root} pool target {pool_target_cycles} cycles is below admitted Component Spec '{component_spec}' initial demand {required_cycles} cycles"
108    )]
109    Insufficient {
110        component_spec: ComponentSpecId,
111        pool_target_cycles: u128,
112        required_cycles: u128,
113        root: String,
114    },
115
116    #[error("Root {root} admits unknown Component Spec '{component_spec}'")]
117    UnknownComponentSpec {
118        component_spec: ComponentSpecId,
119        root: String,
120    },
121}
122
123/// Exact fail-closed mismatch between retained imports and Root initialisation capacity.
124#[derive(Debug, Eq, PartialEq, ThisError)]
125#[error(
126    "Root {root} has {import_count} retained pool imports, above initialisation maximum {maximum_size}"
127)]
128pub struct RootPoolImportCapacityError {
129    pub import_count: usize,
130    pub maximum_size: u32,
131    pub root: String,
132}
133
134///
135/// FleetTopologyPlan
136///
137/// Canonical Fleet topology plus every validated root-local binding.
138///
139
140#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct FleetTopologyPlan {
142    pub component_topology: ComponentTopology,
143    pub fleet_subnet_roots: Vec<FleetSubnetRootBinding>,
144}
145
146///
147/// FleetTopologyPlanError
148///
149/// Typed rejection while finalizing immutable root topology input.
150///
151
152#[derive(Debug, Eq, PartialEq, ThisError)]
153pub enum FleetTopologyPlanError {
154    #[error(
155        "Fleet authority App '{authority_app}' does not match configured App '{configured_app}'"
156    )]
157    AppMismatch {
158        configured_app: String,
159        authority_app: String,
160    },
161
162    #[error("root input repeats Component Spec admission '{component_spec}'")]
163    DuplicateAdmission { component_spec: ComponentSpecId },
164
165    #[error("root input repeats placement Subnet '{placement_subnet}'")]
166    DuplicatePlacementSubnet { placement_subnet: SubnetId },
167
168    #[error("root placement Subnet must not be anonymous")]
169    AnonymousPlacementSubnet,
170
171    #[error(transparent)]
172    Topology(#[from] canic_core::bootstrap::compiled::ComponentTopologyError),
173
174    #[error("root input references unknown Component Spec '{component_spec}'")]
175    UnknownComponentSpec { component_spec: ComponentSpecId },
176}
177
178/// Reject any admitted Component whose exact initial demand exceeds its Root's pool target.
179pub fn validate_root_pool_capacity(
180    config: &ConfigModel,
181    roots: &[RootPoolCapacityInput],
182) -> Result<(), RootPoolCapacityError> {
183    for root in roots {
184        for admission in &root.component_admissions {
185            let component = config
186                .component_specs
187                .get(&admission.component_spec)
188                .ok_or_else(|| RootPoolCapacityError::UnknownComponentSpec {
189                    component_spec: admission.component_spec.clone(),
190                    root: root.root.clone(),
191                })?;
192            let required_cycles = component.initial_cycles.to_u128();
193            if required_cycles > root.pool_target_cycles {
194                return Err(RootPoolCapacityError::Insufficient {
195                    component_spec: admission.component_spec.clone(),
196                    pool_target_cycles: root.pool_target_cycles,
197                    required_cycles,
198                    root: root.root.clone(),
199                });
200            }
201        }
202    }
203    Ok(())
204}
205
206/// Reject a Root bootstrap whose complete retained import set cannot be initialised.
207pub fn validate_root_pool_import_capacity(
208    input: &RootPoolImportCapacityInput,
209) -> Result<(), RootPoolImportCapacityError> {
210    if let Ok(import_count) = u32::try_from(input.import_count)
211        && import_count <= input.maximum_size
212    {
213        return Ok(());
214    }
215    Err(RootPoolImportCapacityError {
216        import_count: input.import_count,
217        maximum_size: input.maximum_size,
218        root: input.root.clone(),
219    })
220}
221
222/// Finalize canonical pre-creation root plans without inventing Canister principals.
223pub fn plan_initial_fleet_topology(
224    config: &ConfigModel,
225    root_inputs: Vec<PlannedFleetSubnetRootTopologyInput>,
226) -> Result<PlannedFleetTopology, FleetTopologyPlanError> {
227    let component_topology = config.compile_component_topology()?;
228    let mut fleet_subnet_roots = root_inputs
229        .into_iter()
230        .map(|input| finalize_planned_root(&component_topology, input))
231        .collect::<Result<Vec<_>, _>>()?;
232    fleet_subnet_roots.sort_by_key(|root| root.placement_subnet);
233
234    let mut placement_subnets = BTreeSet::new();
235    for root in &fleet_subnet_roots {
236        if root.placement_subnet.as_principal() == &Principal::anonymous() {
237            return Err(FleetTopologyPlanError::AnonymousPlacementSubnet);
238        }
239        if !placement_subnets.insert(root.placement_subnet) {
240            return Err(FleetTopologyPlanError::DuplicatePlacementSubnet {
241                placement_subnet: root.placement_subnet,
242            });
243        }
244        component_topology.validate_planned_root(
245            &root.component_admissions,
246            root.component_topology_digest,
247            &root.limits,
248        )?;
249    }
250    let admissions = fleet_subnet_roots
251        .iter()
252        .map(|root| root.component_admissions.as_slice())
253        .collect::<Vec<_>>();
254    component_topology.validate_fleet_admissions(&admissions)?;
255
256    Ok(PlannedFleetTopology {
257        component_topology,
258        fleet_subnet_roots,
259    })
260}
261
262/// Finalize canonical root bindings without accepting caller-supplied hashes or digests.
263pub fn plan_fleet_topology(
264    config: &ConfigModel,
265    authority: FleetRegistryAuthority,
266    root_inputs: Vec<FleetSubnetRootTopologyInput>,
267) -> Result<FleetTopologyPlan, FleetTopologyPlanError> {
268    if config.app_id() != &authority.binding.fleet.app {
269        return Err(FleetTopologyPlanError::AppMismatch {
270            configured_app: config.app_id().to_string(),
271            authority_app: authority.binding.fleet.app.to_string(),
272        });
273    }
274
275    let component_topology = config.compile_component_topology()?;
276    let mut fleet_subnet_roots = Vec::with_capacity(root_inputs.len());
277
278    for input in root_inputs {
279        fleet_subnet_roots.push(finalize_root_binding(
280            &component_topology,
281            &authority,
282            input,
283        )?);
284    }
285    fleet_subnet_roots.sort_by(|left, right| {
286        left.placement_subnet
287            .cmp(&right.placement_subnet)
288            .then_with(|| left.fleet_subnet_root.cmp(&right.fleet_subnet_root))
289    });
290
291    component_topology.validate_fleet_subnet_root_bindings(&fleet_subnet_roots)?;
292    Ok(FleetTopologyPlan {
293        component_topology,
294        fleet_subnet_roots,
295    })
296}
297
298fn finalize_root_binding(
299    component_topology: &ComponentTopology,
300    authority: &FleetRegistryAuthority,
301    mut input: FleetSubnetRootTopologyInput,
302) -> Result<FleetSubnetRootBinding, FleetTopologyPlanError> {
303    input
304        .component_admissions
305        .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
306
307    let mut seen = BTreeSet::new();
308    let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
309    for admission in input.component_admissions {
310        if !seen.insert(admission.component_spec.clone()) {
311            return Err(FleetTopologyPlanError::DuplicateAdmission {
312                component_spec: admission.component_spec,
313            });
314        }
315        let component_spec = component_topology
316            .get(&admission.component_spec)
317            .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
318                component_spec: admission.component_spec.clone(),
319            })?;
320        component_admissions.push(ComponentSpecAdmission {
321            component_spec: admission.component_spec,
322            spec_hash: component_spec.spec_hash,
323            maximum_root_instances: admission.maximum_root_instances,
324        });
325    }
326
327    let projection = component_topology.project_for_admissions(&component_admissions)?;
328    Ok(FleetSubnetRootBinding {
329        authority: authority.clone(),
330        placement_subnet: input.placement_subnet,
331        fleet_subnet_root: input.fleet_subnet_root,
332        component_admissions,
333        component_topology_digest: projection.digest()?,
334        limits: input.limits,
335        funding: input.funding,
336    })
337}
338
339fn finalize_planned_root(
340    component_topology: &ComponentTopology,
341    mut input: PlannedFleetSubnetRootTopologyInput,
342) -> Result<PlannedFleetSubnetRootTopology, FleetTopologyPlanError> {
343    input
344        .component_admissions
345        .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
346
347    let mut seen = BTreeSet::new();
348    let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
349    for admission in input.component_admissions {
350        if !seen.insert(admission.component_spec.clone()) {
351            return Err(FleetTopologyPlanError::DuplicateAdmission {
352                component_spec: admission.component_spec,
353            });
354        }
355        let component_spec = component_topology
356            .get(&admission.component_spec)
357            .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
358                component_spec: admission.component_spec.clone(),
359            })?;
360        component_admissions.push(ComponentSpecAdmission {
361            component_spec: admission.component_spec,
362            spec_hash: component_spec.spec_hash,
363            maximum_root_instances: admission.maximum_root_instances,
364        });
365    }
366
367    let projection = component_topology.project_for_admissions(&component_admissions)?;
368    Ok(PlannedFleetSubnetRootTopology {
369        placement_subnet: input.placement_subnet,
370        component_admissions,
371        component_topology_digest: projection.digest()?,
372        limits: input.limits,
373    })
374}