Skip to main content

canic_core/dto/
fleet_subnet_root.rs

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