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/// Execution balance retained while a removed root completes its deletion handoff.
15pub const FLEET_SUBNET_ROOT_DELETION_EXECUTION_RESERVE_CYCLES: u128 = 100_000_000_000;
16
17/// Margin for management-call refunds that become visible after a cycle transfer returns.
18pub const FLEET_SUBNET_ROOT_DELETION_CALL_REFUND_HEADROOM_CYCLES: u128 = 50_000_000_000;
19
20/// Fail-closed ceiling for cycles intentionally left on a root that will be deleted.
21pub const FLEET_SUBNET_ROOT_DELETION_MAXIMUM_RETAINED_CYCLES: u128 = 1_000_000_000_000;
22
23///
24/// FleetSubnetRootAuthority
25///
26/// Exact immutable root binding, initial release set, and installed module identity.
27///
28
29#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
30pub struct FleetSubnetRootAuthority {
31    pub binding: FleetSubnetRootBinding,
32    pub initial_release_set: FleetSubnetRootReleaseSet,
33    pub expected_module_hash: [u8; 32],
34}
35
36///
37/// FleetSubnetRootCanisterSummary
38///
39/// Compact live inventory bound to one root's exact active Fleet Registry mirror.
40///
41
42#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
43pub struct FleetSubnetRootCanisterSummary {
44    pub fleet_registry: FleetRegistryVersion,
45    pub placement_subnet: SubnetId,
46    pub fleet_subnet_root: Principal,
47    pub status: FleetSubnetRootStatus,
48    pub infrastructure_canisters: u32,
49    pub component_canisters: u32,
50    pub pooled_canisters: u32,
51    pub total_canisters: u32,
52}
53
54///
55/// FleetSubnetRootDrainingRequest
56///
57/// Controller command fencing new top-level Component allocation under exact active authority.
58///
59
60#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
61pub struct FleetSubnetRootDrainingRequest {
62    pub operation_id: [u8; 32],
63    pub expected_registry: FleetRegistryVersion,
64}
65
66///
67/// FleetSubnetRootDrainingStatusRequest
68///
69/// Read-only lookup key for one durable root-draining fence.
70///
71
72#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
73pub struct FleetSubnetRootDrainingStatusRequest {
74    pub operation_id: [u8; 32],
75}
76
77///
78/// FleetSubnetRootDrainingResponse
79///
80/// Durable root-local admission cutoff and exact active authority frozen at that boundary.
81///
82
83#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
84pub struct FleetSubnetRootDrainingResponse {
85    pub operation_id: [u8; 32],
86    pub fleet_subnet_root: Principal,
87    pub placement_subnet: SubnetId,
88    pub active_registry: FleetRegistryVersion,
89    pub component_topology_digest: ComponentTopologyDigest,
90    pub active_release_set: FleetSubnetRootReleaseSet,
91    pub next_allocation_sequence: u64,
92    pub reserved_component_instances: u32,
93    pub committed_component_instances: u32,
94    pub managed_descendants: u32,
95    pub known_created_component_canisters: u32,
96    pub root_registry_encoded_bytes: u64,
97    pub started_at_ns: u64,
98}
99
100///
101/// FleetSubnetRootFinalInventoryRequest
102///
103/// Controller command freezing one exact terminal root-local inventory.
104///
105
106#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
107pub struct FleetSubnetRootFinalInventoryRequest {
108    pub operation_id: [u8; 32],
109    pub expected_registry: FleetRegistryVersion,
110}
111
112///
113/// FleetSubnetRootFinalInventoryStatusRequest
114///
115/// Read-only lookup key for one durable terminal root-local inventory.
116///
117
118#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
119pub struct FleetSubnetRootFinalInventoryStatusRequest {
120    pub operation_id: [u8; 32],
121}
122
123///
124/// FleetSubnetRootRemovalRequest
125///
126/// Controller command revalidating terminal Store authority before logical root removal.
127///
128
129#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
130pub struct FleetSubnetRootRemovalRequest {
131    pub operation_id: [u8; 32],
132    pub expected_registry: FleetRegistryVersion,
133}
134
135/// Read-only lookup key for one durable logical root-removal publication.
136#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
137pub struct FleetSubnetRootRemovalStatusRequest {
138    pub operation_id: [u8; 32],
139}
140
141///
142/// FleetSubnetRootStoreReclamationRequest
143///
144/// Controller command reclaiming the retained Store after exact logical root removal.
145///
146
147#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
148pub struct FleetSubnetRootStoreReclamationRequest {
149    pub operation_id: [u8; 32],
150    pub expected_final_inventory_hash: [u8; 32],
151}
152
153/// Read-only lookup key for one durable root Store-reclamation receipt.
154#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
155pub struct FleetSubnetRootStoreReclamationStatusRequest {
156    pub operation_id: [u8; 32],
157}
158
159///
160/// FleetSubnetRootStoreReclamationResponse
161///
162/// Durable proof that the logically removed root's retained Store completed exact GC.
163///
164
165#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
166pub struct FleetSubnetRootStoreReclamationResponse {
167    pub operation_id: [u8; 32],
168    pub fleet_subnet_root: Principal,
169    pub wasm_store: Principal,
170    pub final_inventory_hash: [u8; 32],
171    pub reclaimed_store_bytes: u64,
172    pub reclaimed_catalog_entries: u32,
173    pub reclaimed_template_count: u32,
174    pub reclaimed_release_count: u32,
175    pub gc_prepared_at_secs: u64,
176    pub gc_started_at_secs: u64,
177    pub gc_completed_at_secs: u64,
178    pub gc_runs_completed: u32,
179    pub completed_at_ns: u64,
180    pub reclamation_hash: [u8; 32],
181}
182
183/// Controller command finalizing the reclaimed Store's root-local binding.
184#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
185pub struct FleetSubnetRootStoreBindingFinalizationRequest {
186    pub operation_id: [u8; 32],
187    pub expected_reclamation_hash: [u8; 32],
188}
189
190/// Read-only lookup key for one durable Store-binding finalization receipt.
191#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
192pub struct FleetSubnetRootStoreBindingFinalizationStatusRequest {
193    pub operation_id: [u8; 32],
194}
195
196/// Durable proof that the reclaimed Store no longer occupies a publication binding slot.
197#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
198pub struct FleetSubnetRootStoreBindingFinalizationResponse {
199    pub operation_id: [u8; 32],
200    pub fleet_subnet_root: Principal,
201    pub wasm_store: Principal,
202    pub final_inventory_hash: [u8; 32],
203    pub reclamation_hash: [u8; 32],
204    pub source_generation: u64,
205    pub finalized_generation: u64,
206    pub finalized_at_secs: u64,
207    pub completed_at_ns: u64,
208    pub finalization_hash: [u8; 32],
209}
210
211/// Controller command physically deleting the reclaimed and unbound Store.
212#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
213pub struct FleetSubnetRootStoreDeletionRequest {
214    pub operation_id: [u8; 32],
215    pub expected_binding_finalization_hash: [u8; 32],
216}
217
218/// Read-only lookup key for one durable Store-deletion receipt.
219#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
220pub struct FleetSubnetRootStoreDeletionStatusRequest {
221    pub operation_id: [u8; 32],
222}
223
224/// Durable proof that the root's reclaimed and unbound Store is physically absent.
225#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
226pub struct FleetSubnetRootStoreDeletionResponse {
227    pub operation_id: [u8; 32],
228    pub fleet_subnet_root: Principal,
229    pub wasm_store: Principal,
230    pub binding_finalization_hash: [u8; 32],
231    pub observed_module_hash: [u8; 32],
232    pub observed_controllers: Vec<Principal>,
233    pub observed_cycles_before_reclamation: u128,
234    pub maximum_cycles_to_retain: u128,
235    pub observed_cycles_after_reclamation: u128,
236    pub cycles_reclaimed_at_ns: u64,
237    pub prepared_at_ns: u64,
238    pub observed_absent_at_ns: u64,
239    pub completed_at_ns: u64,
240    pub deletion_hash: [u8; 32],
241}
242
243/// Controller command preparing a removed root for external physical deletion.
244#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
245pub struct FleetSubnetRootDeletionPreparationRequest {
246    pub operation_id: [u8; 32],
247    pub expected_store_deletion_hash: [u8; 32],
248    pub maximum_cycles_to_retain: u128,
249    pub observed_reserved_cycles: u128,
250    pub observed_idle_cycles_burned_per_day: u128,
251    pub observed_freezing_threshold_seconds: u128,
252}
253
254/// Read-only lookup key for the root's durable external-deletion readiness receipt.
255#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
256pub struct FleetSubnetRootDeletionPreparationStatusRequest {
257    pub operation_id: [u8; 32],
258}
259
260/// Durable proof that a removed root returned excess cycles and is ready for its executor.
261#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
262pub struct FleetSubnetRootDeletionPreparationResponse {
263    pub operation_id: [u8; 32],
264    pub fleet_subnet_root: Principal,
265    pub coordinator: Principal,
266    pub final_inventory_hash: [u8; 32],
267    pub store_deletion_hash: [u8; 32],
268    pub observed_cycles_before_reclamation: u128,
269    pub maximum_cycles_to_retain: u128,
270    pub observed_reserved_cycles: u128,
271    pub observed_idle_cycles_burned_per_day: u128,
272    pub observed_freezing_threshold_seconds: u128,
273    pub observed_cycles_after_reclamation: u128,
274    pub cycles_reclaimed_at_ns: u64,
275    pub coordinator_intent_hash: [u8; 32],
276    pub coordinator_readiness_hash: [u8; 32],
277    pub prepared_at_ns: u64,
278    pub completed_at_ns: u64,
279}
280
281///
282/// FleetSubnetRootFinalInventoryResponse
283///
284/// Exact terminal Component history and retained write-fenced Store authority.
285///
286
287#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
288pub struct FleetSubnetRootFinalInventoryResponse {
289    pub operation_id: [u8; 32],
290    pub fleet_subnet_root: Principal,
291    pub placement_subnet: SubnetId,
292    pub registry: FleetRegistryVersion,
293    pub component_topology_digest: ComponentTopologyDigest,
294    pub active_release_set: FleetSubnetRootReleaseSet,
295    pub next_allocation_sequence: u64,
296    pub removed_component_instances: u32,
297    pub terminal_component_history_hash: [u8; 32],
298    pub root_registry_encoded_bytes: u64,
299    pub wasm_store: Principal,
300    pub wasm_store_catalog_hash: [u8; 32],
301    pub wasm_store_catalog_entries: u32,
302    pub wasm_store_occupied_bytes: u64,
303    pub wasm_store_template_count: u32,
304    pub wasm_store_release_count: u32,
305    pub wasm_store_gc_prepared_at_secs: u64,
306    pub finalized_at_ns: u64,
307    pub inventory_hash: [u8; 32],
308}
309
310///
311/// FleetSubnetRootInitArgs
312///
313/// Fresh-install authority plus the reinstall-local activation operation identity.
314///
315
316#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
317pub struct FleetSubnetRootInitArgs {
318    pub authority: FleetSubnetRootAuthority,
319    pub install_id: [u8; 32],
320    /// Existing prepaid empty Canisters the root must validate, reset, and adopt.
321    pub canister_pool_imports: Vec<Principal>,
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::ids::{
328        AppId, CanonicalNetworkId, FleetBinding, FleetCoordinatorBinding, FleetId, FleetKey,
329        FleetRegistryAuthority,
330    };
331
332    #[test]
333    fn canister_summary_and_root_lifecycle_contracts_round_trip_through_candid() {
334        let summary = canister_summary();
335        let candid = candid::encode_one(&summary).expect("encode Canister summary");
336        let decoded: FleetSubnetRootCanisterSummary =
337            candid::decode_one(&candid).expect("decode Canister summary");
338
339        assert_eq!(decoded, summary);
340
341        let draining = draining_response(&summary);
342        let request = FleetSubnetRootDrainingRequest {
343            operation_id: draining.operation_id,
344            expected_registry: draining.active_registry.clone(),
345        };
346        let status = FleetSubnetRootDrainingStatusRequest {
347            operation_id: draining.operation_id,
348        };
349        let request_bytes = candid::encode_one(&request).expect("encode root draining request");
350        let status_bytes = candid::encode_one(status).expect("encode root draining status");
351        let response_bytes = candid::encode_one(&draining).expect("encode root draining response");
352        assert_eq!(
353            candid::decode_one::<FleetSubnetRootDrainingRequest>(&request_bytes)
354                .expect("decode root draining request"),
355            request
356        );
357        assert_eq!(
358            candid::decode_one::<FleetSubnetRootDrainingStatusRequest>(&status_bytes)
359                .expect("decode root draining status"),
360            status
361        );
362        assert_eq!(
363            candid::decode_one::<FleetSubnetRootDrainingResponse>(&response_bytes)
364                .expect("decode root draining response"),
365            draining
366        );
367
368        let inventory = final_inventory_response(&draining);
369        let inventory_request = FleetSubnetRootFinalInventoryRequest {
370            operation_id: inventory.operation_id,
371            expected_registry: inventory.registry.clone(),
372        };
373        let inventory_status = FleetSubnetRootFinalInventoryStatusRequest {
374            operation_id: inventory.operation_id,
375        };
376        let request_bytes =
377            candid::encode_one(&inventory_request).expect("encode root inventory request");
378        let status_bytes =
379            candid::encode_one(inventory_status).expect("encode root inventory status");
380        let response_bytes =
381            candid::encode_one(&inventory).expect("encode root inventory response");
382        assert_eq!(
383            candid::decode_one::<FleetSubnetRootFinalInventoryRequest>(&request_bytes)
384                .expect("decode root inventory request"),
385            inventory_request
386        );
387        assert_eq!(
388            candid::decode_one::<FleetSubnetRootFinalInventoryStatusRequest>(&status_bytes)
389                .expect("decode root inventory status"),
390            inventory_status
391        );
392        assert_eq!(
393            candid::decode_one::<FleetSubnetRootFinalInventoryResponse>(&response_bytes)
394                .expect("decode root inventory response"),
395            inventory
396        );
397    }
398
399    #[test]
400    fn draining_publication_contracts_round_trip_through_candid() {
401        let draining = draining_response(&canister_summary());
402        let publication = crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationRequest {
403            expected_registry: draining.active_registry.clone(),
404            root_draining: draining.clone(),
405        };
406        let publication_response =
407            crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationResponse {
408                root_draining: draining,
409                previous_version: publication.expected_registry.clone(),
410                version: FleetRegistryVersion {
411                    authority: publication.expected_registry.authority.clone(),
412                    revision: publication.expected_registry.revision + 1,
413                    content_hash: [19; 32],
414                },
415            };
416        let publication_bytes =
417            candid::encode_one(&publication).expect("encode root draining publication");
418        let publication_response_bytes = candid::encode_one(&publication_response)
419            .expect("encode root draining publication response");
420        assert_eq!(
421            candid::decode_one::<
422                crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationRequest,
423            >(&publication_bytes)
424            .expect("decode root draining publication"),
425            publication
426        );
427        assert_eq!(
428            candid::decode_one::<
429                crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationResponse,
430            >(&publication_response_bytes)
431            .expect("decode root draining publication response"),
432            publication_response
433        );
434
435        let final_inventory = final_inventory_response(&publication_response.root_draining);
436        let removal_request = FleetSubnetRootRemovalRequest {
437            operation_id: final_inventory.operation_id,
438            expected_registry: publication_response.version.clone(),
439        };
440        let removal_status = FleetSubnetRootRemovalStatusRequest {
441            operation_id: final_inventory.operation_id,
442        };
443        let coordinator_request =
444            crate::dto::fleet_registry::FleetSubnetRootRemovalPublicationRequest {
445                expected_registry: publication_response.version.clone(),
446                final_inventory: final_inventory.clone(),
447            };
448        let coordinator_response =
449            crate::dto::fleet_registry::FleetSubnetRootRemovalPublicationResponse {
450                final_inventory,
451                previous_version: publication_response.version.clone(),
452                version: FleetRegistryVersion {
453                    authority: publication_response.version.authority.clone(),
454                    revision: publication_response.version.revision + 1,
455                    content_hash: [29; 32],
456                },
457            };
458        assert_candid_round_trip(&removal_request);
459        assert_candid_round_trip(&removal_status);
460        assert_candid_round_trip(&coordinator_request);
461        assert_candid_round_trip(&coordinator_response);
462
463        let reclamation_request = FleetSubnetRootStoreReclamationRequest {
464            operation_id: coordinator_response.final_inventory.operation_id,
465            expected_final_inventory_hash: coordinator_response.final_inventory.inventory_hash,
466        };
467        let reclamation_status = FleetSubnetRootStoreReclamationStatusRequest {
468            operation_id: reclamation_request.operation_id,
469        };
470        let reclamation_response = FleetSubnetRootStoreReclamationResponse {
471            operation_id: reclamation_request.operation_id,
472            fleet_subnet_root: coordinator_response.final_inventory.fleet_subnet_root,
473            wasm_store: coordinator_response.final_inventory.wasm_store,
474            final_inventory_hash: reclamation_request.expected_final_inventory_hash,
475            reclaimed_store_bytes: coordinator_response
476                .final_inventory
477                .wasm_store_occupied_bytes,
478            reclaimed_catalog_entries: coordinator_response
479                .final_inventory
480                .wasm_store_catalog_entries,
481            reclaimed_template_count: coordinator_response
482                .final_inventory
483                .wasm_store_template_count,
484            reclaimed_release_count: coordinator_response
485                .final_inventory
486                .wasm_store_release_count,
487            gc_prepared_at_secs: coordinator_response
488                .final_inventory
489                .wasm_store_gc_prepared_at_secs,
490            gc_started_at_secs: 30,
491            gc_completed_at_secs: 31,
492            gc_runs_completed: 1,
493            completed_at_ns: 32,
494            reclamation_hash: [33; 32],
495        };
496        assert_candid_round_trip(&reclamation_request);
497        assert_candid_round_trip(&reclamation_status);
498        assert_candid_round_trip(&reclamation_response);
499
500        let finalization =
501            assert_store_binding_finalization_contract_round_trip(&reclamation_response);
502        assert_store_deletion_contract_round_trip(&finalization);
503    }
504
505    fn assert_store_binding_finalization_contract_round_trip(
506        reclamation: &FleetSubnetRootStoreReclamationResponse,
507    ) -> FleetSubnetRootStoreBindingFinalizationResponse {
508        let request = FleetSubnetRootStoreBindingFinalizationRequest {
509            operation_id: reclamation.operation_id,
510            expected_reclamation_hash: reclamation.reclamation_hash,
511        };
512        let status = FleetSubnetRootStoreBindingFinalizationStatusRequest {
513            operation_id: request.operation_id,
514        };
515        let response = FleetSubnetRootStoreBindingFinalizationResponse {
516            operation_id: request.operation_id,
517            fleet_subnet_root: reclamation.fleet_subnet_root,
518            wasm_store: reclamation.wasm_store,
519            final_inventory_hash: reclamation.final_inventory_hash,
520            reclamation_hash: request.expected_reclamation_hash,
521            source_generation: 4,
522            finalized_generation: 7,
523            finalized_at_secs: 34,
524            completed_at_ns: 35,
525            finalization_hash: [36; 32],
526        };
527        assert_candid_round_trip(&request);
528        assert_candid_round_trip(&status);
529        assert_candid_round_trip(&response);
530        response
531    }
532
533    fn assert_store_deletion_contract_round_trip(
534        finalization: &FleetSubnetRootStoreBindingFinalizationResponse,
535    ) {
536        let request = FleetSubnetRootStoreDeletionRequest {
537            operation_id: finalization.operation_id,
538            expected_binding_finalization_hash: finalization.finalization_hash,
539        };
540        let status = FleetSubnetRootStoreDeletionStatusRequest {
541            operation_id: request.operation_id,
542        };
543        let response = FleetSubnetRootStoreDeletionResponse {
544            operation_id: request.operation_id,
545            fleet_subnet_root: finalization.fleet_subnet_root,
546            wasm_store: finalization.wasm_store,
547            binding_finalization_hash: finalization.finalization_hash,
548            observed_module_hash: [37; 32],
549            observed_controllers: vec![finalization.fleet_subnet_root],
550            observed_cycles_before_reclamation: 500,
551            maximum_cycles_to_retain: 100,
552            observed_cycles_after_reclamation: 90,
553            cycles_reclaimed_at_ns: 38,
554            prepared_at_ns: 38,
555            observed_absent_at_ns: 39,
556            completed_at_ns: 40,
557            deletion_hash: [41; 32],
558        };
559        assert_candid_round_trip(&request);
560        assert_candid_round_trip(&status);
561        assert_candid_round_trip(&response);
562        assert_root_deletion_handoff_contract_round_trip(
563            &response,
564            finalization.final_inventory_hash,
565        );
566    }
567
568    fn assert_root_deletion_handoff_contract_round_trip(
569        store_deletion: &FleetSubnetRootStoreDeletionResponse,
570        final_inventory_hash: [u8; 32],
571    ) {
572        use crate::dto::fleet_registry::{
573            FleetSubnetRootDeletionReadinessIntentRequest,
574            FleetSubnetRootDeletionReadinessIntentResponse,
575            FleetSubnetRootDeletionReadinessRequest, FleetSubnetRootDeletionReadinessResponse,
576        };
577
578        let preparation_request = FleetSubnetRootDeletionPreparationRequest {
579            operation_id: store_deletion.operation_id,
580            expected_store_deletion_hash: store_deletion.deletion_hash,
581            maximum_cycles_to_retain: 100_000_000_001,
582            observed_reserved_cycles: 0,
583            observed_idle_cycles_burned_per_day: 86_400,
584            observed_freezing_threshold_seconds: 1,
585        };
586        let preparation_status = FleetSubnetRootDeletionPreparationStatusRequest {
587            operation_id: store_deletion.operation_id,
588        };
589        let intent_request = FleetSubnetRootDeletionReadinessIntentRequest {
590            operation_id: store_deletion.operation_id,
591            fleet_subnet_root: store_deletion.fleet_subnet_root,
592            final_inventory_hash,
593            store_deletion_hash: store_deletion.deletion_hash,
594            observed_cycles_before_reclamation: 500_000_000_000,
595            maximum_cycles_to_retain: 100_000_000_001,
596            observed_reserved_cycles: 0,
597            observed_idle_cycles_burned_per_day: 86_400,
598            observed_freezing_threshold_seconds: 1,
599            prepared_at_ns: 42,
600        };
601        let intent = FleetSubnetRootDeletionReadinessIntentResponse {
602            request: intent_request.clone(),
603            coordinator: Principal::from_slice(&[43; 29]),
604            recorded_at_ns: 44,
605            intent_hash: [45; 32],
606        };
607        let readiness_request = FleetSubnetRootDeletionReadinessRequest {
608            operation_id: store_deletion.operation_id,
609            fleet_subnet_root: store_deletion.fleet_subnet_root,
610            expected_intent_hash: intent.intent_hash,
611            observed_cycles_after_reclamation: 90_000_000_000,
612            cycles_reclaimed_at_ns: 46,
613        };
614        let readiness = FleetSubnetRootDeletionReadinessResponse {
615            request: readiness_request.clone(),
616            coordinator: intent.coordinator,
617            final_inventory_hash,
618            store_deletion_hash: store_deletion.deletion_hash,
619            observed_cycles_before_reclamation: 500_000_000_000,
620            maximum_cycles_to_retain: 100_000_000_001,
621            observed_reserved_cycles: 0,
622            observed_idle_cycles_burned_per_day: 86_400,
623            observed_freezing_threshold_seconds: 1,
624            prepared_at_ns: intent.request.prepared_at_ns,
625            recorded_at_ns: 47,
626            readiness_hash: [48; 32],
627        };
628        assert_root_deletion_execution_contract_round_trip(store_deletion, &readiness);
629
630        let preparation = FleetSubnetRootDeletionPreparationResponse {
631            operation_id: store_deletion.operation_id,
632            fleet_subnet_root: store_deletion.fleet_subnet_root,
633            coordinator: intent.coordinator,
634            final_inventory_hash,
635            store_deletion_hash: store_deletion.deletion_hash,
636            observed_cycles_before_reclamation: 500_000_000_000,
637            maximum_cycles_to_retain: 100_000_000_001,
638            observed_reserved_cycles: 0,
639            observed_idle_cycles_burned_per_day: 86_400,
640            observed_freezing_threshold_seconds: 1,
641            observed_cycles_after_reclamation: 90_000_000_000,
642            cycles_reclaimed_at_ns: readiness_request.cycles_reclaimed_at_ns,
643            coordinator_intent_hash: intent.intent_hash,
644            coordinator_readiness_hash: readiness.readiness_hash,
645            prepared_at_ns: intent.request.prepared_at_ns,
646            completed_at_ns: 56,
647        };
648
649        assert_candid_round_trip(&preparation_request);
650        assert_candid_round_trip(&preparation_status);
651        assert_candid_round_trip(&preparation);
652        assert_candid_round_trip(&intent_request);
653        assert_candid_round_trip(&intent);
654        assert_candid_round_trip(&readiness_request);
655        assert_candid_round_trip(&readiness);
656    }
657
658    fn assert_root_deletion_execution_contract_round_trip(
659        store_deletion: &FleetSubnetRootStoreDeletionResponse,
660        readiness: &crate::dto::fleet_registry::FleetSubnetRootDeletionReadinessResponse,
661    ) {
662        use crate::dto::fleet_registry::{
663            FleetSubnetRootDeletionCompletionRequest, FleetSubnetRootDeletionExecutionRequest,
664            FleetSubnetRootDeletionExecutionResponse, FleetSubnetRootDeletionResponse,
665            FleetSubnetRootDeletionStatusRequest,
666        };
667
668        let executor = Principal::from_slice(&[49; 29]);
669        let execution_request = FleetSubnetRootDeletionExecutionRequest {
670            operation_id: store_deletion.operation_id,
671            fleet_subnet_root: store_deletion.fleet_subnet_root,
672            expected_readiness_hash: readiness.readiness_hash,
673            observed_module_hash: [50; 32],
674            observed_controllers: vec![executor],
675            observed_cycles_after_reclamation: 90_000_000_000,
676            observed_reserved_cycles: 0,
677            observed_idle_cycles_burned_per_day: 86_400,
678            observed_freezing_threshold_seconds: 1,
679        };
680        let execution = FleetSubnetRootDeletionExecutionResponse {
681            request: execution_request.clone(),
682            executor,
683            prepared_at_ns: 51,
684            execution_hash: [52; 32],
685        };
686        let completion_request = FleetSubnetRootDeletionCompletionRequest {
687            operation_id: store_deletion.operation_id,
688            fleet_subnet_root: store_deletion.fleet_subnet_root,
689            expected_execution_hash: execution.execution_hash,
690            observed_absent_at_ns: 53,
691        };
692        let status = FleetSubnetRootDeletionStatusRequest {
693            operation_id: store_deletion.operation_id,
694            fleet_subnet_root: store_deletion.fleet_subnet_root,
695        };
696        let deletion = FleetSubnetRootDeletionResponse {
697            operation_id: store_deletion.operation_id,
698            fleet_subnet_root: store_deletion.fleet_subnet_root,
699            coordinator: readiness.coordinator,
700            executor,
701            readiness_hash: readiness.readiness_hash,
702            execution_hash: execution.execution_hash,
703            observed_module_hash: execution_request.observed_module_hash,
704            observed_controllers: execution_request.observed_controllers.clone(),
705            observed_cycles_after_reclamation: 90_000_000_000,
706            observed_absent_at_ns: completion_request.observed_absent_at_ns,
707            completed_at_ns: 54,
708            deletion_hash: [55; 32],
709        };
710        assert_candid_round_trip(&execution_request);
711        assert_candid_round_trip(&execution);
712        assert_candid_round_trip(&completion_request);
713        assert_candid_round_trip(&status);
714        assert_candid_round_trip(&deletion);
715    }
716
717    fn assert_candid_round_trip<T>(value: &T)
718    where
719        T: CandidType + for<'de> candid::Deserialize<'de> + Eq + std::fmt::Debug,
720    {
721        let bytes = candid::encode_one(value).expect("encode Candid contract");
722        assert_eq!(
723            &candid::decode_one::<T>(&bytes).expect("decode Candid contract"),
724            value,
725        );
726    }
727
728    fn canister_summary() -> FleetSubnetRootCanisterSummary {
729        FleetSubnetRootCanisterSummary {
730            fleet_registry: FleetRegistryVersion {
731                authority: FleetRegistryAuthority {
732                    binding: FleetCoordinatorBinding {
733                        fleet: FleetBinding {
734                            fleet: FleetKey {
735                                canonical_network_id: CanonicalNetworkId::ic_mainnet(),
736                                fleet_id: FleetId::from_generated_bytes([1; 32]),
737                            },
738                            app: AppId::from("toko"),
739                        },
740                        coordinator_subnet: SubnetId::from_principal(Principal::from_slice(
741                            &[2; 29],
742                        )),
743                        coordinator: Principal::from_slice(&[3; 29]),
744                    },
745                    epoch: 1,
746                },
747                revision: 4,
748                content_hash: [5; 32],
749            },
750            placement_subnet: SubnetId::from_principal(Principal::from_slice(&[6; 29])),
751            fleet_subnet_root: Principal::from_slice(&[7; 29]),
752            status: FleetSubnetRootStatus::Active,
753            infrastructure_canisters: 2,
754            component_canisters: 3,
755            pooled_canisters: 4,
756            total_canisters: 9,
757        }
758    }
759
760    fn draining_response(
761        summary: &FleetSubnetRootCanisterSummary,
762    ) -> FleetSubnetRootDrainingResponse {
763        FleetSubnetRootDrainingResponse {
764            operation_id: [8; 32],
765            fleet_subnet_root: summary.fleet_subnet_root,
766            placement_subnet: summary.placement_subnet,
767            active_registry: summary.fleet_registry.clone(),
768            component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
769            active_release_set: FleetSubnetRootReleaseSet {
770                release_build_id: crate::ids::ReleaseBuildId::from_nonce(
771                    crate::ids::ReleaseBuildNonce::from_random_bytes([10; 32]),
772                ),
773                manifest_digest: crate::ids::ReleaseSetDigest::from_bytes([11; 32]),
774            },
775            next_allocation_sequence: 12,
776            reserved_component_instances: 13,
777            committed_component_instances: 14,
778            managed_descendants: 15,
779            known_created_component_canisters: 16,
780            root_registry_encoded_bytes: 17_000,
781            started_at_ns: 18,
782        }
783    }
784
785    fn final_inventory_response(
786        draining: &FleetSubnetRootDrainingResponse,
787    ) -> FleetSubnetRootFinalInventoryResponse {
788        FleetSubnetRootFinalInventoryResponse {
789            operation_id: draining.operation_id,
790            fleet_subnet_root: draining.fleet_subnet_root,
791            placement_subnet: draining.placement_subnet,
792            registry: draining.active_registry.clone(),
793            component_topology_digest: draining.component_topology_digest,
794            active_release_set: draining.active_release_set,
795            next_allocation_sequence: draining.next_allocation_sequence,
796            removed_component_instances: 12,
797            terminal_component_history_hash: [19; 32],
798            root_registry_encoded_bytes: 20_000,
799            wasm_store: Principal::from_slice(&[21; 29]),
800            wasm_store_catalog_hash: [22; 32],
801            wasm_store_catalog_entries: 23,
802            wasm_store_occupied_bytes: 24_000,
803            wasm_store_template_count: 25,
804            wasm_store_release_count: 26,
805            wasm_store_gc_prepared_at_secs: 27,
806            finalized_at_ns: 28,
807            inventory_hash: [29; 32],
808        }
809    }
810}