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