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