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