Skip to main content

canic_core/ids/fleet_topology/
mod.rs

1//! Module: ids::fleet_topology
2//!
3//! Responsibility: define protected Fleet topology, admission, limit, and binding facts.
4//! Does not own: configuration compilation, placement decisions, Registry mutation, or storage.
5//! Boundary: these passive cross-layer contracts are validated before authoritative use.
6
7use crate::{
8    cdk::types::Cycles,
9    ids::{
10        CanisterRole, ComponentInstanceId, ComponentSpecId, FleetBinding, ReleaseBuildId,
11        ReleaseSetDigest, SubnetId,
12    },
13};
14use candid::{CandidType, Principal};
15use serde::{Deserialize, Serialize};
16use std::fmt;
17
18///
19/// ComponentTopologyDigest
20///
21/// SHA-256 identity of one canonical root-local Component Topology projection.
22///
23
24#[derive(
25    CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
26)]
27#[serde(transparent)]
28pub struct ComponentTopologyDigest([u8; 32]);
29
30impl ComponentTopologyDigest {
31    #[must_use]
32    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
33        Self(bytes)
34    }
35
36    #[must_use]
37    pub const fn as_bytes(&self) -> &[u8; 32] {
38        &self.0
39    }
40
41    #[must_use]
42    pub const fn into_bytes(self) -> [u8; 32] {
43        self.0
44    }
45}
46
47impl fmt::Display for ComponentTopologyDigest {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        for byte in self.0 {
50            write!(formatter, "{byte:02x}")?;
51        }
52        Ok(())
53    }
54}
55
56///
57/// CyclesFundingBudget
58///
59/// Positive aggregate cycles-funding ceiling applied over one bounded window.
60///
61
62#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
63#[serde(deny_unknown_fields)]
64pub struct CyclesFundingBudget {
65    pub window_secs: u64,
66    pub maximum_cycles: Cycles,
67}
68
69/// Protected physical-topology class that selects the minimum Root funding baseline.
70#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
71pub enum FleetFundingProfile {
72    #[serde(rename = "single_subnet")]
73    SingleSubnet,
74    #[serde(rename = "multi_subnet")]
75    MultiSubnet,
76    #[serde(rename = "preview_multi_subnet")]
77    PreviewMultiSubnet,
78}
79
80/// Minimum post-grant Coordinator execution reserve established by the 0.108 M0 proof.
81pub const COORDINATOR_ROOT_FUNDING_EXECUTION_RESERVE_FLOOR_CYCLES: u128 = 100_000_000;
82
83/// Conservative current-cost reservation for one bounded 16 KiB funding command.
84pub const FLEET_ROOT_FUNDING_CALL_RESERVATION_CYCLES: u128 = 42_118_809_000;
85
86/// Minimum Root balance admitted for the Coordinator request and exact-retry path.
87pub const FLEET_SUBNET_ROOT_FUNDING_REQUEST_FLOOR_CYCLES: u128 = 42_200_000_000;
88
89/// Minimum Root balance admitted for automatic ICP-refill execution and recovery.
90pub const FLEET_SUBNET_ROOT_ICP_REFILL_FLOOR_CYCLES: u128 = 42_200_000_000;
91
92/// Maximum registered roots represented by the bounded Coordinator funding ledger.
93pub const MAX_FLEET_ROOT_FUNDING_SLOTS: usize = 4_096;
94
95/// Maximum physical sources in one reviewed Root capacity import.
96pub const MAX_FLEET_CAPACITY_IMPORT_SOURCES: usize = 256;
97
98///
99/// FleetCoordinatorRootFundingPolicy
100///
101/// Immutable Fleet-wide reserve and grant-budget authority installed into one Coordinator.
102///
103
104#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105#[serde(deny_unknown_fields)]
106pub struct FleetCoordinatorRootFundingPolicy {
107    pub funding_profile: FleetFundingProfile,
108    pub minimum_reserve_cycles: Cycles,
109    pub budget: CyclesFundingBudget,
110    pub maximum_automatic_grants: u32,
111    pub maximum_automatic_cycles: Cycles,
112}
113
114///
115/// FleetSubnetRootFundingPolicy
116///
117/// Immutable Coordinator-grant thresholds and budget for one registered root.
118///
119
120#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
121#[serde(deny_unknown_fields)]
122pub struct FleetSubnetRootFundingPolicy {
123    pub funding_profile: FleetFundingProfile,
124    pub request_threshold: Cycles,
125    pub target_balance: Cycles,
126    pub cooldown_secs: u64,
127    pub budget: CyclesFundingBudget,
128    pub maximum_automatic_grants: u32,
129    pub maximum_automatic_cycles: Cycles,
130}
131
132///
133/// FleetSubnetRootAutomaticIcpRefillPolicy
134///
135/// Optional emergency trigger and target subordinate to one root's ICP-refill policy.
136///
137
138#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
139#[serde(deny_unknown_fields)]
140pub struct FleetSubnetRootAutomaticIcpRefillPolicy {
141    pub emergency_threshold: Cycles,
142    pub target_balance: Cycles,
143    pub maximum_automatic_refills: u32,
144    pub maximum_automatic_refill_e8s: u64,
145}
146
147///
148/// FleetSubnetRootIcpRefillPolicy
149///
150/// Immutable root-owned ICP conversion budget, balance floor, and system-Canister authority.
151///
152
153#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
154#[serde(deny_unknown_fields)]
155pub struct FleetSubnetRootIcpRefillPolicy {
156    pub max_refill_e8s_per_call: u64,
157    pub window_secs: u64,
158    pub maximum_refill_e8s: u64,
159    pub minimum_icp_balance_e8s: u64,
160    pub min_xdr_permyriad_per_icp: Option<u64>,
161    pub ledger_canister_id: Option<Principal>,
162    pub cmc_canister_id: Option<Principal>,
163    pub allow_ic_system_canister_overrides: bool,
164    pub automatic: Option<FleetSubnetRootAutomaticIcpRefillPolicy>,
165}
166
167///
168/// FleetSubnetRootFundingAuthority
169///
170/// Complete immutable Coordinator-grant and optional ICP-refill policy for one root.
171///
172
173#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
174#[serde(deny_unknown_fields)]
175pub struct FleetSubnetRootFundingAuthority {
176    pub root_funding: FleetSubnetRootFundingPolicy,
177    pub icp_refill: Option<FleetSubnetRootIcpRefillPolicy>,
178}
179
180///
181/// FleetSubnetCanisterPoolConfig
182///
183/// Immutable prepaid empty-Canister inventory policy for one Fleet Subnet Root.
184///
185
186#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
187#[serde(deny_unknown_fields)]
188pub struct FleetSubnetCanisterPoolConfig {
189    /// Ready empty Canisters automatically maintained for the root.
190    pub minimum_size: u32,
191    /// Ceiling for standby and operator-imported pool assets.
192    ///
193    /// Recycled assets remain tracked even when their return temporarily exceeds this target.
194    pub maximum_size: u32,
195    /// Minimum retained balance required before a pool asset becomes Ready.
196    pub canister_cycles: Cycles,
197    /// Native cycles retained above the Ready floor while a newly created asset is
198    /// created, inspected, controller-checked, and admitted to the pool.
199    pub creation_execution_margin: Cycles,
200}
201
202///
203/// ComponentSpecAdmission
204///
205/// Immutable permission and concrete-instance ceiling for one Spec on one Fleet Subnet Root.
206///
207
208#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
209#[serde(deny_unknown_fields)]
210pub struct ComponentSpecAdmission {
211    pub component_spec: ComponentSpecId,
212    pub spec_hash: [u8; 32],
213    pub maximum_root_instances: u32,
214}
215
216///
217/// FleetSubnetRootLimits
218///
219/// Immutable aggregate policy ceilings for one Fleet Subnet Root.
220///
221
222#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
223#[serde(deny_unknown_fields)]
224pub struct FleetSubnetRootLimits {
225    pub maximum_component_instances: u32,
226    pub maximum_registry_bytes: u64,
227    pub maximum_wasm_store_bytes: u64,
228    pub canister_pool: FleetSubnetCanisterPoolConfig,
229    pub cycles_funding: CyclesFundingBudget,
230    /// Maximum accepted or committed Component Group placements on this root.
231    pub maximum_group_placements: u32,
232}
233
234///
235/// FleetCoordinatorBinding
236///
237/// Immutable Coordinator identity, placement and direct Fleet recovery authority.
238///
239
240#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
241#[serde(deny_unknown_fields)]
242pub struct FleetCoordinatorBinding {
243    pub fleet: FleetBinding,
244    pub coordinator_subnet: SubnetId,
245    pub coordinator: Principal,
246    /// Canonical direct controllers that may recover any canister in this Fleet.
247    pub recovery_controllers: Vec<Principal>,
248}
249
250impl FleetCoordinatorBinding {
251    /// Exact controller set for a Root-owned canister, including recovery authority.
252    #[must_use]
253    pub fn root_controllers(&self, root: Principal) -> Vec<Principal> {
254        let mut controllers = self.recovery_controllers.clone();
255        controllers.push(root);
256        controllers.sort_unstable();
257        controllers.dedup();
258        controllers
259    }
260
261    /// Compare IC controller sets without relying on management response order.
262    #[must_use]
263    pub fn has_exact_root_controllers(&self, root: Principal, observed: &[Principal]) -> bool {
264        let mut observed = observed.to_vec();
265        observed.sort_unstable();
266        observed == self.root_controllers(root)
267    }
268}
269
270///
271/// FleetRegistryAuthority
272///
273/// Exact Coordinator binding and reinstall-local authority epoch for one Fleet Registry.
274///
275
276#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
277#[serde(deny_unknown_fields)]
278pub struct FleetRegistryAuthority {
279    pub binding: FleetCoordinatorBinding,
280    pub epoch: u64,
281}
282
283///
284/// FleetSubnetRootBinding
285///
286/// Complete immutable identity, placement, admissions, and limits of one Fleet Subnet Root.
287///
288
289#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
290#[serde(deny_unknown_fields)]
291pub struct FleetSubnetRootBinding {
292    pub authority: FleetRegistryAuthority,
293    pub placement_subnet: SubnetId,
294    pub fleet_subnet_root: Principal,
295    pub component_admissions: Vec<ComponentSpecAdmission>,
296    pub component_topology_digest: ComponentTopologyDigest,
297    pub limits: FleetSubnetRootLimits,
298    pub funding: FleetSubnetRootFundingAuthority,
299}
300
301///
302/// FleetSubnetWasmStoreAuthority
303///
304/// Exact reciprocal authority retained by one root and its host-installed sibling Store.
305///
306
307#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
308#[serde(deny_unknown_fields)]
309pub struct FleetSubnetWasmStoreAuthority {
310    pub authority: FleetRegistryAuthority,
311    pub placement_subnet: SubnetId,
312    pub fleet_subnet_root: Principal,
313    pub wasm_store: Principal,
314    pub installation_controller: Principal,
315    pub release_build_id: ReleaseBuildId,
316    pub wasm_module_hash: [u8; 32],
317}
318
319/// Exact child identity Root must use while activating its independently installed Store.
320#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
321pub struct FleetSubnetWasmStoreActivationAuthority {
322    pub fleet: FleetBinding,
323    pub operation_id: [u8; 32],
324    pub fleet_subnet_root: Principal,
325    pub wasm_store: Principal,
326    pub release_build_id: ReleaseBuildId,
327    pub component_topology_digest: ComponentTopologyDigest,
328    pub controllers: Vec<Principal>,
329    pub manifest_digest: ReleaseSetDigest,
330}
331
332///
333/// ComponentBinding
334///
335/// Complete immutable identity and placement of one concrete Component.
336///
337
338#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
339#[serde(deny_unknown_fields)]
340pub struct ComponentBinding {
341    pub authority: FleetRegistryAuthority,
342    pub component: ComponentInstanceId,
343    pub component_spec: ComponentSpecId,
344    pub spec_hash: [u8; 32],
345    pub role: CanisterRole,
346    pub placement_subnet: SubnetId,
347    pub fleet_subnet_root: Principal,
348    pub canister_id: Principal,
349}
350
351///
352/// ComponentChildBinding
353///
354/// Complete immutable identity of one child at any depth in one exact Component tree.
355///
356
357#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
358#[serde(deny_unknown_fields)]
359pub struct ComponentChildBinding {
360    pub component: ComponentBinding,
361    pub parent_canister_id: Principal,
362    pub role: CanisterRole,
363    pub canister_id: Principal,
364}
365
366///
367/// ManagedCanisterBinding
368///
369/// Immutable Registry-issued identity retained by one managed application Canister.
370///
371
372#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
373#[serde(deny_unknown_fields)]
374pub enum ManagedCanisterBinding {
375    Component(ComponentBinding),
376    ComponentChild(ComponentChildBinding),
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn root_controller_set_includes_exact_recovery_principals() {
385        use crate::ids::{AppId, CanonicalNetworkId, FleetId, FleetKey};
386
387        let root = Principal::from_slice(&[21; 29]);
388        let recovery = Principal::from_slice(&[22; 29]);
389        let binding = FleetCoordinatorBinding {
390            fleet: FleetBinding {
391                fleet: FleetKey {
392                    canonical_network_id: CanonicalNetworkId::ic_mainnet(),
393                    fleet_id: FleetId::from_generated_bytes([23; 32]),
394                },
395                app: AppId::from("recovery-test"),
396            },
397            coordinator_subnet: SubnetId::from_principal(Principal::from_slice(&[24; 29])),
398            coordinator: Principal::from_slice(&[25; 29]),
399            recovery_controllers: vec![recovery],
400        };
401        let mut expected = vec![root, recovery];
402        expected.sort_unstable();
403        assert_eq!(binding.root_controllers(root), expected);
404        assert!(binding.has_exact_root_controllers(root, &[recovery, root]));
405        assert!(!binding.has_exact_root_controllers(root, &[root]));
406    }
407
408    #[test]
409    fn funding_profile_candid_spelling_roundtrips() {
410        for profile in [
411            FleetFundingProfile::SingleSubnet,
412            FleetFundingProfile::PreviewMultiSubnet,
413            FleetFundingProfile::MultiSubnet,
414        ] {
415            let bytes = candid::encode_one(profile).expect("encode funding profile Candid");
416            assert_eq!(
417                candid::decode_one::<FleetFundingProfile>(&bytes)
418                    .expect("decode funding profile Candid"),
419                profile
420            );
421        }
422    }
423}