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/// Controller command finalizing the reclaimed Store's root-local binding.
174#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
175pub struct FleetSubnetRootStoreBindingFinalizationRequest {
176    pub operation_id: [u8; 32],
177    pub expected_reclamation_hash: [u8; 32],
178}
179
180/// Read-only lookup key for one durable Store-binding finalization receipt.
181#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
182pub struct FleetSubnetRootStoreBindingFinalizationStatusRequest {
183    pub operation_id: [u8; 32],
184}
185
186/// Durable proof that the reclaimed Store no longer occupies a publication binding slot.
187#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
188pub struct FleetSubnetRootStoreBindingFinalizationResponse {
189    pub operation_id: [u8; 32],
190    pub fleet_subnet_root: Principal,
191    pub wasm_store: Principal,
192    pub final_inventory_hash: [u8; 32],
193    pub reclamation_hash: [u8; 32],
194    pub source_generation: u64,
195    pub finalized_generation: u64,
196    pub finalized_at_secs: u64,
197    pub completed_at_ns: u64,
198    pub finalization_hash: [u8; 32],
199}
200
201///
202/// FleetSubnetRootFinalInventoryResponse
203///
204/// Exact terminal Component history and retained write-fenced Store authority.
205///
206
207#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
208pub struct FleetSubnetRootFinalInventoryResponse {
209    pub operation_id: [u8; 32],
210    pub fleet_subnet_root: Principal,
211    pub placement_subnet: SubnetId,
212    pub registry: FleetRegistryVersion,
213    pub component_topology_digest: ComponentTopologyDigest,
214    pub active_release_set: FleetSubnetRootReleaseSet,
215    pub next_allocation_sequence: u64,
216    pub removed_component_instances: u32,
217    pub terminal_component_history_hash: [u8; 32],
218    pub root_registry_encoded_bytes: u64,
219    pub wasm_store: Principal,
220    pub wasm_store_catalog_hash: [u8; 32],
221    pub wasm_store_catalog_entries: u32,
222    pub wasm_store_occupied_bytes: u64,
223    pub wasm_store_template_count: u32,
224    pub wasm_store_release_count: u32,
225    pub wasm_store_gc_prepared_at_secs: u64,
226    pub finalized_at_ns: u64,
227    pub inventory_hash: [u8; 32],
228}
229
230///
231/// FleetSubnetRootInitArgs
232///
233/// Fresh-install authority plus the reinstall-local activation operation identity.
234///
235
236#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
237pub struct FleetSubnetRootInitArgs {
238    pub authority: FleetSubnetRootAuthority,
239    pub install_id: [u8; 32],
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::ids::{
246        AppId, CanonicalNetworkId, FleetBinding, FleetCoordinatorBinding, FleetId, FleetKey,
247        FleetRegistryAuthority,
248    };
249
250    #[test]
251    fn canister_summary_and_root_lifecycle_contracts_round_trip_through_candid() {
252        let summary = canister_summary();
253        let candid = candid::encode_one(&summary).expect("encode Canister summary");
254        let decoded: FleetSubnetRootCanisterSummary =
255            candid::decode_one(&candid).expect("decode Canister summary");
256
257        assert_eq!(decoded, summary);
258
259        let draining = draining_response(&summary);
260        let request = FleetSubnetRootDrainingRequest {
261            operation_id: draining.operation_id,
262            expected_registry: draining.active_registry.clone(),
263        };
264        let status = FleetSubnetRootDrainingStatusRequest {
265            operation_id: draining.operation_id,
266        };
267        let request_bytes = candid::encode_one(&request).expect("encode root draining request");
268        let status_bytes = candid::encode_one(status).expect("encode root draining status");
269        let response_bytes = candid::encode_one(&draining).expect("encode root draining response");
270        assert_eq!(
271            candid::decode_one::<FleetSubnetRootDrainingRequest>(&request_bytes)
272                .expect("decode root draining request"),
273            request
274        );
275        assert_eq!(
276            candid::decode_one::<FleetSubnetRootDrainingStatusRequest>(&status_bytes)
277                .expect("decode root draining status"),
278            status
279        );
280        assert_eq!(
281            candid::decode_one::<FleetSubnetRootDrainingResponse>(&response_bytes)
282                .expect("decode root draining response"),
283            draining
284        );
285
286        let inventory = final_inventory_response(&draining);
287        let inventory_request = FleetSubnetRootFinalInventoryRequest {
288            operation_id: inventory.operation_id,
289            expected_registry: inventory.registry.clone(),
290        };
291        let inventory_status = FleetSubnetRootFinalInventoryStatusRequest {
292            operation_id: inventory.operation_id,
293        };
294        let request_bytes =
295            candid::encode_one(&inventory_request).expect("encode root inventory request");
296        let status_bytes =
297            candid::encode_one(inventory_status).expect("encode root inventory status");
298        let response_bytes =
299            candid::encode_one(&inventory).expect("encode root inventory response");
300        assert_eq!(
301            candid::decode_one::<FleetSubnetRootFinalInventoryRequest>(&request_bytes)
302                .expect("decode root inventory request"),
303            inventory_request
304        );
305        assert_eq!(
306            candid::decode_one::<FleetSubnetRootFinalInventoryStatusRequest>(&status_bytes)
307                .expect("decode root inventory status"),
308            inventory_status
309        );
310        assert_eq!(
311            candid::decode_one::<FleetSubnetRootFinalInventoryResponse>(&response_bytes)
312                .expect("decode root inventory response"),
313            inventory
314        );
315    }
316
317    #[test]
318    fn draining_publication_contracts_round_trip_through_candid() {
319        let draining = draining_response(&canister_summary());
320        let publication = crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationRequest {
321            expected_registry: draining.active_registry.clone(),
322            root_draining: draining.clone(),
323        };
324        let publication_response =
325            crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationResponse {
326                root_draining: draining,
327                previous_version: publication.expected_registry.clone(),
328                version: FleetRegistryVersion {
329                    authority: publication.expected_registry.authority.clone(),
330                    revision: publication.expected_registry.revision + 1,
331                    content_hash: [19; 32],
332                },
333            };
334        let publication_bytes =
335            candid::encode_one(&publication).expect("encode root draining publication");
336        let publication_response_bytes = candid::encode_one(&publication_response)
337            .expect("encode root draining publication response");
338        assert_eq!(
339            candid::decode_one::<
340                crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationRequest,
341            >(&publication_bytes)
342            .expect("decode root draining publication"),
343            publication
344        );
345        assert_eq!(
346            candid::decode_one::<
347                crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationResponse,
348            >(&publication_response_bytes)
349            .expect("decode root draining publication response"),
350            publication_response
351        );
352
353        let final_inventory = final_inventory_response(&publication_response.root_draining);
354        let removal_request = FleetSubnetRootRemovalRequest {
355            operation_id: final_inventory.operation_id,
356            expected_registry: publication_response.version.clone(),
357        };
358        let removal_status = FleetSubnetRootRemovalStatusRequest {
359            operation_id: final_inventory.operation_id,
360        };
361        let coordinator_request =
362            crate::dto::fleet_registry::FleetSubnetRootRemovalPublicationRequest {
363                expected_registry: publication_response.version.clone(),
364                final_inventory: final_inventory.clone(),
365            };
366        let coordinator_response =
367            crate::dto::fleet_registry::FleetSubnetRootRemovalPublicationResponse {
368                final_inventory,
369                previous_version: publication_response.version.clone(),
370                version: FleetRegistryVersion {
371                    authority: publication_response.version.authority.clone(),
372                    revision: publication_response.version.revision + 1,
373                    content_hash: [29; 32],
374                },
375            };
376        assert_candid_round_trip(&removal_request);
377        assert_candid_round_trip(&removal_status);
378        assert_candid_round_trip(&coordinator_request);
379        assert_candid_round_trip(&coordinator_response);
380
381        let reclamation_request = FleetSubnetRootStoreReclamationRequest {
382            operation_id: coordinator_response.final_inventory.operation_id,
383            expected_final_inventory_hash: coordinator_response.final_inventory.inventory_hash,
384        };
385        let reclamation_status = FleetSubnetRootStoreReclamationStatusRequest {
386            operation_id: reclamation_request.operation_id,
387        };
388        let reclamation_response = FleetSubnetRootStoreReclamationResponse {
389            operation_id: reclamation_request.operation_id,
390            fleet_subnet_root: coordinator_response.final_inventory.fleet_subnet_root,
391            wasm_store: coordinator_response.final_inventory.wasm_store,
392            final_inventory_hash: reclamation_request.expected_final_inventory_hash,
393            reclaimed_store_bytes: coordinator_response
394                .final_inventory
395                .wasm_store_occupied_bytes,
396            reclaimed_catalog_entries: coordinator_response
397                .final_inventory
398                .wasm_store_catalog_entries,
399            reclaimed_template_count: coordinator_response
400                .final_inventory
401                .wasm_store_template_count,
402            reclaimed_release_count: coordinator_response
403                .final_inventory
404                .wasm_store_release_count,
405            gc_prepared_at_secs: coordinator_response
406                .final_inventory
407                .wasm_store_gc_prepared_at_secs,
408            gc_started_at_secs: 30,
409            gc_completed_at_secs: 31,
410            gc_runs_completed: 1,
411            completed_at_ns: 32,
412            reclamation_hash: [33; 32],
413        };
414        assert_candid_round_trip(&reclamation_request);
415        assert_candid_round_trip(&reclamation_status);
416        assert_candid_round_trip(&reclamation_response);
417
418        assert_store_binding_finalization_contract_round_trip(&reclamation_response);
419    }
420
421    fn assert_store_binding_finalization_contract_round_trip(
422        reclamation: &FleetSubnetRootStoreReclamationResponse,
423    ) {
424        let request = FleetSubnetRootStoreBindingFinalizationRequest {
425            operation_id: reclamation.operation_id,
426            expected_reclamation_hash: reclamation.reclamation_hash,
427        };
428        let status = FleetSubnetRootStoreBindingFinalizationStatusRequest {
429            operation_id: request.operation_id,
430        };
431        let response = FleetSubnetRootStoreBindingFinalizationResponse {
432            operation_id: request.operation_id,
433            fleet_subnet_root: reclamation.fleet_subnet_root,
434            wasm_store: reclamation.wasm_store,
435            final_inventory_hash: reclamation.final_inventory_hash,
436            reclamation_hash: request.expected_reclamation_hash,
437            source_generation: 4,
438            finalized_generation: 7,
439            finalized_at_secs: 34,
440            completed_at_ns: 35,
441            finalization_hash: [36; 32],
442        };
443        assert_candid_round_trip(&request);
444        assert_candid_round_trip(&status);
445        assert_candid_round_trip(&response);
446    }
447
448    fn assert_candid_round_trip<T>(value: &T)
449    where
450        T: CandidType + for<'de> candid::Deserialize<'de> + Eq + std::fmt::Debug,
451    {
452        let bytes = candid::encode_one(value).expect("encode Candid contract");
453        assert_eq!(
454            &candid::decode_one::<T>(&bytes).expect("decode Candid contract"),
455            value,
456        );
457    }
458
459    fn canister_summary() -> FleetSubnetRootCanisterSummary {
460        FleetSubnetRootCanisterSummary {
461            fleet_registry: FleetRegistryVersion {
462                authority: FleetRegistryAuthority {
463                    binding: FleetCoordinatorBinding {
464                        fleet: FleetBinding {
465                            fleet: FleetKey {
466                                canonical_network_id: CanonicalNetworkId::ic_mainnet(),
467                                fleet_id: FleetId::from_generated_bytes([1; 32]),
468                            },
469                            app: AppId::from("toko"),
470                        },
471                        coordinator_subnet: SubnetId::from_principal(Principal::from_slice(
472                            &[2; 29],
473                        )),
474                        coordinator: Principal::from_slice(&[3; 29]),
475                    },
476                    epoch: 1,
477                },
478                revision: 4,
479                content_hash: [5; 32],
480            },
481            placement_subnet: SubnetId::from_principal(Principal::from_slice(&[6; 29])),
482            fleet_subnet_root: Principal::from_slice(&[7; 29]),
483            status: FleetSubnetRootStatus::Active,
484            infrastructure_canisters: 2,
485            component_canisters: 3,
486            total_canisters: 5,
487        }
488    }
489
490    fn draining_response(
491        summary: &FleetSubnetRootCanisterSummary,
492    ) -> FleetSubnetRootDrainingResponse {
493        FleetSubnetRootDrainingResponse {
494            operation_id: [8; 32],
495            fleet_subnet_root: summary.fleet_subnet_root,
496            placement_subnet: summary.placement_subnet,
497            active_registry: summary.fleet_registry.clone(),
498            component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
499            active_release_set: FleetSubnetRootReleaseSet {
500                release_build_id: crate::ids::ReleaseBuildId::from_nonce(
501                    crate::ids::ReleaseBuildNonce::from_random_bytes([10; 32]),
502                ),
503                manifest_digest: crate::ids::ReleaseSetDigest::from_bytes([11; 32]),
504            },
505            next_allocation_sequence: 12,
506            reserved_component_instances: 13,
507            committed_component_instances: 14,
508            managed_descendants: 15,
509            known_created_component_canisters: 16,
510            root_registry_encoded_bytes: 17_000,
511            started_at_ns: 18,
512        }
513    }
514
515    fn final_inventory_response(
516        draining: &FleetSubnetRootDrainingResponse,
517    ) -> FleetSubnetRootFinalInventoryResponse {
518        FleetSubnetRootFinalInventoryResponse {
519            operation_id: draining.operation_id,
520            fleet_subnet_root: draining.fleet_subnet_root,
521            placement_subnet: draining.placement_subnet,
522            registry: draining.active_registry.clone(),
523            component_topology_digest: draining.component_topology_digest,
524            active_release_set: draining.active_release_set,
525            next_allocation_sequence: draining.next_allocation_sequence,
526            removed_component_instances: 12,
527            terminal_component_history_hash: [19; 32],
528            root_registry_encoded_bytes: 20_000,
529            wasm_store: Principal::from_slice(&[21; 29]),
530            wasm_store_catalog_hash: [22; 32],
531            wasm_store_catalog_entries: 23,
532            wasm_store_occupied_bytes: 24_000,
533            wasm_store_template_count: 25,
534            wasm_store_release_count: 26,
535            wasm_store_gc_prepared_at_secs: 27,
536            finalized_at_ns: 28,
537            inventory_hash: [29; 32],
538        }
539    }
540}