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        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 temporary_controllers: Vec<Principal>,
64    pub final_controllers: Vec<Principal>,
65    pub adopted_at_ns: u64,
66}
67
68///
69/// FleetSubnetRootCanisterSummary
70///
71/// Compact live inventory bound to one root's exact active Fleet Registry mirror.
72///
73
74#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75pub struct FleetSubnetRootCanisterSummary {
76    pub fleet_registry: FleetRegistryVersion,
77    pub placement_subnet: SubnetId,
78    pub fleet_subnet_root: Principal,
79    pub status: FleetSubnetRootStatus,
80    pub infrastructure_canisters: u32,
81    pub component_canisters: u32,
82    pub pooled_canisters: u32,
83    pub total_canisters: u32,
84}
85
86///
87/// FleetSubnetRootDrainingRequest
88///
89/// Controller command fencing new top-level Component allocation under exact active authority.
90///
91
92#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
93pub struct FleetSubnetRootDrainingRequest {
94    pub operation_id: [u8; 32],
95    pub expected_registry: FleetRegistryVersion,
96}
97
98///
99/// FleetSubnetRootDrainingStatusRequest
100///
101/// Read-only lookup key for one durable root-draining fence.
102///
103
104#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
105pub struct FleetSubnetRootDrainingStatusRequest {
106    pub operation_id: [u8; 32],
107}
108
109///
110/// FleetSubnetRootDrainingResponse
111///
112/// Durable root-local admission cutoff and exact active authority frozen at that boundary.
113///
114
115#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
116pub struct FleetSubnetRootDrainingResponse {
117    pub operation_id: [u8; 32],
118    pub fleet_subnet_root: Principal,
119    pub placement_subnet: SubnetId,
120    pub active_registry: FleetRegistryVersion,
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    /// Existing prepaid empty Canisters the root must validate, reset, and adopt.
353    pub canister_pool_imports: Vec<Principal>,
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::ids::{
360        AppId, CanonicalNetworkId, FleetBinding, FleetCoordinatorBinding, FleetId, FleetKey,
361        FleetRegistryAuthority,
362    };
363
364    #[test]
365    fn canister_summary_and_root_lifecycle_contracts_round_trip_through_candid() {
366        let summary = canister_summary();
367        let candid = candid::encode_one(&summary).expect("encode Canister summary");
368        let decoded: FleetSubnetRootCanisterSummary =
369            candid::decode_one(&candid).expect("decode Canister summary");
370
371        assert_eq!(decoded, summary);
372
373        let draining = draining_response(&summary);
374        let request = FleetSubnetRootDrainingRequest {
375            operation_id: draining.operation_id,
376            expected_registry: draining.active_registry.clone(),
377        };
378        let status = FleetSubnetRootDrainingStatusRequest {
379            operation_id: draining.operation_id,
380        };
381        let request_bytes = candid::encode_one(&request).expect("encode root draining request");
382        let status_bytes = candid::encode_one(status).expect("encode root draining status");
383        let response_bytes = candid::encode_one(&draining).expect("encode root draining response");
384        assert_eq!(
385            candid::decode_one::<FleetSubnetRootDrainingRequest>(&request_bytes)
386                .expect("decode root draining request"),
387            request
388        );
389        assert_eq!(
390            candid::decode_one::<FleetSubnetRootDrainingStatusRequest>(&status_bytes)
391                .expect("decode root draining status"),
392            status
393        );
394        assert_eq!(
395            candid::decode_one::<FleetSubnetRootDrainingResponse>(&response_bytes)
396                .expect("decode root draining response"),
397            draining
398        );
399
400        let inventory = final_inventory_response(&draining);
401        let inventory_request = FleetSubnetRootFinalInventoryRequest {
402            operation_id: inventory.operation_id,
403            expected_registry: inventory.registry.clone(),
404        };
405        let inventory_status = FleetSubnetRootFinalInventoryStatusRequest {
406            operation_id: inventory.operation_id,
407        };
408        let request_bytes =
409            candid::encode_one(&inventory_request).expect("encode root inventory request");
410        let status_bytes =
411            candid::encode_one(inventory_status).expect("encode root inventory status");
412        let response_bytes =
413            candid::encode_one(&inventory).expect("encode root inventory response");
414        assert_eq!(
415            candid::decode_one::<FleetSubnetRootFinalInventoryRequest>(&request_bytes)
416                .expect("decode root inventory request"),
417            inventory_request
418        );
419        assert_eq!(
420            candid::decode_one::<FleetSubnetRootFinalInventoryStatusRequest>(&status_bytes)
421                .expect("decode root inventory status"),
422            inventory_status
423        );
424        assert_eq!(
425            candid::decode_one::<FleetSubnetRootFinalInventoryResponse>(&response_bytes)
426                .expect("decode root inventory response"),
427            inventory
428        );
429    }
430
431    #[test]
432    fn draining_publication_contracts_round_trip_through_candid() {
433        let draining = draining_response(&canister_summary());
434        let publication = crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationRequest {
435            expected_registry: draining.active_registry.clone(),
436            root_draining: draining.clone(),
437        };
438        let publication_response =
439            crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationResponse {
440                root_draining: draining,
441                previous_version: publication.expected_registry.clone(),
442                version: FleetRegistryVersion {
443                    authority: publication.expected_registry.authority.clone(),
444                    revision: publication.expected_registry.revision + 1,
445                    content_hash: [19; 32],
446                },
447            };
448        let publication_bytes =
449            candid::encode_one(&publication).expect("encode root draining publication");
450        let publication_response_bytes = candid::encode_one(&publication_response)
451            .expect("encode root draining publication response");
452        assert_eq!(
453            candid::decode_one::<
454                crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationRequest,
455            >(&publication_bytes)
456            .expect("decode root draining publication"),
457            publication
458        );
459        assert_eq!(
460            candid::decode_one::<
461                crate::dto::fleet_registry::FleetSubnetRootDrainingPublicationResponse,
462            >(&publication_response_bytes)
463            .expect("decode root draining publication response"),
464            publication_response
465        );
466
467        let final_inventory = final_inventory_response(&publication_response.root_draining);
468        let removal_request = FleetSubnetRootRemovalRequest {
469            operation_id: final_inventory.operation_id,
470            expected_registry: publication_response.version.clone(),
471        };
472        let removal_status = FleetSubnetRootRemovalStatusRequest {
473            operation_id: final_inventory.operation_id,
474        };
475        let coordinator_request =
476            crate::dto::fleet_registry::FleetSubnetRootRemovalPublicationRequest {
477                expected_registry: publication_response.version.clone(),
478                final_inventory: final_inventory.clone(),
479            };
480        let coordinator_response =
481            crate::dto::fleet_registry::FleetSubnetRootRemovalPublicationResponse {
482                final_inventory,
483                previous_version: publication_response.version.clone(),
484                version: FleetRegistryVersion {
485                    authority: publication_response.version.authority.clone(),
486                    revision: publication_response.version.revision + 1,
487                    content_hash: [29; 32],
488                },
489            };
490        assert_candid_round_trip(&removal_request);
491        assert_candid_round_trip(&removal_status);
492        assert_candid_round_trip(&coordinator_request);
493        assert_candid_round_trip(&coordinator_response);
494
495        let reclamation_request = FleetSubnetRootStoreReclamationRequest {
496            operation_id: coordinator_response.final_inventory.operation_id,
497            expected_final_inventory_hash: coordinator_response.final_inventory.inventory_hash,
498        };
499        let reclamation_status = FleetSubnetRootStoreReclamationStatusRequest {
500            operation_id: reclamation_request.operation_id,
501        };
502        let reclamation_response = FleetSubnetRootStoreReclamationResponse {
503            operation_id: reclamation_request.operation_id,
504            fleet_subnet_root: coordinator_response.final_inventory.fleet_subnet_root,
505            wasm_store: coordinator_response.final_inventory.wasm_store,
506            final_inventory_hash: reclamation_request.expected_final_inventory_hash,
507            reclaimed_store_bytes: coordinator_response
508                .final_inventory
509                .wasm_store_occupied_bytes,
510            reclaimed_catalog_entries: coordinator_response
511                .final_inventory
512                .wasm_store_catalog_entries,
513            reclaimed_template_count: coordinator_response
514                .final_inventory
515                .wasm_store_template_count,
516            reclaimed_release_count: coordinator_response
517                .final_inventory
518                .wasm_store_release_count,
519            gc_prepared_at_secs: coordinator_response
520                .final_inventory
521                .wasm_store_gc_prepared_at_secs,
522            gc_started_at_secs: 30,
523            gc_completed_at_secs: 31,
524            gc_runs_completed: 1,
525            completed_at_ns: 32,
526            reclamation_hash: [33; 32],
527        };
528        assert_candid_round_trip(&reclamation_request);
529        assert_candid_round_trip(&reclamation_status);
530        assert_candid_round_trip(&reclamation_response);
531
532        let finalization =
533            assert_store_binding_finalization_contract_round_trip(&reclamation_response);
534        assert_store_deletion_contract_round_trip(&finalization);
535    }
536
537    fn assert_store_binding_finalization_contract_round_trip(
538        reclamation: &FleetSubnetRootStoreReclamationResponse,
539    ) -> FleetSubnetRootStoreBindingFinalizationResponse {
540        let request = FleetSubnetRootStoreBindingFinalizationRequest {
541            operation_id: reclamation.operation_id,
542            expected_reclamation_hash: reclamation.reclamation_hash,
543        };
544        let status = FleetSubnetRootStoreBindingFinalizationStatusRequest {
545            operation_id: request.operation_id,
546        };
547        let response = FleetSubnetRootStoreBindingFinalizationResponse {
548            operation_id: request.operation_id,
549            fleet_subnet_root: reclamation.fleet_subnet_root,
550            wasm_store: reclamation.wasm_store,
551            final_inventory_hash: reclamation.final_inventory_hash,
552            reclamation_hash: request.expected_reclamation_hash,
553            source_generation: 4,
554            finalized_generation: 7,
555            finalized_at_secs: 34,
556            completed_at_ns: 35,
557            finalization_hash: [36; 32],
558        };
559        assert_candid_round_trip(&request);
560        assert_candid_round_trip(&status);
561        assert_candid_round_trip(&response);
562        response
563    }
564
565    fn assert_store_deletion_contract_round_trip(
566        finalization: &FleetSubnetRootStoreBindingFinalizationResponse,
567    ) {
568        let request = FleetSubnetRootStoreDeletionRequest {
569            operation_id: finalization.operation_id,
570            expected_binding_finalization_hash: finalization.finalization_hash,
571        };
572        let status = FleetSubnetRootStoreDeletionStatusRequest {
573            operation_id: request.operation_id,
574        };
575        let response = FleetSubnetRootStoreDeletionResponse {
576            operation_id: request.operation_id,
577            fleet_subnet_root: finalization.fleet_subnet_root,
578            wasm_store: finalization.wasm_store,
579            binding_finalization_hash: finalization.finalization_hash,
580            observed_module_hash: [37; 32],
581            observed_controllers: vec![finalization.fleet_subnet_root],
582            observed_cycles_before_reclamation: 500,
583            retained_cycles_target: 100,
584            observed_cycles_after_reclamation: 90,
585            cycles_reclaimed_at_ns: 38,
586            prepared_at_ns: 38,
587            observed_absent_at_ns: 39,
588            completed_at_ns: 40,
589            deletion_hash: [41; 32],
590        };
591        assert_candid_round_trip(&request);
592        assert_candid_round_trip(&status);
593        assert_candid_round_trip(&response);
594        assert_root_deletion_handoff_contract_round_trip(
595            &response,
596            finalization.final_inventory_hash,
597        );
598    }
599
600    fn assert_root_deletion_handoff_contract_round_trip(
601        store_deletion: &FleetSubnetRootStoreDeletionResponse,
602        final_inventory_hash: [u8; 32],
603    ) {
604        use crate::dto::fleet_registry::{
605            FleetSubnetRootDeletionReadinessIntentRequest,
606            FleetSubnetRootDeletionReadinessIntentResponse,
607            FleetSubnetRootDeletionReadinessRequest, FleetSubnetRootDeletionReadinessResponse,
608        };
609
610        let preparation_request = FleetSubnetRootDeletionPreparationRequest {
611            operation_id: store_deletion.operation_id,
612            expected_store_deletion_hash: store_deletion.deletion_hash,
613            retained_cycles_target: 100_000_000_001,
614            observed_reserved_cycles: 0,
615            observed_idle_cycles_burned_per_day: 86_400,
616            observed_freezing_threshold_seconds: 1,
617        };
618        let preparation_status = FleetSubnetRootDeletionPreparationStatusRequest {
619            operation_id: store_deletion.operation_id,
620        };
621        let intent_request = FleetSubnetRootDeletionReadinessIntentRequest {
622            operation_id: store_deletion.operation_id,
623            fleet_subnet_root: store_deletion.fleet_subnet_root,
624            final_inventory_hash,
625            store_deletion_hash: store_deletion.deletion_hash,
626            observed_cycles_before_reclamation: 500_000_000_000,
627            retained_cycles_target: 100_000_000_001,
628            observed_reserved_cycles: 0,
629            observed_idle_cycles_burned_per_day: 86_400,
630            observed_freezing_threshold_seconds: 1,
631            prepared_at_ns: 42,
632        };
633        let intent = FleetSubnetRootDeletionReadinessIntentResponse {
634            request: intent_request.clone(),
635            coordinator: Principal::from_slice(&[43; 29]),
636            recorded_at_ns: 44,
637            intent_hash: [45; 32],
638        };
639        let readiness_request = FleetSubnetRootDeletionReadinessRequest {
640            operation_id: store_deletion.operation_id,
641            fleet_subnet_root: store_deletion.fleet_subnet_root,
642            expected_intent_hash: intent.intent_hash,
643            observed_cycles_after_reclamation: 90_000_000_000,
644            cycles_reclaimed_at_ns: 46,
645        };
646        let readiness = FleetSubnetRootDeletionReadinessResponse {
647            request: readiness_request.clone(),
648            coordinator: intent.coordinator,
649            final_inventory_hash,
650            store_deletion_hash: store_deletion.deletion_hash,
651            observed_cycles_before_reclamation: 500_000_000_000,
652            retained_cycles_target: 100_000_000_001,
653            observed_reserved_cycles: 0,
654            observed_idle_cycles_burned_per_day: 86_400,
655            observed_freezing_threshold_seconds: 1,
656            prepared_at_ns: intent.request.prepared_at_ns,
657            recorded_at_ns: 47,
658            readiness_hash: [48; 32],
659        };
660        assert_root_deletion_execution_contract_round_trip(store_deletion, &readiness);
661
662        let preparation = FleetSubnetRootDeletionPreparationResponse {
663            operation_id: store_deletion.operation_id,
664            fleet_subnet_root: store_deletion.fleet_subnet_root,
665            coordinator: intent.coordinator,
666            final_inventory_hash,
667            store_deletion_hash: store_deletion.deletion_hash,
668            observed_cycles_before_reclamation: 500_000_000_000,
669            retained_cycles_target: 100_000_000_001,
670            observed_reserved_cycles: 0,
671            observed_idle_cycles_burned_per_day: 86_400,
672            observed_freezing_threshold_seconds: 1,
673            observed_cycles_after_reclamation: 90_000_000_000,
674            cycles_reclaimed_at_ns: readiness_request.cycles_reclaimed_at_ns,
675            coordinator_intent_hash: intent.intent_hash,
676            coordinator_readiness_hash: readiness.readiness_hash,
677            prepared_at_ns: intent.request.prepared_at_ns,
678            completed_at_ns: 56,
679        };
680
681        assert_candid_round_trip(&preparation_request);
682        assert_candid_round_trip(&preparation_status);
683        assert_candid_round_trip(&preparation);
684        assert_candid_round_trip(&intent_request);
685        assert_candid_round_trip(&intent);
686        assert_candid_round_trip(&readiness_request);
687        assert_candid_round_trip(&readiness);
688    }
689
690    fn assert_root_deletion_execution_contract_round_trip(
691        store_deletion: &FleetSubnetRootStoreDeletionResponse,
692        readiness: &crate::dto::fleet_registry::FleetSubnetRootDeletionReadinessResponse,
693    ) {
694        use crate::dto::fleet_registry::{
695            FleetSubnetRootDeletionCompletionRequest, FleetSubnetRootDeletionExecutionRequest,
696            FleetSubnetRootDeletionExecutionResponse, FleetSubnetRootDeletionResponse,
697            FleetSubnetRootDeletionStatusRequest,
698        };
699
700        let executor = Principal::from_slice(&[49; 29]);
701        let execution_request = FleetSubnetRootDeletionExecutionRequest {
702            operation_id: store_deletion.operation_id,
703            fleet_subnet_root: store_deletion.fleet_subnet_root,
704            expected_readiness_hash: readiness.readiness_hash,
705            observed_module_hash: [50; 32],
706            observed_controllers: vec![executor],
707            observed_cycles_after_reclamation: 90_000_000_000,
708            observed_reserved_cycles: 0,
709            observed_idle_cycles_burned_per_day: 86_400,
710            observed_freezing_threshold_seconds: 1,
711        };
712        let execution = FleetSubnetRootDeletionExecutionResponse {
713            request: execution_request.clone(),
714            executor,
715            prepared_at_ns: 51,
716            execution_hash: [52; 32],
717        };
718        let completion_request = FleetSubnetRootDeletionCompletionRequest {
719            operation_id: store_deletion.operation_id,
720            fleet_subnet_root: store_deletion.fleet_subnet_root,
721            expected_execution_hash: execution.execution_hash,
722            observed_absent_at_ns: 53,
723        };
724        let status = FleetSubnetRootDeletionStatusRequest {
725            operation_id: store_deletion.operation_id,
726            fleet_subnet_root: store_deletion.fleet_subnet_root,
727        };
728        let deletion = FleetSubnetRootDeletionResponse {
729            operation_id: store_deletion.operation_id,
730            fleet_subnet_root: store_deletion.fleet_subnet_root,
731            coordinator: readiness.coordinator,
732            executor,
733            readiness_hash: readiness.readiness_hash,
734            execution_hash: execution.execution_hash,
735            observed_module_hash: execution_request.observed_module_hash,
736            observed_controllers: execution_request.observed_controllers.clone(),
737            observed_cycles_after_reclamation: 90_000_000_000,
738            observed_absent_at_ns: completion_request.observed_absent_at_ns,
739            completed_at_ns: 54,
740            deletion_hash: [55; 32],
741        };
742        assert_candid_round_trip(&execution_request);
743        assert_candid_round_trip(&execution);
744        assert_candid_round_trip(&completion_request);
745        assert_candid_round_trip(&status);
746        assert_candid_round_trip(&deletion);
747    }
748
749    fn assert_candid_round_trip<T>(value: &T)
750    where
751        T: CandidType + for<'de> candid::Deserialize<'de> + Eq + std::fmt::Debug,
752    {
753        let bytes = candid::encode_one(value).expect("encode Candid contract");
754        assert_eq!(
755            &candid::decode_one::<T>(&bytes).expect("decode Candid contract"),
756            value,
757        );
758    }
759
760    fn canister_summary() -> FleetSubnetRootCanisterSummary {
761        FleetSubnetRootCanisterSummary {
762            fleet_registry: FleetRegistryVersion {
763                authority: FleetRegistryAuthority {
764                    binding: FleetCoordinatorBinding {
765                        fleet: FleetBinding {
766                            fleet: FleetKey {
767                                canonical_network_id: CanonicalNetworkId::ic_mainnet(),
768                                fleet_id: FleetId::from_generated_bytes([1; 32]),
769                            },
770                            app: AppId::from("toko"),
771                        },
772                        coordinator_subnet: SubnetId::from_principal(Principal::from_slice(
773                            &[2; 29],
774                        )),
775                        coordinator: Principal::from_slice(&[3; 29]),
776                    },
777                    epoch: 1,
778                },
779                revision: 4,
780                content_hash: [5; 32],
781            },
782            placement_subnet: SubnetId::from_principal(Principal::from_slice(&[6; 29])),
783            fleet_subnet_root: Principal::from_slice(&[7; 29]),
784            status: FleetSubnetRootStatus::Active,
785            infrastructure_canisters: 2,
786            component_canisters: 3,
787            pooled_canisters: 4,
788            total_canisters: 9,
789        }
790    }
791
792    fn draining_response(
793        summary: &FleetSubnetRootCanisterSummary,
794    ) -> FleetSubnetRootDrainingResponse {
795        FleetSubnetRootDrainingResponse {
796            operation_id: [8; 32],
797            fleet_subnet_root: summary.fleet_subnet_root,
798            placement_subnet: summary.placement_subnet,
799            active_registry: summary.fleet_registry.clone(),
800            component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
801            active_release_set: FleetSubnetRootReleaseSet {
802                release_build_id: crate::ids::ReleaseBuildId::from_nonce(
803                    crate::ids::ReleaseBuildNonce::from_random_bytes([10; 32]),
804                ),
805                manifest_digest: crate::ids::ReleaseSetDigest::from_bytes([11; 32]),
806            },
807            next_allocation_sequence: 12,
808            reserved_component_instances: 13,
809            committed_component_instances: 14,
810            managed_descendants: 15,
811            known_created_component_canisters: 16,
812            root_registry_encoded_bytes: 17_000,
813            started_at_ns: 18,
814        }
815    }
816
817    fn final_inventory_response(
818        draining: &FleetSubnetRootDrainingResponse,
819    ) -> FleetSubnetRootFinalInventoryResponse {
820        FleetSubnetRootFinalInventoryResponse {
821            operation_id: draining.operation_id,
822            fleet_subnet_root: draining.fleet_subnet_root,
823            placement_subnet: draining.placement_subnet,
824            registry: draining.active_registry.clone(),
825            component_topology_digest: draining.component_topology_digest,
826            active_release_set: draining.active_release_set,
827            next_allocation_sequence: draining.next_allocation_sequence,
828            removed_component_instances: 12,
829            terminal_component_history_hash: [19; 32],
830            root_registry_encoded_bytes: 20_000,
831            wasm_store: Principal::from_slice(&[21; 29]),
832            wasm_store_catalog_hash: [22; 32],
833            wasm_store_catalog_entries: 23,
834            wasm_store_occupied_bytes: 24_000,
835            wasm_store_template_count: 25,
836            wasm_store_release_count: 26,
837            wasm_store_gc_prepared_at_secs: 27,
838            finalized_at_ns: 28,
839            inventory_hash: [29; 32],
840        }
841    }
842}