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/// ComponentProvisioningOrigin
121///
122/// Authenticated causal authority retained with one top-level Component allocation.
123///
124
125#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
126pub enum ComponentProvisioningOrigin {
127    FleetAdministrator { caller: Principal },
128}
129
130///
131/// RootComponentAllocationPhase
132///
133/// Durable root-local progress of one top-level Component allocation operation.
134///
135
136#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
137pub enum RootComponentAllocationPhase {
138    Reserved,
139    CreationIntent,
140    Created,
141    InstallIntent,
142    Installed,
143    Verified,
144    Committed,
145}
146
147///
148/// ComponentLifecycleStatus
149///
150/// Root-owned runtime lifecycle state of one committed Component Registry member.
151///
152
153#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
154pub enum ComponentLifecycleStatus {
155    Prepared,
156    Active,
157    Draining,
158    Removed,
159}
160
161///
162/// ComponentRegistryHead
163///
164/// Exact independently versioned authority of one Component Registry partition.
165///
166
167#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
168pub struct ComponentRegistryHead {
169    pub component: ComponentInstanceId,
170    pub revision: u64,
171    pub content_hash: [u8; 32],
172}
173
174///
175/// ComponentRegistryPartitionRequest
176///
177/// Read-only lookup key for one committed Component Registry partition.
178///
179
180#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
181pub struct ComponentRegistryPartitionRequest {
182    pub component: ComponentInstanceId,
183}
184
185///
186/// ComponentRegistryPartitionResponse
187///
188/// Protected top-level row and independent head of one Component Registry partition.
189///
190
191#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
192pub struct ComponentRegistryPartitionResponse {
193    pub head: ComponentRegistryHead,
194    pub binding: ComponentBinding,
195    pub provisioning_origin: ComponentProvisioningOrigin,
196    pub release_set: FleetSubnetRootReleaseSet,
197    pub status: ComponentLifecycleStatus,
198    pub encoded_bytes: u64,
199}
200
201///
202/// ComponentDirectoryProvenance
203///
204/// Exact Component Registry authority from which one Component Directory is derived.
205///
206
207#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
208pub struct ComponentDirectoryProvenance {
209    pub component: ComponentBinding,
210    pub source_fleet_subnet_root: Principal,
211    pub component_registry_revision: u64,
212    pub component_registry_content_hash: [u8; 32],
213    pub synchronized_at_ns: u64,
214}
215
216///
217/// ComponentDirectoryHead
218///
219/// Compact independently versioned discovery projection for one Component tree.
220///
221
222#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
223pub struct ComponentDirectoryHead {
224    pub provenance: ComponentDirectoryProvenance,
225    pub descendant_count: u32,
226}
227
228///
229/// ComponentDirectoryHeadRequest
230///
231/// Read-only lookup key for one committed Component Directory head.
232///
233
234#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
235pub struct ComponentDirectoryHeadRequest {
236    pub component: ComponentInstanceId,
237}
238
239///
240/// ComponentRuntimeDirectoryAuthority
241///
242/// Exact Fleet and Component discovery authority retained by one managed Component-tree node.
243///
244
245#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
246pub struct ComponentRuntimeDirectoryAuthority {
247    pub fleet: FleetDirectorySnapshot,
248    pub component: ComponentDirectoryHead,
249}
250
251///
252/// ComponentRuntimeDirectoryPreparationRequest
253///
254/// Root-issued exact Directory preparation command for one managed Component-tree node.
255///
256
257#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
258pub struct ComponentRuntimeDirectoryPreparationRequest {
259    pub operation_id: [u8; 32],
260    pub authority: ComponentRuntimeDirectoryAuthority,
261}
262
263///
264/// ComponentRuntimeDirectoryPhase
265///
266/// Target-local progress before Component runtime activation.
267///
268
269#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
270pub enum ComponentRuntimeDirectoryPhase {
271    AwaitingDirectory,
272    DirectoryPrepared,
273}
274
275///
276/// ComponentRuntimeDirectoryStatusResponse
277///
278/// Independently observable target-local binding and exact retained Directory authority.
279///
280
281#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
282pub struct ComponentRuntimeDirectoryStatusResponse {
283    pub operation_id: [u8; 32],
284    pub binding: ManagedCanisterBinding,
285    pub phase: ComponentRuntimeDirectoryPhase,
286    pub authority: Option<ComponentRuntimeDirectoryAuthority>,
287    pub authority_hash: Option<[u8; 32]>,
288}
289
290///
291/// RootComponentCreationEvidence
292///
293/// Exact Store artifact and root-owned creation settings frozen before the paid effect.
294///
295
296#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
297pub struct RootComponentCreationEvidence {
298    pub wasm_store: Principal,
299    pub payload_hash: [u8; 32],
300    pub payload_size_bytes: u64,
301    pub initial_cycles: Cycles,
302    pub controller: Principal,
303    pub canister: Option<Principal>,
304}
305
306///
307/// RootComponentInstallEvidence
308///
309/// Exact raw artifact, chunk source and immutable target binding frozen before installation.
310///
311
312#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
313pub struct RootComponentInstallEvidence {
314    pub raw_module_hash: [u8; 32],
315    pub chunk_hashes: Vec<Vec<u8>>,
316    pub binding: ComponentBinding,
317}
318
319///
320/// RootComponentAllocationResponse
321///
322/// Durable identity reservation returned identically for exact operation retry.
323///
324
325#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
326pub struct RootComponentAllocationResponse {
327    pub operation_id: [u8; 32],
328    pub allocation_sequence: u64,
329    pub component: ComponentInstanceId,
330    pub component_spec: ComponentSpecId,
331    pub spec_hash: [u8; 32],
332    pub role: CanisterRole,
333    pub provisioning_origin: ComponentProvisioningOrigin,
334    pub release_set: FleetSubnetRootReleaseSet,
335    pub phase: RootComponentAllocationPhase,
336    pub creation: Option<RootComponentCreationEvidence>,
337    pub installation: Option<RootComponentInstallEvidence>,
338}
339
340///
341/// RootComponentCommitResponse
342///
343/// Exact committed allocation, authoritative Registry row and derived Directory head.
344///
345
346#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
347pub struct RootComponentCommitResponse {
348    pub allocation: RootComponentAllocationResponse,
349    pub registry: ComponentRegistryPartitionResponse,
350    pub directory: ComponentDirectoryHead,
351}
352
353///
354/// RootComponentDirectoryPreparationResponse
355///
356/// Exact root authority plus independently observed target-local Directory preparation.
357///
358
359#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
360pub struct RootComponentDirectoryPreparationResponse {
361    pub committed: RootComponentCommitResponse,
362    pub target: ComponentRuntimeDirectoryStatusResponse,
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::{
369        dto::root_store::RootStoreBootstrapRequest,
370        ids::{
371            AppId, CanonicalNetworkId, FleetCoordinatorBinding, FleetId, FleetKey,
372            FleetRegistryAuthority, ReleaseBuildId, ReleaseBuildNonce, ReleaseSetDigest, SubnetId,
373        },
374    };
375
376    #[test]
377    fn component_registry_contracts_round_trip_through_candid() {
378        let request = RootComponentRegistryPreparationRequest {
379            store_bootstrap: RootStoreBootstrapRequest {
380                manifest_payload_size_bytes: 128,
381            },
382            expected_fleet_registry: FleetRegistryVersion {
383                authority: fleet_registry_authority(),
384                revision: 4,
385                content_hash: [5; 32],
386            },
387        };
388        let response = RootComponentRegistryStatusResponse {
389            fleet_subnet_root: Principal::from_slice(&[6; 29]),
390            prepared_against_registry: request.expected_fleet_registry.clone(),
391            release_set: FleetSubnetRootReleaseSet {
392                release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
393                    [7; 32],
394                )),
395                manifest_digest: ReleaseSetDigest::from_bytes([8; 32]),
396            },
397            component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
398            next_allocation_sequence: 1,
399            reserved_component_instances: 0,
400            committed_component_instances: 0,
401            managed_descendants: 0,
402            encoded_bytes: 0,
403        };
404        let allocation = RootComponentAllocationResponse {
405            operation_id: [10; 32],
406            allocation_sequence: 1,
407            component: ComponentInstanceId::from_generated_bytes([11; 32]),
408            component_spec: "projects".parse().expect("Component Spec ID"),
409            spec_hash: [12; 32],
410            role: CanisterRole::new("project_hub"),
411            provisioning_origin: ComponentProvisioningOrigin::FleetAdministrator {
412                caller: Principal::from_slice(&[13; 29]),
413            },
414            release_set: response.release_set,
415            phase: RootComponentAllocationPhase::Reserved,
416            creation: None,
417            installation: None,
418        };
419        let created = RootComponentAllocationResponse {
420            phase: RootComponentAllocationPhase::Created,
421            creation: Some(RootComponentCreationEvidence {
422                wasm_store: Principal::from_slice(&[14; 29]),
423                payload_hash: [15; 32],
424                payload_size_bytes: 4_096,
425                initial_cycles: Cycles::new(5_000_000_000_000),
426                controller: Principal::from_slice(&[6; 29]),
427                canister: Some(Principal::from_slice(&[16; 29])),
428            }),
429            installation: None,
430            ..allocation.clone()
431        };
432        let request_bytes = candid::encode_one(&request).expect("encode request");
433        let response_bytes = candid::encode_one(&response).expect("encode response");
434        let allocation_bytes = candid::encode_one(&allocation).expect("encode allocation");
435        let created_bytes = candid::encode_one(&created).expect("encode created allocation");
436
437        assert_eq!(
438            candid::decode_one::<RootComponentRegistryPreparationRequest>(&request_bytes)
439                .expect("decode request"),
440            request
441        );
442        assert_eq!(
443            candid::decode_one::<RootComponentRegistryStatusResponse>(&response_bytes)
444                .expect("decode response"),
445            response
446        );
447        assert_eq!(
448            candid::decode_one::<RootComponentAllocationResponse>(&allocation_bytes)
449                .expect("decode allocation"),
450            allocation
451        );
452        assert_eq!(
453            candid::decode_one::<RootComponentAllocationResponse>(&created_bytes)
454                .expect("decode created allocation"),
455            created
456        );
457    }
458
459    #[test]
460    fn component_commit_response_round_trips_through_candid() {
461        let root = Principal::from_slice(&[6; 29]);
462        let component = ComponentInstanceId::from_generated_bytes([11; 32]);
463        let component_spec: ComponentSpecId = "projects".parse().expect("Component Spec ID");
464        let release_set = FleetSubnetRootReleaseSet {
465            release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
466                [7; 32],
467            )),
468            manifest_digest: ReleaseSetDigest::from_bytes([8; 32]),
469        };
470        let provisioning_origin = ComponentProvisioningOrigin::FleetAdministrator {
471            caller: Principal::from_slice(&[13; 29]),
472        };
473        let binding = ComponentBinding {
474            authority: fleet_registry_authority(),
475            component,
476            component_spec: component_spec.clone(),
477            spec_hash: [12; 32],
478            role: CanisterRole::new("project_hub"),
479            placement_subnet: SubnetId::from_principal(Principal::from_slice(&[17; 29])),
480            fleet_subnet_root: root,
481            canister_id: Principal::from_slice(&[16; 29]),
482        };
483        let head = ComponentRegistryHead {
484            component,
485            revision: 1,
486            content_hash: [18; 32],
487        };
488        let committed = RootComponentCommitResponse {
489            allocation: RootComponentAllocationResponse {
490                operation_id: [10; 32],
491                allocation_sequence: 1,
492                component,
493                component_spec,
494                spec_hash: binding.spec_hash,
495                role: binding.role.clone(),
496                provisioning_origin: provisioning_origin.clone(),
497                release_set,
498                phase: RootComponentAllocationPhase::Committed,
499                creation: Some(RootComponentCreationEvidence {
500                    wasm_store: Principal::from_slice(&[14; 29]),
501                    payload_hash: [15; 32],
502                    payload_size_bytes: 4_096,
503                    initial_cycles: Cycles::new(5_000_000_000_000),
504                    controller: root,
505                    canister: Some(binding.canister_id),
506                }),
507                installation: Some(RootComponentInstallEvidence {
508                    raw_module_hash: [20; 32],
509                    chunk_hashes: vec![vec![21; 32]],
510                    binding: binding.clone(),
511                }),
512            },
513            registry: ComponentRegistryPartitionResponse {
514                head: head.clone(),
515                binding: binding.clone(),
516                provisioning_origin,
517                release_set,
518                status: ComponentLifecycleStatus::Prepared,
519                encoded_bytes: 2_048,
520            },
521            directory: ComponentDirectoryHead {
522                provenance: ComponentDirectoryProvenance {
523                    component: binding,
524                    source_fleet_subnet_root: root,
525                    component_registry_revision: head.revision,
526                    component_registry_content_hash: head.content_hash,
527                    synchronized_at_ns: 19,
528                },
529                descendant_count: 0,
530            },
531        };
532        let committed_bytes = candid::encode_one(&committed).expect("encode committed allocation");
533
534        assert_eq!(
535            candid::decode_one::<RootComponentCommitResponse>(&committed_bytes)
536                .expect("decode committed allocation"),
537            committed
538        );
539    }
540
541    fn fleet_registry_authority() -> FleetRegistryAuthority {
542        FleetRegistryAuthority {
543            binding: FleetCoordinatorBinding {
544                fleet: crate::ids::FleetBinding {
545                    fleet: FleetKey {
546                        canonical_network_id: CanonicalNetworkId::public_ic(),
547                        fleet_id: FleetId::from_generated_bytes([1; 32]),
548                    },
549                    app: AppId::from("toko"),
550                },
551                coordinator_subnet: SubnetId::from_principal(Principal::from_slice(&[2; 29])),
552                coordinator: Principal::from_slice(&[3; 29]),
553            },
554            epoch: 1,
555        }
556    }
557
558    #[test]
559    fn component_creation_request_round_trips_through_candid() {
560        let request = RootComponentCreationRequest {
561            operation_id: [10; 32],
562        };
563        let bytes = candid::encode_one(request).expect("encode creation request");
564
565        assert_eq!(
566            candid::decode_one::<RootComponentCreationRequest>(&bytes)
567                .expect("decode creation request"),
568            request
569        );
570    }
571
572    #[test]
573    fn component_install_request_round_trips_through_candid() {
574        let request = RootComponentInstallRequest {
575            operation_id: [10; 32],
576        };
577        let bytes = candid::encode_one(request).expect("encode install request");
578
579        assert_eq!(
580            candid::decode_one::<RootComponentInstallRequest>(&bytes)
581                .expect("decode install request"),
582            request
583        );
584    }
585
586    #[test]
587    fn component_commit_request_round_trips_through_candid() {
588        let request = RootComponentCommitRequest {
589            operation_id: [10; 32],
590        };
591        let bytes = candid::encode_one(request).expect("encode commit request");
592
593        assert_eq!(
594            candid::decode_one::<RootComponentCommitRequest>(&bytes)
595                .expect("decode commit request"),
596            request
597        );
598    }
599}