Skip to main content

canic_core/dto/
component_registry.rs

1//! Module: dto::component_registry
2//!
3//! Responsibility: carry root-local Component Registry preparation and allocation evidence.
4//! Does not own: admission policy, stable mutation, artifact resolution, or lifecycle effects.
5//! Boundary: callers name intent and Spec while the root allocates identity under verified authority.
6
7use crate::{
8    cdk::types::Cycles,
9    config::schema::ComponentChildKind,
10    dto::{
11        fleet_registry::{FleetDirectorySnapshot, FleetRegistryVersion},
12        root_store::RootStoreBootstrapRequest,
13    },
14    ids::{
15        CanisterRole, ComponentBinding, ComponentChildBinding, ComponentInstanceId,
16        ComponentSpecId, ComponentTopologyDigest, FleetSubnetRootReleaseSet,
17        ManagedCanisterBinding,
18    },
19};
20use candid::{CandidType, Principal};
21use serde::{Deserialize, Serialize};
22
23///
24/// RootComponentRegistryPreparationRequest
25///
26/// Exact authority required before an empty root-local Component Registry may be prepared.
27///
28
29#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30pub struct RootComponentRegistryPreparationRequest {
31    pub store_bootstrap: RootStoreBootstrapRequest,
32    pub expected_fleet_registry: FleetRegistryVersion,
33}
34
35///
36/// RootComponentInitialInventoryStatus
37///
38/// Durable initial Component inventory sealed for one Fleet Subnet Root activation.
39///
40
41#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct RootComponentInitialInventoryStatus {
43    pub fleet_activation_operation_id: [u8; 32],
44    pub component_count: u32,
45    pub inventory_hash: [u8; 32],
46    pub sealed_at_ns: u64,
47    pub directories_converged: bool,
48    pub root_runtime_activated: bool,
49}
50
51///
52/// RootComponentRegistryStatusResponse
53///
54/// Compact durable Component Registry authority and current allocation counters.
55///
56
57#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
58pub struct RootComponentRegistryStatusResponse {
59    pub fleet_subnet_root: Principal,
60    pub prepared_against_registry: FleetRegistryVersion,
61    pub release_set: FleetSubnetRootReleaseSet,
62    pub component_topology_digest: ComponentTopologyDigest,
63    pub next_allocation_sequence: u64,
64    pub reserved_component_instances: u32,
65    pub committed_component_instances: u32,
66    pub managed_descendants: u32,
67    pub known_created_component_canisters: u32,
68    pub encoded_bytes: u64,
69    pub initial_inventory: Option<RootComponentInitialInventoryStatus>,
70}
71
72///
73/// RootComponentAllocationRequest
74///
75/// Controller command naming one idempotent top-level Component reservation intent.
76///
77
78#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
79pub struct RootComponentAllocationRequest {
80    pub operation_id: [u8; 32],
81    pub component_spec: ComponentSpecId,
82}
83
84///
85/// RootComponentAllocationStatusRequest
86///
87/// Read-only lookup key for one durable top-level Component allocation operation.
88///
89
90#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
91pub struct RootComponentAllocationStatusRequest {
92    pub operation_id: [u8; 32],
93}
94
95///
96/// RootComponentChildAllocationRequest
97///
98/// Parent command naming one idempotent direct-child reservation intent.
99///
100
101#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102pub struct RootComponentChildAllocationRequest {
103    pub operation_id: [u8; 32],
104    pub component: ComponentInstanceId,
105    pub expected_registry: ComponentRegistryHead,
106    pub child_role: CanisterRole,
107}
108
109///
110/// RootComponentChildAllocationStatusRequest
111///
112/// Parent lookup key for one durable direct-child reservation.
113///
114
115#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
116pub struct RootComponentChildAllocationStatusRequest {
117    pub operation_id: [u8; 32],
118    pub component: ComponentInstanceId,
119}
120
121///
122/// RootComponentChildCreationRequest
123///
124/// Parent command continuing one already reserved direct-child operation.
125///
126
127#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
128pub struct RootComponentChildCreationRequest {
129    pub operation_id: [u8; 32],
130    pub component: ComponentInstanceId,
131}
132
133///
134/// RootComponentChildInstallRequest
135///
136/// Parent command installing and verifying one already created direct-child operation.
137///
138
139#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
140pub struct RootComponentChildInstallRequest {
141    pub operation_id: [u8; 32],
142    pub component: ComponentInstanceId,
143}
144
145///
146/// RootComponentCreationRequest
147///
148/// Controller command continuing one already reserved top-level Component operation.
149///
150
151#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
152pub struct RootComponentCreationRequest {
153    pub operation_id: [u8; 32],
154}
155
156///
157/// RootComponentInstallRequest
158///
159/// Controller command continuing one already created top-level Component operation.
160///
161
162#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
163pub struct RootComponentInstallRequest {
164    pub operation_id: [u8; 32],
165}
166
167///
168/// RootComponentCommitRequest
169///
170/// Controller command committing one already verified top-level Component operation.
171///
172
173#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
174pub struct RootComponentCommitRequest {
175    pub operation_id: [u8; 32],
176}
177
178///
179/// RootComponentDirectoryPreparationRequest
180///
181/// Controller command distributing exact Directories to one committed top-level Component.
182///
183
184#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
185pub struct RootComponentDirectoryPreparationRequest {
186    pub operation_id: [u8; 32],
187}
188
189///
190/// RootComponentRuntimeActivationRequest
191///
192/// Controller command activating one Directory-prepared top-level Component runtime.
193///
194
195#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
196pub struct RootComponentRuntimeActivationRequest {
197    pub operation_id: [u8; 32],
198}
199
200///
201/// RootComponentMembershipActivationRequest
202///
203/// Controller command activating one runtime-active Component's Registry membership.
204///
205
206#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
207pub struct RootComponentMembershipActivationRequest {
208    pub operation_id: [u8; 32],
209}
210
211///
212/// ComponentProvisioningOrigin
213///
214/// Authenticated causal authority retained with one top-level Component allocation.
215///
216
217#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
218pub enum ComponentProvisioningOrigin {
219    FleetAdministrator { caller: Principal },
220}
221
222///
223/// RootComponentAllocationPhase
224///
225/// Durable root-local progress of one top-level Component allocation operation.
226///
227
228#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
229pub enum RootComponentAllocationPhase {
230    Reserved,
231    CreationIntent,
232    Created,
233    InstallIntent,
234    Installed,
235    Verified,
236    Committed,
237}
238
239///
240/// ComponentLifecycleStatus
241///
242/// Root-owned runtime lifecycle state of one committed Component Registry member.
243///
244
245#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
246pub enum ComponentLifecycleStatus {
247    Prepared,
248    Active,
249    Draining,
250    Removed,
251}
252
253///
254/// ComponentRegistryHead
255///
256/// Exact independently versioned authority of one Component Registry partition.
257///
258
259#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
260pub struct ComponentRegistryHead {
261    pub component: ComponentInstanceId,
262    pub revision: u64,
263    pub content_hash: [u8; 32],
264}
265
266///
267/// ComponentRegistryPartitionRequest
268///
269/// Read-only lookup key for one committed Component Registry partition.
270///
271
272#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
273pub struct ComponentRegistryPartitionRequest {
274    pub component: ComponentInstanceId,
275}
276
277///
278/// ComponentRegistryPartitionResponse
279///
280/// Protected top-level row and independent head of one Component Registry partition.
281///
282
283#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
284pub struct ComponentRegistryPartitionResponse {
285    pub head: ComponentRegistryHead,
286    pub binding: ComponentBinding,
287    pub provisioning_origin: ComponentProvisioningOrigin,
288    pub release_set: FleetSubnetRootReleaseSet,
289    pub status: ComponentLifecycleStatus,
290    pub reserved_descendants: u32,
291    pub committed_descendants: u32,
292    pub encoded_bytes: u64,
293}
294
295///
296/// ComponentDirectoryProvenance
297///
298/// Exact Component Registry authority from which one Component Directory is derived.
299///
300
301#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
302pub struct ComponentDirectoryProvenance {
303    pub component: ComponentBinding,
304    pub source_fleet_subnet_root: Principal,
305    pub component_registry_revision: u64,
306    pub component_registry_content_hash: [u8; 32],
307    pub synchronized_at_ns: u64,
308}
309
310///
311/// ComponentDirectoryHead
312///
313/// Compact independently versioned discovery projection for one Component tree.
314///
315
316#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
317pub struct ComponentDirectoryHead {
318    pub provenance: ComponentDirectoryProvenance,
319    pub descendant_count: u32,
320}
321
322///
323/// ComponentDirectoryHeadRequest
324///
325/// Read-only lookup key for one committed Component Directory head.
326///
327
328#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
329pub struct ComponentDirectoryHeadRequest {
330    pub component: ComponentInstanceId,
331}
332
333///
334/// ComponentRuntimeDirectoryAuthority
335///
336/// Exact Fleet and Component discovery authority retained by one managed Component-tree node.
337///
338
339#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
340pub struct ComponentRuntimeDirectoryAuthority {
341    pub fleet: FleetDirectorySnapshot,
342    pub component: ComponentDirectoryHead,
343}
344
345///
346/// ComponentRuntimeDirectoryPreparationRequest
347///
348/// Root-issued exact Directory preparation command for one managed Component-tree node.
349///
350
351#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
352pub struct ComponentRuntimeDirectoryPreparationRequest {
353    pub operation_id: [u8; 32],
354    pub authority: ComponentRuntimeDirectoryAuthority,
355}
356
357///
358/// ComponentRuntimeDirectorySynchronizationRequest
359///
360/// Root-issued replacement of one active managed Component node's current Directory authority.
361///
362
363#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
364pub struct ComponentRuntimeDirectorySynchronizationRequest {
365    pub operation_id: [u8; 32],
366    pub authority: ComponentRuntimeDirectoryAuthority,
367}
368
369///
370/// ComponentRuntimePhase
371///
372/// Target-local progress from installation through Component runtime activation.
373///
374
375#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
376pub enum ComponentRuntimePhase {
377    AwaitingDirectory,
378    DirectoryPrepared,
379    Active,
380}
381
382///
383/// ComponentRuntimeActivationEvidence
384///
385/// Exact retained Directory authority under which one Component runtime became Active.
386///
387
388#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
389pub struct ComponentRuntimeActivationEvidence {
390    pub directory_authority_hash: [u8; 32],
391    pub activated_at_ns: u64,
392}
393
394///
395/// ComponentRuntimeActivationRequest
396///
397/// Root-issued exact activation command for one Directory-prepared managed Component node.
398///
399
400#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
401pub struct ComponentRuntimeActivationRequest {
402    pub operation_id: [u8; 32],
403    pub directory_authority_hash: [u8; 32],
404}
405
406///
407/// ComponentRuntimeStatusResponse
408///
409/// Independently observable target-local binding and exact retained Directory authority.
410///
411
412#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
413pub struct ComponentRuntimeStatusResponse {
414    pub operation_id: [u8; 32],
415    pub binding: ManagedCanisterBinding,
416    pub phase: ComponentRuntimePhase,
417    pub authority: Option<ComponentRuntimeDirectoryAuthority>,
418    pub authority_hash: Option<[u8; 32]>,
419    pub activation: Option<ComponentRuntimeActivationEvidence>,
420}
421
422///
423/// RootComponentCreationEvidence
424///
425/// Exact Store artifact and root-owned creation settings frozen before the paid effect.
426///
427
428#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
429pub struct RootComponentCreationEvidence {
430    pub wasm_store: Principal,
431    pub payload_hash: [u8; 32],
432    pub payload_size_bytes: u64,
433    pub initial_cycles: Cycles,
434    pub controller: Principal,
435    pub canister: Option<Principal>,
436}
437
438///
439/// RootComponentInstallEvidence
440///
441/// Exact raw artifact, chunk source and immutable target binding frozen before installation.
442///
443
444#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
445pub struct RootComponentInstallEvidence {
446    pub raw_module_hash: [u8; 32],
447    pub chunk_hashes: Vec<Vec<u8>>,
448    pub binding: ComponentBinding,
449}
450
451///
452/// RootComponentChildInstallEvidence
453///
454/// Exact child module and immutable retained binding frozen before installation.
455///
456
457#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
458pub struct RootComponentChildInstallEvidence {
459    pub raw_module_hash: [u8; 32],
460    pub chunk_hashes: Vec<Vec<u8>>,
461    pub binding: ComponentChildBinding,
462}
463
464///
465/// RootComponentAllocationResponse
466///
467/// Durable identity reservation returned identically for exact operation retry.
468///
469
470#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
471pub struct RootComponentAllocationResponse {
472    pub operation_id: [u8; 32],
473    pub allocation_sequence: u64,
474    pub component: ComponentInstanceId,
475    pub component_spec: ComponentSpecId,
476    pub spec_hash: [u8; 32],
477    pub role: CanisterRole,
478    pub provisioning_origin: ComponentProvisioningOrigin,
479    pub release_set: FleetSubnetRootReleaseSet,
480    pub phase: RootComponentAllocationPhase,
481    pub creation: Option<RootComponentCreationEvidence>,
482    pub installation: Option<RootComponentInstallEvidence>,
483}
484
485///
486/// RootComponentChildAllocationResponse
487///
488/// Durable direct-child lifecycle progress returned identically for exact parent retry.
489///
490
491#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
492pub struct RootComponentChildAllocationResponse {
493    pub operation_id: [u8; 32],
494    pub component: ComponentInstanceId,
495    pub parent_canister_id: Principal,
496    pub parent_role: CanisterRole,
497    pub child_role: CanisterRole,
498    pub child_kind: ComponentChildKind,
499    pub maximum_instances_per_parent: u32,
500    pub maximum_descendants: u32,
501    pub maximum_registry_bytes: u64,
502    pub reserved_against_registry: ComponentRegistryHead,
503    pub release_set: FleetSubnetRootReleaseSet,
504    pub phase: RootComponentAllocationPhase,
505    pub creation: Option<RootComponentCreationEvidence>,
506    pub installation: Option<RootComponentChildInstallEvidence>,
507}
508
509///
510/// RootComponentCommitResponse
511///
512/// Exact committed allocation, authoritative Registry row and derived Directory head.
513///
514
515#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
516pub struct RootComponentCommitResponse {
517    pub allocation: RootComponentAllocationResponse,
518    pub registry: ComponentRegistryPartitionResponse,
519    pub directory: ComponentDirectoryHead,
520}
521
522///
523/// RootComponentDirectoryPreparationResponse
524///
525/// Exact root authority plus independently observed target-local Directory preparation.
526///
527
528#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
529pub struct RootComponentDirectoryPreparationResponse {
530    pub committed: RootComponentCommitResponse,
531    pub target: ComponentRuntimeStatusResponse,
532}
533
534///
535/// RootComponentRuntimeActivationResponse
536///
537/// Exact root authority plus independently observed target-local runtime activation.
538///
539
540#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
541pub struct RootComponentRuntimeActivationResponse {
542    pub committed: RootComponentCommitResponse,
543    pub target: ComponentRuntimeStatusResponse,
544}
545
546///
547/// RootComponentMembershipActivationResponse
548///
549/// Exact active Registry authority plus independently observed current target Directory.
550///
551
552#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
553pub struct RootComponentMembershipActivationResponse {
554    pub allocation: RootComponentAllocationResponse,
555    pub registry: ComponentRegistryPartitionResponse,
556    pub directory: ComponentDirectoryHead,
557    pub target: ComponentRuntimeStatusResponse,
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563    use crate::{
564        dto::root_store::RootStoreBootstrapRequest,
565        ids::{
566            AppId, CanonicalNetworkId, FleetCoordinatorBinding, FleetId, FleetKey,
567            FleetRegistryAuthority, ReleaseBuildId, ReleaseBuildNonce, ReleaseSetDigest, SubnetId,
568        },
569    };
570
571    #[test]
572    fn component_registry_contracts_round_trip_through_candid() {
573        let request = RootComponentRegistryPreparationRequest {
574            store_bootstrap: RootStoreBootstrapRequest {
575                manifest_payload_size_bytes: 128,
576            },
577            expected_fleet_registry: FleetRegistryVersion {
578                authority: fleet_registry_authority(),
579                revision: 4,
580                content_hash: [5; 32],
581            },
582        };
583        let response = RootComponentRegistryStatusResponse {
584            fleet_subnet_root: Principal::from_slice(&[6; 29]),
585            prepared_against_registry: request.expected_fleet_registry.clone(),
586            release_set: FleetSubnetRootReleaseSet {
587                release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
588                    [7; 32],
589                )),
590                manifest_digest: ReleaseSetDigest::from_bytes([8; 32]),
591            },
592            component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
593            next_allocation_sequence: 1,
594            reserved_component_instances: 0,
595            committed_component_instances: 0,
596            managed_descendants: 0,
597            known_created_component_canisters: 0,
598            encoded_bytes: 0,
599            initial_inventory: Some(RootComponentInitialInventoryStatus {
600                fleet_activation_operation_id: [10; 32],
601                component_count: 0,
602                inventory_hash: [11; 32],
603                sealed_at_ns: 12,
604                directories_converged: true,
605                root_runtime_activated: true,
606            }),
607        };
608        let allocation = RootComponentAllocationResponse {
609            operation_id: [10; 32],
610            allocation_sequence: 1,
611            component: ComponentInstanceId::from_generated_bytes([11; 32]),
612            component_spec: "projects".parse().expect("Component Spec ID"),
613            spec_hash: [12; 32],
614            role: CanisterRole::new("project_hub"),
615            provisioning_origin: ComponentProvisioningOrigin::FleetAdministrator {
616                caller: Principal::from_slice(&[13; 29]),
617            },
618            release_set: response.release_set,
619            phase: RootComponentAllocationPhase::Reserved,
620            creation: None,
621            installation: None,
622        };
623        let created = RootComponentAllocationResponse {
624            phase: RootComponentAllocationPhase::Created,
625            creation: Some(RootComponentCreationEvidence {
626                wasm_store: Principal::from_slice(&[14; 29]),
627                payload_hash: [15; 32],
628                payload_size_bytes: 4_096,
629                initial_cycles: Cycles::new(5_000_000_000_000),
630                controller: Principal::from_slice(&[6; 29]),
631                canister: Some(Principal::from_slice(&[16; 29])),
632            }),
633            installation: None,
634            ..allocation.clone()
635        };
636        let request_bytes = candid::encode_one(&request).expect("encode request");
637        let response_bytes = candid::encode_one(&response).expect("encode response");
638        let allocation_bytes = candid::encode_one(&allocation).expect("encode allocation");
639        let created_bytes = candid::encode_one(&created).expect("encode created allocation");
640
641        assert_eq!(
642            candid::decode_one::<RootComponentRegistryPreparationRequest>(&request_bytes)
643                .expect("decode request"),
644            request
645        );
646        assert_eq!(
647            candid::decode_one::<RootComponentRegistryStatusResponse>(&response_bytes)
648                .expect("decode response"),
649            response
650        );
651        assert_eq!(
652            candid::decode_one::<RootComponentAllocationResponse>(&allocation_bytes)
653                .expect("decode allocation"),
654            allocation
655        );
656        assert_eq!(
657            candid::decode_one::<RootComponentAllocationResponse>(&created_bytes)
658                .expect("decode created allocation"),
659            created
660        );
661    }
662
663    #[test]
664    fn component_commit_response_round_trips_through_candid() {
665        let root = Principal::from_slice(&[6; 29]);
666        let component = ComponentInstanceId::from_generated_bytes([11; 32]);
667        let component_spec: ComponentSpecId = "projects".parse().expect("Component Spec ID");
668        let release_set = FleetSubnetRootReleaseSet {
669            release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
670                [7; 32],
671            )),
672            manifest_digest: ReleaseSetDigest::from_bytes([8; 32]),
673        };
674        let provisioning_origin = ComponentProvisioningOrigin::FleetAdministrator {
675            caller: Principal::from_slice(&[13; 29]),
676        };
677        let binding = ComponentBinding {
678            authority: fleet_registry_authority(),
679            component,
680            component_spec: component_spec.clone(),
681            spec_hash: [12; 32],
682            role: CanisterRole::new("project_hub"),
683            placement_subnet: SubnetId::from_principal(Principal::from_slice(&[17; 29])),
684            fleet_subnet_root: root,
685            canister_id: Principal::from_slice(&[16; 29]),
686        };
687        let head = ComponentRegistryHead {
688            component,
689            revision: 1,
690            content_hash: [18; 32],
691        };
692        let committed = RootComponentCommitResponse {
693            allocation: RootComponentAllocationResponse {
694                operation_id: [10; 32],
695                allocation_sequence: 1,
696                component,
697                component_spec,
698                spec_hash: binding.spec_hash,
699                role: binding.role.clone(),
700                provisioning_origin: provisioning_origin.clone(),
701                release_set,
702                phase: RootComponentAllocationPhase::Committed,
703                creation: Some(RootComponentCreationEvidence {
704                    wasm_store: Principal::from_slice(&[14; 29]),
705                    payload_hash: [15; 32],
706                    payload_size_bytes: 4_096,
707                    initial_cycles: Cycles::new(5_000_000_000_000),
708                    controller: root,
709                    canister: Some(binding.canister_id),
710                }),
711                installation: Some(RootComponentInstallEvidence {
712                    raw_module_hash: [20; 32],
713                    chunk_hashes: vec![vec![21; 32]],
714                    binding: binding.clone(),
715                }),
716            },
717            registry: ComponentRegistryPartitionResponse {
718                head: head.clone(),
719                binding: binding.clone(),
720                provisioning_origin,
721                release_set,
722                status: ComponentLifecycleStatus::Prepared,
723                reserved_descendants: 0,
724                committed_descendants: 0,
725                encoded_bytes: 2_048,
726            },
727            directory: ComponentDirectoryHead {
728                provenance: ComponentDirectoryProvenance {
729                    component: binding,
730                    source_fleet_subnet_root: root,
731                    component_registry_revision: head.revision,
732                    component_registry_content_hash: head.content_hash,
733                    synchronized_at_ns: 19,
734                },
735                descendant_count: 0,
736            },
737        };
738        let committed_bytes = candid::encode_one(&committed).expect("encode committed allocation");
739
740        assert_eq!(
741            candid::decode_one::<RootComponentCommitResponse>(&committed_bytes)
742                .expect("decode committed allocation"),
743            committed
744        );
745    }
746
747    fn fleet_registry_authority() -> FleetRegistryAuthority {
748        FleetRegistryAuthority {
749            binding: FleetCoordinatorBinding {
750                fleet: crate::ids::FleetBinding {
751                    fleet: FleetKey {
752                        canonical_network_id: CanonicalNetworkId::public_ic(),
753                        fleet_id: FleetId::from_generated_bytes([1; 32]),
754                    },
755                    app: AppId::from("toko"),
756                },
757                coordinator_subnet: SubnetId::from_principal(Principal::from_slice(&[2; 29])),
758                coordinator: Principal::from_slice(&[3; 29]),
759            },
760            epoch: 1,
761        }
762    }
763
764    #[test]
765    fn component_creation_request_round_trips_through_candid() {
766        let request = RootComponentCreationRequest {
767            operation_id: [10; 32],
768        };
769        let bytes = candid::encode_one(request).expect("encode creation request");
770
771        assert_eq!(
772            candid::decode_one::<RootComponentCreationRequest>(&bytes)
773                .expect("decode creation request"),
774            request
775        );
776    }
777
778    #[test]
779    #[expect(
780        clippy::too_many_lines,
781        reason = "one round-trip test keeps the complete child lifecycle boundary coherent"
782    )]
783    fn component_child_lifecycle_contracts_round_trip_through_candid() {
784        let component = ComponentInstanceId::from_generated_bytes([11; 32]);
785        let registry = ComponentRegistryHead {
786            component,
787            revision: 2,
788            content_hash: [12; 32],
789        };
790        let request = RootComponentChildAllocationRequest {
791            operation_id: [13; 32],
792            component,
793            expected_registry: registry.clone(),
794            child_role: CanisterRole::new("project_instance"),
795        };
796        let status_request = RootComponentChildAllocationStatusRequest {
797            operation_id: request.operation_id,
798            component,
799        };
800        let creation_request = RootComponentChildCreationRequest {
801            operation_id: request.operation_id,
802            component,
803        };
804        let install_request = RootComponentChildInstallRequest {
805            operation_id: request.operation_id,
806            component,
807        };
808        let root = Principal::from_slice(&[17; 29]);
809        let parent = Principal::from_slice(&[14; 29]);
810        let child = Principal::from_slice(&[18; 29]);
811        let child_binding = ComponentChildBinding {
812            component: ComponentBinding {
813                authority: fleet_registry_authority(),
814                component,
815                component_spec: "projects".parse().expect("Component Spec"),
816                spec_hash: [19; 32],
817                role: CanisterRole::new("project_hub"),
818                placement_subnet: SubnetId::from_principal(Principal::from_slice(&[20; 29])),
819                fleet_subnet_root: root,
820                canister_id: parent,
821            },
822            parent_canister_id: parent,
823            role: request.child_role.clone(),
824            canister_id: child,
825        };
826        let response = RootComponentChildAllocationResponse {
827            operation_id: request.operation_id,
828            component,
829            parent_canister_id: parent,
830            parent_role: CanisterRole::new("project_hub"),
831            child_role: request.child_role.clone(),
832            child_kind: ComponentChildKind::Instance,
833            maximum_instances_per_parent: 10_000,
834            maximum_descendants: 20_000,
835            maximum_registry_bytes: 16_777_216,
836            reserved_against_registry: registry,
837            release_set: FleetSubnetRootReleaseSet {
838                release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
839                    [15; 32],
840                )),
841                manifest_digest: ReleaseSetDigest::from_bytes([16; 32]),
842            },
843            phase: RootComponentAllocationPhase::Verified,
844            creation: Some(RootComponentCreationEvidence {
845                wasm_store: Principal::from_slice(&[21; 29]),
846                payload_hash: [22; 32],
847                payload_size_bytes: 4_096,
848                initial_cycles: Cycles::new(5_000_000_000_000),
849                controller: root,
850                canister: Some(child),
851            }),
852            installation: Some(RootComponentChildInstallEvidence {
853                raw_module_hash: [23; 32],
854                chunk_hashes: vec![vec![24; 32]],
855                binding: child_binding,
856            }),
857        };
858
859        let request_bytes = candid::encode_one(&request).expect("encode child reservation");
860        let status_bytes =
861            candid::encode_one(status_request).expect("encode child reservation status");
862        let creation_bytes =
863            candid::encode_one(creation_request).expect("encode child creation request");
864        let install_bytes =
865            candid::encode_one(install_request).expect("encode child install request");
866        let response_bytes = candid::encode_one(&response).expect("encode child response");
867
868        assert_eq!(
869            candid::decode_one::<RootComponentChildAllocationRequest>(&request_bytes)
870                .expect("decode child reservation"),
871            request
872        );
873        assert_eq!(
874            candid::decode_one::<RootComponentChildAllocationStatusRequest>(&status_bytes)
875                .expect("decode child reservation status"),
876            status_request
877        );
878        assert_eq!(
879            candid::decode_one::<RootComponentChildCreationRequest>(&creation_bytes)
880                .expect("decode child creation request"),
881            creation_request
882        );
883        assert_eq!(
884            candid::decode_one::<RootComponentChildInstallRequest>(&install_bytes)
885                .expect("decode child install request"),
886            install_request
887        );
888        assert_eq!(
889            candid::decode_one::<RootComponentChildAllocationResponse>(&response_bytes)
890                .expect("decode child response"),
891            response
892        );
893    }
894
895    #[test]
896    fn component_install_request_round_trips_through_candid() {
897        let request = RootComponentInstallRequest {
898            operation_id: [10; 32],
899        };
900        let bytes = candid::encode_one(request).expect("encode install request");
901
902        assert_eq!(
903            candid::decode_one::<RootComponentInstallRequest>(&bytes)
904                .expect("decode install request"),
905            request
906        );
907    }
908
909    #[test]
910    fn component_commit_request_round_trips_through_candid() {
911        let request = RootComponentCommitRequest {
912            operation_id: [10; 32],
913        };
914        let bytes = candid::encode_one(request).expect("encode commit request");
915
916        assert_eq!(
917            candid::decode_one::<RootComponentCommitRequest>(&bytes)
918                .expect("decode commit request"),
919            request
920        );
921    }
922
923    #[test]
924    fn component_runtime_activation_requests_round_trip_through_candid() {
925        let root_request = RootComponentRuntimeActivationRequest {
926            operation_id: [22; 32],
927        };
928        let target_request = ComponentRuntimeActivationRequest {
929            operation_id: root_request.operation_id,
930            directory_authority_hash: [23; 32],
931        };
932        let membership_request = RootComponentMembershipActivationRequest {
933            operation_id: root_request.operation_id,
934        };
935        let root_bytes = candid::encode_one(root_request).expect("encode root activation request");
936        let target_bytes =
937            candid::encode_one(target_request).expect("encode target activation request");
938        let membership_bytes =
939            candid::encode_one(membership_request).expect("encode membership activation request");
940
941        assert_eq!(
942            candid::decode_one::<RootComponentRuntimeActivationRequest>(&root_bytes)
943                .expect("decode root activation request"),
944            root_request
945        );
946        assert_eq!(
947            candid::decode_one::<ComponentRuntimeActivationRequest>(&target_bytes)
948                .expect("decode target activation request"),
949            target_request
950        );
951        assert_eq!(
952            candid::decode_one::<RootComponentMembershipActivationRequest>(&membership_bytes)
953                .expect("decode membership activation request"),
954            membership_request
955        );
956    }
957}