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    dto::{
10        fleet_registry::{FleetDirectorySnapshot, FleetRegistryVersion},
11        root_store::RootStoreBootstrapRequest,
12    },
13    ids::{
14        CanisterRole, ComponentBinding, ComponentInstanceId, ComponentSpecId,
15        ComponentTopologyDigest, FleetSubnetRootReleaseSet, ManagedCanisterBinding,
16    },
17};
18use candid::{CandidType, Principal};
19use serde::{Deserialize, Serialize};
20
21///
22/// RootComponentRegistryPreparationRequest
23///
24/// Exact authority required before an empty root-local Component Registry may be prepared.
25///
26
27#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
28pub struct RootComponentRegistryPreparationRequest {
29    pub store_bootstrap: RootStoreBootstrapRequest,
30    pub expected_fleet_registry: FleetRegistryVersion,
31}
32
33///
34/// RootComponentRegistryStatusResponse
35///
36/// Compact durable Component Registry authority and current allocation counters.
37///
38
39#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
40pub struct RootComponentRegistryStatusResponse {
41    pub fleet_subnet_root: Principal,
42    pub prepared_against_registry: FleetRegistryVersion,
43    pub release_set: FleetSubnetRootReleaseSet,
44    pub component_topology_digest: ComponentTopologyDigest,
45    pub next_allocation_sequence: u64,
46    pub reserved_component_instances: u32,
47    pub committed_component_instances: u32,
48    pub managed_descendants: u32,
49    pub encoded_bytes: u64,
50}
51
52///
53/// RootComponentAllocationRequest
54///
55/// Controller command naming one idempotent top-level Component reservation intent.
56///
57
58#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
59pub struct RootComponentAllocationRequest {
60    pub operation_id: [u8; 32],
61    pub component_spec: ComponentSpecId,
62}
63
64///
65/// RootComponentAllocationStatusRequest
66///
67/// Read-only lookup key for one durable top-level Component allocation operation.
68///
69
70#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
71pub struct RootComponentAllocationStatusRequest {
72    pub operation_id: [u8; 32],
73}
74
75///
76/// RootComponentCreationRequest
77///
78/// Controller command continuing one already reserved top-level Component operation.
79///
80
81#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
82pub struct RootComponentCreationRequest {
83    pub operation_id: [u8; 32],
84}
85
86///
87/// RootComponentInstallRequest
88///
89/// Controller command continuing one already created top-level Component operation.
90///
91
92#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
93pub struct RootComponentInstallRequest {
94    pub operation_id: [u8; 32],
95}
96
97///
98/// RootComponentCommitRequest
99///
100/// Controller command committing one already verified top-level Component operation.
101///
102
103#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
104pub struct RootComponentCommitRequest {
105    pub operation_id: [u8; 32],
106}
107
108///
109/// RootComponentDirectoryPreparationRequest
110///
111/// Controller command distributing exact Directories to one committed top-level Component.
112///
113
114#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
115pub struct RootComponentDirectoryPreparationRequest {
116    pub operation_id: [u8; 32],
117}
118
119///
120/// RootComponentRuntimeActivationRequest
121///
122/// Controller command activating one Directory-prepared top-level Component runtime.
123///
124
125#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
126pub struct RootComponentRuntimeActivationRequest {
127    pub operation_id: [u8; 32],
128}
129
130///
131/// ComponentProvisioningOrigin
132///
133/// Authenticated causal authority retained with one top-level Component allocation.
134///
135
136#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
137pub enum ComponentProvisioningOrigin {
138    FleetAdministrator { caller: Principal },
139}
140
141///
142/// RootComponentAllocationPhase
143///
144/// Durable root-local progress of one top-level Component allocation operation.
145///
146
147#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
148pub enum RootComponentAllocationPhase {
149    Reserved,
150    CreationIntent,
151    Created,
152    InstallIntent,
153    Installed,
154    Verified,
155    Committed,
156}
157
158///
159/// ComponentLifecycleStatus
160///
161/// Root-owned runtime lifecycle state of one committed Component Registry member.
162///
163
164#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
165pub enum ComponentLifecycleStatus {
166    Prepared,
167    Active,
168    Draining,
169    Removed,
170}
171
172///
173/// ComponentRegistryHead
174///
175/// Exact independently versioned authority of one Component Registry partition.
176///
177
178#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
179pub struct ComponentRegistryHead {
180    pub component: ComponentInstanceId,
181    pub revision: u64,
182    pub content_hash: [u8; 32],
183}
184
185///
186/// ComponentRegistryPartitionRequest
187///
188/// Read-only lookup key for one committed Component Registry partition.
189///
190
191#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
192pub struct ComponentRegistryPartitionRequest {
193    pub component: ComponentInstanceId,
194}
195
196///
197/// ComponentRegistryPartitionResponse
198///
199/// Protected top-level row and independent head of one Component Registry partition.
200///
201
202#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
203pub struct ComponentRegistryPartitionResponse {
204    pub head: ComponentRegistryHead,
205    pub binding: ComponentBinding,
206    pub provisioning_origin: ComponentProvisioningOrigin,
207    pub release_set: FleetSubnetRootReleaseSet,
208    pub status: ComponentLifecycleStatus,
209    pub encoded_bytes: u64,
210}
211
212///
213/// ComponentDirectoryProvenance
214///
215/// Exact Component Registry authority from which one Component Directory is derived.
216///
217
218#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
219pub struct ComponentDirectoryProvenance {
220    pub component: ComponentBinding,
221    pub source_fleet_subnet_root: Principal,
222    pub component_registry_revision: u64,
223    pub component_registry_content_hash: [u8; 32],
224    pub synchronized_at_ns: u64,
225}
226
227///
228/// ComponentDirectoryHead
229///
230/// Compact independently versioned discovery projection for one Component tree.
231///
232
233#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
234pub struct ComponentDirectoryHead {
235    pub provenance: ComponentDirectoryProvenance,
236    pub descendant_count: u32,
237}
238
239///
240/// ComponentDirectoryHeadRequest
241///
242/// Read-only lookup key for one committed Component Directory head.
243///
244
245#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
246pub struct ComponentDirectoryHeadRequest {
247    pub component: ComponentInstanceId,
248}
249
250///
251/// ComponentRuntimeDirectoryAuthority
252///
253/// Exact Fleet and Component discovery authority retained by one managed Component-tree node.
254///
255
256#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
257pub struct ComponentRuntimeDirectoryAuthority {
258    pub fleet: FleetDirectorySnapshot,
259    pub component: ComponentDirectoryHead,
260}
261
262///
263/// ComponentRuntimeDirectoryPreparationRequest
264///
265/// Root-issued exact Directory preparation command for one managed Component-tree node.
266///
267
268#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
269pub struct ComponentRuntimeDirectoryPreparationRequest {
270    pub operation_id: [u8; 32],
271    pub authority: ComponentRuntimeDirectoryAuthority,
272}
273
274///
275/// ComponentRuntimePhase
276///
277/// Target-local progress from installation through Component runtime activation.
278///
279
280#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
281pub enum ComponentRuntimePhase {
282    AwaitingDirectory,
283    DirectoryPrepared,
284    Active,
285}
286
287///
288/// ComponentRuntimeActivationEvidence
289///
290/// Exact retained Directory authority under which one Component runtime became Active.
291///
292
293#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
294pub struct ComponentRuntimeActivationEvidence {
295    pub directory_authority_hash: [u8; 32],
296    pub activated_at_ns: u64,
297}
298
299///
300/// ComponentRuntimeActivationRequest
301///
302/// Root-issued exact activation command for one Directory-prepared managed Component node.
303///
304
305#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
306pub struct ComponentRuntimeActivationRequest {
307    pub operation_id: [u8; 32],
308    pub directory_authority_hash: [u8; 32],
309}
310
311///
312/// ComponentRuntimeStatusResponse
313///
314/// Independently observable target-local binding and exact retained Directory authority.
315///
316
317#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
318pub struct ComponentRuntimeStatusResponse {
319    pub operation_id: [u8; 32],
320    pub binding: ManagedCanisterBinding,
321    pub phase: ComponentRuntimePhase,
322    pub authority: Option<ComponentRuntimeDirectoryAuthority>,
323    pub authority_hash: Option<[u8; 32]>,
324    pub activation: Option<ComponentRuntimeActivationEvidence>,
325}
326
327///
328/// RootComponentCreationEvidence
329///
330/// Exact Store artifact and root-owned creation settings frozen before the paid effect.
331///
332
333#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
334pub struct RootComponentCreationEvidence {
335    pub wasm_store: Principal,
336    pub payload_hash: [u8; 32],
337    pub payload_size_bytes: u64,
338    pub initial_cycles: Cycles,
339    pub controller: Principal,
340    pub canister: Option<Principal>,
341}
342
343///
344/// RootComponentInstallEvidence
345///
346/// Exact raw artifact, chunk source and immutable target binding frozen before installation.
347///
348
349#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
350pub struct RootComponentInstallEvidence {
351    pub raw_module_hash: [u8; 32],
352    pub chunk_hashes: Vec<Vec<u8>>,
353    pub binding: ComponentBinding,
354}
355
356///
357/// RootComponentAllocationResponse
358///
359/// Durable identity reservation returned identically for exact operation retry.
360///
361
362#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
363pub struct RootComponentAllocationResponse {
364    pub operation_id: [u8; 32],
365    pub allocation_sequence: u64,
366    pub component: ComponentInstanceId,
367    pub component_spec: ComponentSpecId,
368    pub spec_hash: [u8; 32],
369    pub role: CanisterRole,
370    pub provisioning_origin: ComponentProvisioningOrigin,
371    pub release_set: FleetSubnetRootReleaseSet,
372    pub phase: RootComponentAllocationPhase,
373    pub creation: Option<RootComponentCreationEvidence>,
374    pub installation: Option<RootComponentInstallEvidence>,
375}
376
377///
378/// RootComponentCommitResponse
379///
380/// Exact committed allocation, authoritative Registry row and derived Directory head.
381///
382
383#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
384pub struct RootComponentCommitResponse {
385    pub allocation: RootComponentAllocationResponse,
386    pub registry: ComponentRegistryPartitionResponse,
387    pub directory: ComponentDirectoryHead,
388}
389
390///
391/// RootComponentDirectoryPreparationResponse
392///
393/// Exact root authority plus independently observed target-local Directory preparation.
394///
395
396#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
397pub struct RootComponentDirectoryPreparationResponse {
398    pub committed: RootComponentCommitResponse,
399    pub target: ComponentRuntimeStatusResponse,
400}
401
402///
403/// RootComponentRuntimeActivationResponse
404///
405/// Exact root authority plus independently observed target-local runtime activation.
406///
407
408#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
409pub struct RootComponentRuntimeActivationResponse {
410    pub committed: RootComponentCommitResponse,
411    pub target: ComponentRuntimeStatusResponse,
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use crate::{
418        dto::root_store::RootStoreBootstrapRequest,
419        ids::{
420            AppId, CanonicalNetworkId, FleetCoordinatorBinding, FleetId, FleetKey,
421            FleetRegistryAuthority, ReleaseBuildId, ReleaseBuildNonce, ReleaseSetDigest, SubnetId,
422        },
423    };
424
425    #[test]
426    fn component_registry_contracts_round_trip_through_candid() {
427        let request = RootComponentRegistryPreparationRequest {
428            store_bootstrap: RootStoreBootstrapRequest {
429                manifest_payload_size_bytes: 128,
430            },
431            expected_fleet_registry: FleetRegistryVersion {
432                authority: fleet_registry_authority(),
433                revision: 4,
434                content_hash: [5; 32],
435            },
436        };
437        let response = RootComponentRegistryStatusResponse {
438            fleet_subnet_root: Principal::from_slice(&[6; 29]),
439            prepared_against_registry: request.expected_fleet_registry.clone(),
440            release_set: FleetSubnetRootReleaseSet {
441                release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
442                    [7; 32],
443                )),
444                manifest_digest: ReleaseSetDigest::from_bytes([8; 32]),
445            },
446            component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
447            next_allocation_sequence: 1,
448            reserved_component_instances: 0,
449            committed_component_instances: 0,
450            managed_descendants: 0,
451            encoded_bytes: 0,
452        };
453        let allocation = RootComponentAllocationResponse {
454            operation_id: [10; 32],
455            allocation_sequence: 1,
456            component: ComponentInstanceId::from_generated_bytes([11; 32]),
457            component_spec: "projects".parse().expect("Component Spec ID"),
458            spec_hash: [12; 32],
459            role: CanisterRole::new("project_hub"),
460            provisioning_origin: ComponentProvisioningOrigin::FleetAdministrator {
461                caller: Principal::from_slice(&[13; 29]),
462            },
463            release_set: response.release_set,
464            phase: RootComponentAllocationPhase::Reserved,
465            creation: None,
466            installation: None,
467        };
468        let created = RootComponentAllocationResponse {
469            phase: RootComponentAllocationPhase::Created,
470            creation: Some(RootComponentCreationEvidence {
471                wasm_store: Principal::from_slice(&[14; 29]),
472                payload_hash: [15; 32],
473                payload_size_bytes: 4_096,
474                initial_cycles: Cycles::new(5_000_000_000_000),
475                controller: Principal::from_slice(&[6; 29]),
476                canister: Some(Principal::from_slice(&[16; 29])),
477            }),
478            installation: None,
479            ..allocation.clone()
480        };
481        let request_bytes = candid::encode_one(&request).expect("encode request");
482        let response_bytes = candid::encode_one(&response).expect("encode response");
483        let allocation_bytes = candid::encode_one(&allocation).expect("encode allocation");
484        let created_bytes = candid::encode_one(&created).expect("encode created allocation");
485
486        assert_eq!(
487            candid::decode_one::<RootComponentRegistryPreparationRequest>(&request_bytes)
488                .expect("decode request"),
489            request
490        );
491        assert_eq!(
492            candid::decode_one::<RootComponentRegistryStatusResponse>(&response_bytes)
493                .expect("decode response"),
494            response
495        );
496        assert_eq!(
497            candid::decode_one::<RootComponentAllocationResponse>(&allocation_bytes)
498                .expect("decode allocation"),
499            allocation
500        );
501        assert_eq!(
502            candid::decode_one::<RootComponentAllocationResponse>(&created_bytes)
503                .expect("decode created allocation"),
504            created
505        );
506    }
507
508    #[test]
509    fn component_commit_response_round_trips_through_candid() {
510        let root = Principal::from_slice(&[6; 29]);
511        let component = ComponentInstanceId::from_generated_bytes([11; 32]);
512        let component_spec: ComponentSpecId = "projects".parse().expect("Component Spec ID");
513        let release_set = FleetSubnetRootReleaseSet {
514            release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
515                [7; 32],
516            )),
517            manifest_digest: ReleaseSetDigest::from_bytes([8; 32]),
518        };
519        let provisioning_origin = ComponentProvisioningOrigin::FleetAdministrator {
520            caller: Principal::from_slice(&[13; 29]),
521        };
522        let binding = ComponentBinding {
523            authority: fleet_registry_authority(),
524            component,
525            component_spec: component_spec.clone(),
526            spec_hash: [12; 32],
527            role: CanisterRole::new("project_hub"),
528            placement_subnet: SubnetId::from_principal(Principal::from_slice(&[17; 29])),
529            fleet_subnet_root: root,
530            canister_id: Principal::from_slice(&[16; 29]),
531        };
532        let head = ComponentRegistryHead {
533            component,
534            revision: 1,
535            content_hash: [18; 32],
536        };
537        let committed = RootComponentCommitResponse {
538            allocation: RootComponentAllocationResponse {
539                operation_id: [10; 32],
540                allocation_sequence: 1,
541                component,
542                component_spec,
543                spec_hash: binding.spec_hash,
544                role: binding.role.clone(),
545                provisioning_origin: provisioning_origin.clone(),
546                release_set,
547                phase: RootComponentAllocationPhase::Committed,
548                creation: Some(RootComponentCreationEvidence {
549                    wasm_store: Principal::from_slice(&[14; 29]),
550                    payload_hash: [15; 32],
551                    payload_size_bytes: 4_096,
552                    initial_cycles: Cycles::new(5_000_000_000_000),
553                    controller: root,
554                    canister: Some(binding.canister_id),
555                }),
556                installation: Some(RootComponentInstallEvidence {
557                    raw_module_hash: [20; 32],
558                    chunk_hashes: vec![vec![21; 32]],
559                    binding: binding.clone(),
560                }),
561            },
562            registry: ComponentRegistryPartitionResponse {
563                head: head.clone(),
564                binding: binding.clone(),
565                provisioning_origin,
566                release_set,
567                status: ComponentLifecycleStatus::Prepared,
568                encoded_bytes: 2_048,
569            },
570            directory: ComponentDirectoryHead {
571                provenance: ComponentDirectoryProvenance {
572                    component: binding,
573                    source_fleet_subnet_root: root,
574                    component_registry_revision: head.revision,
575                    component_registry_content_hash: head.content_hash,
576                    synchronized_at_ns: 19,
577                },
578                descendant_count: 0,
579            },
580        };
581        let committed_bytes = candid::encode_one(&committed).expect("encode committed allocation");
582
583        assert_eq!(
584            candid::decode_one::<RootComponentCommitResponse>(&committed_bytes)
585                .expect("decode committed allocation"),
586            committed
587        );
588    }
589
590    fn fleet_registry_authority() -> FleetRegistryAuthority {
591        FleetRegistryAuthority {
592            binding: FleetCoordinatorBinding {
593                fleet: crate::ids::FleetBinding {
594                    fleet: FleetKey {
595                        canonical_network_id: CanonicalNetworkId::public_ic(),
596                        fleet_id: FleetId::from_generated_bytes([1; 32]),
597                    },
598                    app: AppId::from("toko"),
599                },
600                coordinator_subnet: SubnetId::from_principal(Principal::from_slice(&[2; 29])),
601                coordinator: Principal::from_slice(&[3; 29]),
602            },
603            epoch: 1,
604        }
605    }
606
607    #[test]
608    fn component_creation_request_round_trips_through_candid() {
609        let request = RootComponentCreationRequest {
610            operation_id: [10; 32],
611        };
612        let bytes = candid::encode_one(request).expect("encode creation request");
613
614        assert_eq!(
615            candid::decode_one::<RootComponentCreationRequest>(&bytes)
616                .expect("decode creation request"),
617            request
618        );
619    }
620
621    #[test]
622    fn component_install_request_round_trips_through_candid() {
623        let request = RootComponentInstallRequest {
624            operation_id: [10; 32],
625        };
626        let bytes = candid::encode_one(request).expect("encode install request");
627
628        assert_eq!(
629            candid::decode_one::<RootComponentInstallRequest>(&bytes)
630                .expect("decode install request"),
631            request
632        );
633    }
634
635    #[test]
636    fn component_commit_request_round_trips_through_candid() {
637        let request = RootComponentCommitRequest {
638            operation_id: [10; 32],
639        };
640        let bytes = candid::encode_one(request).expect("encode commit request");
641
642        assert_eq!(
643            candid::decode_one::<RootComponentCommitRequest>(&bytes)
644                .expect("decode commit request"),
645            request
646        );
647    }
648
649    #[test]
650    fn component_runtime_activation_requests_round_trip_through_candid() {
651        let root_request = RootComponentRuntimeActivationRequest {
652            operation_id: [22; 32],
653        };
654        let target_request = ComponentRuntimeActivationRequest {
655            operation_id: root_request.operation_id,
656            directory_authority_hash: [23; 32],
657        };
658        let root_bytes = candid::encode_one(root_request).expect("encode root activation request");
659        let target_bytes =
660            candid::encode_one(target_request).expect("encode target activation request");
661
662        assert_eq!(
663            candid::decode_one::<RootComponentRuntimeActivationRequest>(&root_bytes)
664                .expect("decode root activation request"),
665            root_request
666        );
667        assert_eq!(
668            candid::decode_one::<ComponentRuntimeActivationRequest>(&target_bytes)
669                .expect("decode target activation request"),
670            target_request
671        );
672    }
673}