Skip to main content

canic_core/dto/
fleet_subnet_root.rs

1//! Module: dto::fleet_subnet_root
2//!
3//! Responsibility: carry protected Fleet Subnet Root authority and controller lifecycle DTOs.
4//! Does not own: validation, persistence, topology compilation, or lifecycle effects.
5//! Boundary: lifecycle adapters pass init/command authority to workflow and return passive data.
6
7use crate::{
8    dto::fleet_registry::{FleetRegistryVersion, FleetSubnetRootStatus},
9    ids::{ComponentTopologyDigest, FleetSubnetRootBinding, FleetSubnetRootReleaseSet, SubnetId},
10};
11use candid::{CandidType, Principal};
12use serde::{Deserialize, Serialize};
13
14///
15/// FleetSubnetRootAuthority
16///
17/// Exact immutable root binding, initial release set, and installed module identity.
18///
19
20#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
21pub struct FleetSubnetRootAuthority {
22    pub binding: FleetSubnetRootBinding,
23    pub initial_release_set: FleetSubnetRootReleaseSet,
24    pub expected_module_hash: [u8; 32],
25}
26
27///
28/// FleetSubnetRootCanisterSummary
29///
30/// Compact live inventory bound to one root's exact active Fleet Registry mirror.
31///
32
33#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
34pub struct FleetSubnetRootCanisterSummary {
35    pub fleet_registry: FleetRegistryVersion,
36    pub placement_subnet: SubnetId,
37    pub fleet_subnet_root: Principal,
38    pub status: FleetSubnetRootStatus,
39    pub infrastructure_canisters: u32,
40    pub component_canisters: u32,
41    pub total_canisters: u32,
42}
43
44///
45/// FleetSubnetRootDrainingRequest
46///
47/// Controller command fencing new top-level Component allocation under exact active authority.
48///
49
50#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub struct FleetSubnetRootDrainingRequest {
52    pub operation_id: [u8; 32],
53    pub expected_registry: FleetRegistryVersion,
54}
55
56///
57/// FleetSubnetRootDrainingStatusRequest
58///
59/// Read-only lookup key for one durable root-draining fence.
60///
61
62#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
63pub struct FleetSubnetRootDrainingStatusRequest {
64    pub operation_id: [u8; 32],
65}
66
67///
68/// FleetSubnetRootDrainingResponse
69///
70/// Durable root-local admission cutoff and exact active authority frozen at that boundary.
71///
72
73#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
74pub struct FleetSubnetRootDrainingResponse {
75    pub operation_id: [u8; 32],
76    pub fleet_subnet_root: Principal,
77    pub placement_subnet: SubnetId,
78    pub active_registry: FleetRegistryVersion,
79    pub component_topology_digest: ComponentTopologyDigest,
80    pub active_release_set: FleetSubnetRootReleaseSet,
81    pub next_allocation_sequence: u64,
82    pub reserved_component_instances: u32,
83    pub committed_component_instances: u32,
84    pub managed_descendants: u32,
85    pub known_created_component_canisters: u32,
86    pub root_registry_encoded_bytes: u64,
87    pub started_at_ns: u64,
88}
89
90///
91/// FleetSubnetRootFinalInventoryRequest
92///
93/// Controller command freezing one exact terminal root-local inventory.
94///
95
96#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
97pub struct FleetSubnetRootFinalInventoryRequest {
98    pub operation_id: [u8; 32],
99    pub expected_registry: FleetRegistryVersion,
100}
101
102///
103/// FleetSubnetRootFinalInventoryStatusRequest
104///
105/// Read-only lookup key for one durable terminal root-local inventory.
106///
107
108#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
109pub struct FleetSubnetRootFinalInventoryStatusRequest {
110    pub operation_id: [u8; 32],
111}
112
113///
114/// FleetSubnetRootRemovalRequest
115///
116/// Controller command revalidating terminal Store authority before logical root removal.
117///
118
119#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
120pub struct FleetSubnetRootRemovalRequest {
121    pub operation_id: [u8; 32],
122    pub expected_registry: FleetRegistryVersion,
123}
124
125/// Read-only lookup key for one durable logical root-removal publication.
126#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
127pub struct FleetSubnetRootRemovalStatusRequest {
128    pub operation_id: [u8; 32],
129}
130
131///
132/// FleetSubnetRootStoreReclamationRequest
133///
134/// Controller command reclaiming the retained Store after exact logical root removal.
135///
136
137#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
138pub struct FleetSubnetRootStoreReclamationRequest {
139    pub operation_id: [u8; 32],
140    pub expected_final_inventory_hash: [u8; 32],
141}
142
143/// Read-only lookup key for one durable root Store-reclamation receipt.
144#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
145pub struct FleetSubnetRootStoreReclamationStatusRequest {
146    pub operation_id: [u8; 32],
147}
148
149///
150/// FleetSubnetRootStoreReclamationResponse
151///
152/// Durable proof that the logically removed root's retained Store completed exact GC.
153///
154
155#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156pub struct FleetSubnetRootStoreReclamationResponse {
157    pub operation_id: [u8; 32],
158    pub fleet_subnet_root: Principal,
159    pub wasm_store: Principal,
160    pub final_inventory_hash: [u8; 32],
161    pub reclaimed_store_bytes: u64,
162    pub reclaimed_catalog_entries: u32,
163    pub reclaimed_template_count: u32,
164    pub reclaimed_release_count: u32,
165    pub gc_prepared_at_secs: u64,
166    pub gc_started_at_secs: u64,
167    pub gc_completed_at_secs: u64,
168    pub gc_runs_completed: u32,
169    pub completed_at_ns: u64,
170    pub reclamation_hash: [u8; 32],
171}
172
173///
174/// FleetSubnetRootFinalInventoryResponse
175///
176/// Exact terminal Component history and retained write-fenced Store authority.
177///
178
179#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
180pub struct FleetSubnetRootFinalInventoryResponse {
181    pub operation_id: [u8; 32],
182    pub fleet_subnet_root: Principal,
183    pub placement_subnet: SubnetId,
184    pub registry: FleetRegistryVersion,
185    pub component_topology_digest: ComponentTopologyDigest,
186    pub active_release_set: FleetSubnetRootReleaseSet,
187    pub next_allocation_sequence: u64,
188    pub removed_component_instances: u32,
189    pub terminal_component_history_hash: [u8; 32],
190    pub root_registry_encoded_bytes: u64,
191    pub wasm_store: Principal,
192    pub wasm_store_catalog_hash: [u8; 32],
193    pub wasm_store_catalog_entries: u32,
194    pub wasm_store_occupied_bytes: u64,
195    pub wasm_store_template_count: u32,
196    pub wasm_store_release_count: u32,
197    pub wasm_store_gc_prepared_at_secs: u64,
198    pub finalized_at_ns: u64,
199    pub inventory_hash: [u8; 32],
200}
201
202///
203/// FleetSubnetRootInitArgs
204///
205/// Fresh-install authority plus the reinstall-local activation operation identity.
206///
207
208#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
209pub struct FleetSubnetRootInitArgs {
210    pub authority: FleetSubnetRootAuthority,
211    pub install_id: [u8; 32],
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::ids::{
218        AppId, CanonicalNetworkId, FleetBinding, FleetCoordinatorBinding, FleetId, FleetKey,
219        FleetRegistryAuthority,
220    };
221
222    #[test]
223    fn canister_summary_and_root_lifecycle_contracts_round_trip_through_candid() {
224        let summary = canister_summary();
225        let candid = candid::encode_one(&summary).expect("encode Canister summary");
226        let decoded: FleetSubnetRootCanisterSummary =
227            candid::decode_one(&candid).expect("decode Canister summary");
228
229        assert_eq!(decoded, summary);
230
231        let draining = draining_response(&summary);
232        let request = FleetSubnetRootDrainingRequest {
233            operation_id: draining.operation_id,
234            expected_registry: draining.active_registry.clone(),
235        };
236        let status = FleetSubnetRootDrainingStatusRequest {
237            operation_id: draining.operation_id,
238        };
239        let request_bytes = candid::encode_one(&request).expect("encode root draining request");
240        let status_bytes = candid::encode_one(status).expect("encode root draining status");
241        let response_bytes = candid::encode_one(&draining).expect("encode root draining response");
242        assert_eq!(
243            candid::decode_one::<FleetSubnetRootDrainingRequest>(&request_bytes)
244                .expect("decode root draining request"),
245            request
246        );
247        assert_eq!(
248            candid::decode_one::<FleetSubnetRootDrainingStatusRequest>(&status_bytes)
249                .expect("decode root draining status"),
250            status
251        );
252        assert_eq!(
253            candid::decode_one::<FleetSubnetRootDrainingResponse>(&response_bytes)
254                .expect("decode root draining response"),
255            draining
256        );
257
258        let inventory = final_inventory_response(&draining);
259        let inventory_request = FleetSubnetRootFinalInventoryRequest {
260            operation_id: inventory.operation_id,
261            expected_registry: inventory.registry.clone(),
262        };
263        let inventory_status = FleetSubnetRootFinalInventoryStatusRequest {
264            operation_id: inventory.operation_id,
265        };
266        let request_bytes =
267            candid::encode_one(&inventory_request).expect("encode root inventory request");
268        let status_bytes =
269            candid::encode_one(inventory_status).expect("encode root inventory status");
270        let response_bytes =
271            candid::encode_one(&inventory).expect("encode root inventory response");
272        assert_eq!(
273            candid::decode_one::<FleetSubnetRootFinalInventoryRequest>(&request_bytes)
274                .expect("decode root inventory request"),
275            inventory_request
276        );
277        assert_eq!(
278            candid::decode_one::<FleetSubnetRootFinalInventoryStatusRequest>(&status_bytes)
279                .expect("decode root inventory status"),
280            inventory_status
281        );
282        assert_eq!(
283            candid::decode_one::<FleetSubnetRootFinalInventoryResponse>(&response_bytes)
284                .expect("decode root inventory response"),
285            inventory
286        );
287    }
288
289    #[test]
290    fn draining_publication_contracts_round_trip_through_candid() {
291        let draining = draining_response(&canister_summary());
292        let publication = crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationRequest {
293            expected_registry: draining.active_registry.clone(),
294            root_draining: draining.clone(),
295        };
296        let publication_response =
297            crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationResponse {
298                root_draining: draining,
299                previous_version: publication.expected_registry.clone(),
300                version: FleetRegistryVersion {
301                    authority: publication.expected_registry.authority.clone(),
302                    revision: publication.expected_registry.revision + 1,
303                    content_hash: [19; 32],
304                },
305            };
306        let publication_bytes =
307            candid::encode_one(&publication).expect("encode root draining publication");
308        let publication_response_bytes = candid::encode_one(&publication_response)
309            .expect("encode root draining publication response");
310        assert_eq!(
311            candid::decode_one::<
312                crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationRequest,
313            >(&publication_bytes)
314            .expect("decode root draining publication"),
315            publication
316        );
317        assert_eq!(
318            candid::decode_one::<
319                crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationResponse,
320            >(&publication_response_bytes)
321            .expect("decode root draining publication response"),
322            publication_response
323        );
324
325        let final_inventory = final_inventory_response(&publication_response.root_draining);
326        let removal_request = FleetSubnetRootRemovalRequest {
327            operation_id: final_inventory.operation_id,
328            expected_registry: publication_response.version.clone(),
329        };
330        let removal_status = FleetSubnetRootRemovalStatusRequest {
331            operation_id: final_inventory.operation_id,
332        };
333        let coordinator_request =
334            crate::dto::fleet_registry::FleetSubnetRootRemovalPublicationRequest {
335                expected_registry: publication_response.version.clone(),
336                final_inventory: final_inventory.clone(),
337            };
338        let coordinator_response =
339            crate::dto::fleet_registry::FleetSubnetRootRemovalPublicationResponse {
340                final_inventory,
341                previous_version: publication_response.version.clone(),
342                version: FleetRegistryVersion {
343                    authority: publication_response.version.authority.clone(),
344                    revision: publication_response.version.revision + 1,
345                    content_hash: [29; 32],
346                },
347            };
348        assert_candid_round_trip(&removal_request);
349        assert_candid_round_trip(&removal_status);
350        assert_candid_round_trip(&coordinator_request);
351        assert_candid_round_trip(&coordinator_response);
352
353        let reclamation_request = FleetSubnetRootStoreReclamationRequest {
354            operation_id: coordinator_response.final_inventory.operation_id,
355            expected_final_inventory_hash: coordinator_response.final_inventory.inventory_hash,
356        };
357        let reclamation_status = FleetSubnetRootStoreReclamationStatusRequest {
358            operation_id: reclamation_request.operation_id,
359        };
360        let reclamation_response = FleetSubnetRootStoreReclamationResponse {
361            operation_id: reclamation_request.operation_id,
362            fleet_subnet_root: coordinator_response.final_inventory.fleet_subnet_root,
363            wasm_store: coordinator_response.final_inventory.wasm_store,
364            final_inventory_hash: reclamation_request.expected_final_inventory_hash,
365            reclaimed_store_bytes: coordinator_response
366                .final_inventory
367                .wasm_store_occupied_bytes,
368            reclaimed_catalog_entries: coordinator_response
369                .final_inventory
370                .wasm_store_catalog_entries,
371            reclaimed_template_count: coordinator_response
372                .final_inventory
373                .wasm_store_template_count,
374            reclaimed_release_count: coordinator_response
375                .final_inventory
376                .wasm_store_release_count,
377            gc_prepared_at_secs: coordinator_response
378                .final_inventory
379                .wasm_store_gc_prepared_at_secs,
380            gc_started_at_secs: 30,
381            gc_completed_at_secs: 31,
382            gc_runs_completed: 1,
383            completed_at_ns: 32,
384            reclamation_hash: [33; 32],
385        };
386        assert_candid_round_trip(&reclamation_request);
387        assert_candid_round_trip(&reclamation_status);
388        assert_candid_round_trip(&reclamation_response);
389    }
390
391    fn assert_candid_round_trip<T>(value: &T)
392    where
393        T: CandidType + for<'de> candid::Deserialize<'de> + Eq + std::fmt::Debug,
394    {
395        let bytes = candid::encode_one(value).expect("encode Candid contract");
396        assert_eq!(
397            &candid::decode_one::<T>(&bytes).expect("decode Candid contract"),
398            value,
399        );
400    }
401
402    fn canister_summary() -> FleetSubnetRootCanisterSummary {
403        FleetSubnetRootCanisterSummary {
404            fleet_registry: FleetRegistryVersion {
405                authority: FleetRegistryAuthority {
406                    binding: FleetCoordinatorBinding {
407                        fleet: FleetBinding {
408                            fleet: FleetKey {
409                                canonical_network_id: CanonicalNetworkId::ic_mainnet(),
410                                fleet_id: FleetId::from_generated_bytes([1; 32]),
411                            },
412                            app: AppId::from("toko"),
413                        },
414                        coordinator_subnet: SubnetId::from_principal(Principal::from_slice(
415                            &[2; 29],
416                        )),
417                        coordinator: Principal::from_slice(&[3; 29]),
418                    },
419                    epoch: 1,
420                },
421                revision: 4,
422                content_hash: [5; 32],
423            },
424            placement_subnet: SubnetId::from_principal(Principal::from_slice(&[6; 29])),
425            fleet_subnet_root: Principal::from_slice(&[7; 29]),
426            status: FleetSubnetRootStatus::Active,
427            infrastructure_canisters: 2,
428            component_canisters: 3,
429            total_canisters: 5,
430        }
431    }
432
433    fn draining_response(
434        summary: &FleetSubnetRootCanisterSummary,
435    ) -> FleetSubnetRootDrainingResponse {
436        FleetSubnetRootDrainingResponse {
437            operation_id: [8; 32],
438            fleet_subnet_root: summary.fleet_subnet_root,
439            placement_subnet: summary.placement_subnet,
440            active_registry: summary.fleet_registry.clone(),
441            component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
442            active_release_set: FleetSubnetRootReleaseSet {
443                release_build_id: crate::ids::ReleaseBuildId::from_nonce(
444                    crate::ids::ReleaseBuildNonce::from_random_bytes([10; 32]),
445                ),
446                manifest_digest: crate::ids::ReleaseSetDigest::from_bytes([11; 32]),
447            },
448            next_allocation_sequence: 12,
449            reserved_component_instances: 13,
450            committed_component_instances: 14,
451            managed_descendants: 15,
452            known_created_component_canisters: 16,
453            root_registry_encoded_bytes: 17_000,
454            started_at_ns: 18,
455        }
456    }
457
458    fn final_inventory_response(
459        draining: &FleetSubnetRootDrainingResponse,
460    ) -> FleetSubnetRootFinalInventoryResponse {
461        FleetSubnetRootFinalInventoryResponse {
462            operation_id: draining.operation_id,
463            fleet_subnet_root: draining.fleet_subnet_root,
464            placement_subnet: draining.placement_subnet,
465            registry: draining.active_registry.clone(),
466            component_topology_digest: draining.component_topology_digest,
467            active_release_set: draining.active_release_set,
468            next_allocation_sequence: draining.next_allocation_sequence,
469            removed_component_instances: 12,
470            terminal_component_history_hash: [19; 32],
471            root_registry_encoded_bytes: 20_000,
472            wasm_store: Principal::from_slice(&[21; 29]),
473            wasm_store_catalog_hash: [22; 32],
474            wasm_store_catalog_entries: 23,
475            wasm_store_occupied_bytes: 24_000,
476            wasm_store_template_count: 25,
477            wasm_store_release_count: 26,
478            wasm_store_gc_prepared_at_secs: 27,
479            finalized_at_ns: 28,
480            inventory_hash: [29; 32],
481        }
482    }
483}