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::{fleet_registry::FleetRegistryVersion, root_store::RootStoreBootstrapRequest},
10    ids::{
11        CanisterRole, ComponentInstanceId, ComponentSpecId, ComponentTopologyDigest,
12        FleetSubnetRootReleaseSet,
13    },
14};
15use candid::{CandidType, Principal};
16use serde::{Deserialize, Serialize};
17
18///
19/// RootComponentRegistryPreparationRequest
20///
21/// Exact authority required before an empty root-local Component Registry may be prepared.
22///
23
24#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25pub struct RootComponentRegistryPreparationRequest {
26    pub store_bootstrap: RootStoreBootstrapRequest,
27    pub expected_fleet_registry: FleetRegistryVersion,
28}
29
30///
31/// RootComponentRegistryStatusResponse
32///
33/// Compact durable Component Registry authority and current allocation counters.
34///
35
36#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
37pub struct RootComponentRegistryStatusResponse {
38    pub fleet_subnet_root: Principal,
39    pub prepared_against_registry: FleetRegistryVersion,
40    pub release_set: FleetSubnetRootReleaseSet,
41    pub component_topology_digest: ComponentTopologyDigest,
42    pub next_allocation_sequence: u64,
43    pub reserved_component_instances: u32,
44    pub committed_component_instances: u32,
45    pub managed_descendants: u32,
46    pub encoded_bytes: u64,
47}
48
49///
50/// RootComponentAllocationRequest
51///
52/// Controller command naming one idempotent top-level Component reservation intent.
53///
54
55#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
56pub struct RootComponentAllocationRequest {
57    pub operation_id: [u8; 32],
58    pub component_spec: ComponentSpecId,
59}
60
61///
62/// RootComponentAllocationStatusRequest
63///
64/// Read-only lookup key for one durable top-level Component allocation operation.
65///
66
67#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
68pub struct RootComponentAllocationStatusRequest {
69    pub operation_id: [u8; 32],
70}
71
72///
73/// RootComponentCreationRequest
74///
75/// Controller command continuing one already reserved top-level Component operation.
76///
77
78#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
79pub struct RootComponentCreationRequest {
80    pub operation_id: [u8; 32],
81}
82
83///
84/// ComponentProvisioningOrigin
85///
86/// Authenticated causal authority retained with one top-level Component allocation.
87///
88
89#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
90pub enum ComponentProvisioningOrigin {
91    FleetAdministrator { caller: Principal },
92}
93
94///
95/// RootComponentAllocationPhase
96///
97/// Durable root-local progress of one top-level Component allocation operation.
98///
99
100#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
101pub enum RootComponentAllocationPhase {
102    Reserved,
103    CreationIntent,
104    Created,
105}
106
107///
108/// RootComponentCreationEvidence
109///
110/// Exact Store artifact and root-owned creation settings frozen before the paid effect.
111///
112
113#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
114pub struct RootComponentCreationEvidence {
115    pub wasm_store: Principal,
116    pub payload_hash: [u8; 32],
117    pub payload_size_bytes: u64,
118    pub initial_cycles: Cycles,
119    pub controller: Principal,
120    pub canister: Option<Principal>,
121}
122
123///
124/// RootComponentAllocationResponse
125///
126/// Durable identity reservation returned identically for exact operation retry.
127///
128
129#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
130pub struct RootComponentAllocationResponse {
131    pub operation_id: [u8; 32],
132    pub allocation_sequence: u64,
133    pub component: ComponentInstanceId,
134    pub component_spec: ComponentSpecId,
135    pub spec_hash: [u8; 32],
136    pub role: CanisterRole,
137    pub provisioning_origin: ComponentProvisioningOrigin,
138    pub release_set: FleetSubnetRootReleaseSet,
139    pub phase: RootComponentAllocationPhase,
140    pub creation: Option<RootComponentCreationEvidence>,
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::{
147        dto::root_store::RootStoreBootstrapRequest,
148        ids::{
149            AppId, CanonicalNetworkId, FleetCoordinatorBinding, FleetId, FleetKey,
150            FleetRegistryAuthority, ReleaseBuildId, ReleaseBuildNonce, ReleaseSetDigest, SubnetId,
151        },
152    };
153
154    #[test]
155    fn component_registry_contracts_round_trip_through_candid() {
156        let authority = FleetRegistryAuthority {
157            binding: FleetCoordinatorBinding {
158                fleet: crate::ids::FleetBinding {
159                    fleet: FleetKey {
160                        canonical_network_id: CanonicalNetworkId::public_ic(),
161                        fleet_id: FleetId::from_generated_bytes([1; 32]),
162                    },
163                    app: AppId::from("toko"),
164                },
165                coordinator_subnet: SubnetId::from_principal(Principal::from_slice(&[2; 29])),
166                coordinator: Principal::from_slice(&[3; 29]),
167            },
168            epoch: 1,
169        };
170        let request = RootComponentRegistryPreparationRequest {
171            store_bootstrap: RootStoreBootstrapRequest {
172                manifest_payload_size_bytes: 128,
173            },
174            expected_fleet_registry: FleetRegistryVersion {
175                authority,
176                revision: 4,
177                content_hash: [5; 32],
178            },
179        };
180        let response = RootComponentRegistryStatusResponse {
181            fleet_subnet_root: Principal::from_slice(&[6; 29]),
182            prepared_against_registry: request.expected_fleet_registry.clone(),
183            release_set: FleetSubnetRootReleaseSet {
184                release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
185                    [7; 32],
186                )),
187                manifest_digest: ReleaseSetDigest::from_bytes([8; 32]),
188            },
189            component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
190            next_allocation_sequence: 1,
191            reserved_component_instances: 0,
192            committed_component_instances: 0,
193            managed_descendants: 0,
194            encoded_bytes: 0,
195        };
196        let allocation = RootComponentAllocationResponse {
197            operation_id: [10; 32],
198            allocation_sequence: 1,
199            component: ComponentInstanceId::from_generated_bytes([11; 32]),
200            component_spec: "projects".parse().expect("Component Spec ID"),
201            spec_hash: [12; 32],
202            role: CanisterRole::new("project_hub"),
203            provisioning_origin: ComponentProvisioningOrigin::FleetAdministrator {
204                caller: Principal::from_slice(&[13; 29]),
205            },
206            release_set: response.release_set,
207            phase: RootComponentAllocationPhase::Reserved,
208            creation: None,
209        };
210        let creation_request = RootComponentCreationRequest {
211            operation_id: allocation.operation_id,
212        };
213        let created = RootComponentAllocationResponse {
214            phase: RootComponentAllocationPhase::Created,
215            creation: Some(RootComponentCreationEvidence {
216                wasm_store: Principal::from_slice(&[14; 29]),
217                payload_hash: [15; 32],
218                payload_size_bytes: 4_096,
219                initial_cycles: Cycles::new(5_000_000_000_000),
220                controller: Principal::from_slice(&[6; 29]),
221                canister: Some(Principal::from_slice(&[16; 29])),
222            }),
223            ..allocation.clone()
224        };
225
226        let request_bytes = candid::encode_one(&request).expect("encode request");
227        let response_bytes = candid::encode_one(&response).expect("encode response");
228        let allocation_bytes = candid::encode_one(&allocation).expect("encode allocation");
229        let creation_request_bytes =
230            candid::encode_one(creation_request).expect("encode creation request");
231        let created_bytes = candid::encode_one(&created).expect("encode created allocation");
232
233        assert_eq!(
234            candid::decode_one::<RootComponentRegistryPreparationRequest>(&request_bytes)
235                .expect("decode request"),
236            request
237        );
238        assert_eq!(
239            candid::decode_one::<RootComponentRegistryStatusResponse>(&response_bytes)
240                .expect("decode response"),
241            response
242        );
243        assert_eq!(
244            candid::decode_one::<RootComponentAllocationResponse>(&allocation_bytes)
245                .expect("decode allocation"),
246            allocation
247        );
248        assert_eq!(
249            candid::decode_one::<RootComponentCreationRequest>(&creation_request_bytes)
250                .expect("decode creation request"),
251            creation_request
252        );
253        assert_eq!(
254            candid::decode_one::<RootComponentAllocationResponse>(&created_bytes)
255                .expect("decode created allocation"),
256            created
257        );
258    }
259}