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