Skip to main content

canic_core/dto/
fleet_subnet_root.rs

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