canic_host/role_contract/descriptor/
mod.rs1use canic_control_plane::state_contract::canic_control_plane_state_descriptors;
8use canic_core::{
9 role_contract::{
10 MemoryId, ResolvedRoleContract, RoleContractFinding, StateAllocationKey,
11 allocation::{allocation_definitions, validate_canonical_allocations},
12 },
13 state_contract::{
14 STATE_MANIFEST_SCHEMA_VERSION, StateAllocationDescriptor, StateManifest, StateRoleManifest,
15 canic_state_descriptors,
16 },
17};
18use std::collections::{BTreeMap, BTreeSet};
19
20#[derive(Clone, Debug)]
21pub struct StateDescriptorRegistry {
22 descriptors: BTreeMap<StateAllocationKey, StateAllocationDescriptor>,
23}
24
25impl StateDescriptorRegistry {
26 #[must_use]
27 pub fn descriptor(&self, key: StateAllocationKey) -> Option<&StateAllocationDescriptor> {
28 self.descriptors.get(&key)
29 }
30
31 pub fn descriptors(&self) -> impl Iterator<Item = &StateAllocationDescriptor> {
32 self.descriptors.values()
33 }
34}
35
36pub fn validate_state_descriptor_registry()
37-> Result<StateDescriptorRegistry, Vec<RoleContractFinding>> {
38 validate_descriptors(
39 canic_state_descriptors()
40 .into_iter()
41 .chain(canic_control_plane_state_descriptors()),
42 )
43}
44
45fn validate_descriptors(
46 descriptors: impl IntoIterator<Item = StateAllocationDescriptor>,
47) -> Result<StateDescriptorRegistry, Vec<RoleContractFinding>> {
48 let mut errors = Vec::new();
49 if let Err(error) = validate_canonical_allocations() {
50 errors.push(error);
51 }
52
53 let mut by_key = BTreeMap::new();
54 for descriptor in descriptors {
55 let key = descriptor.allocation;
56 if by_key.insert(key, descriptor).is_some() {
57 errors.push(RoleContractFinding::AllocationDescriptorDuplicate { key });
58 }
59 }
60
61 for definition in allocation_definitions() {
62 let Some(descriptor) = by_key.get(&definition.key) else {
63 errors.push(RoleContractFinding::AllocationDescriptorMissing {
64 key: definition.key,
65 });
66 continue;
67 };
68 if descriptor.owner != definition.owner {
69 errors.push(RoleContractFinding::CatalogInvalid {
70 reason: format!(
71 "allocation {:?} descriptor owner {} does not match canonical owner {}",
72 definition.key,
73 descriptor.owner.as_str(),
74 definition.owner.as_str()
75 ),
76 });
77 }
78
79 let expected = sorted_ids(definition.memory_ids.iter().copied());
80 let actual = descriptor_active_ids(descriptor);
81 if actual != expected {
82 errors.push(RoleContractFinding::AllocationDescriptorIdMismatch {
83 key: definition.key,
84 expected,
85 actual,
86 });
87 }
88 validate_descriptor_owners(descriptor, &mut errors);
89 }
90
91 for key in by_key.keys() {
92 if !allocation_definitions()
93 .iter()
94 .any(|definition| definition.key == *key)
95 {
96 errors.push(RoleContractFinding::CatalogInvalid {
97 reason: format!("descriptor references unknown allocation {key:?}"),
98 });
99 }
100 }
101
102 if errors.is_empty() {
103 Ok(StateDescriptorRegistry {
104 descriptors: by_key,
105 })
106 } else {
107 Err(errors)
108 }
109}
110
111fn descriptor_active_ids(descriptor: &StateAllocationDescriptor) -> Vec<MemoryId> {
112 sorted_ids(
113 descriptor
114 .state
115 .iter()
116 .filter_map(|domain| domain.memory_id)
117 .chain(
118 descriptor
119 .reserved_memory
120 .iter()
121 .map(|reservation| reservation.memory_id),
122 )
123 .map(MemoryId::new),
124 )
125}
126
127fn sorted_ids(ids: impl IntoIterator<Item = MemoryId>) -> Vec<MemoryId> {
128 let mut ids = ids.into_iter().collect::<Vec<_>>();
129 ids.sort_unstable();
130 ids
131}
132
133fn validate_descriptor_owners(
134 descriptor: &StateAllocationDescriptor,
135 errors: &mut Vec<RoleContractFinding>,
136) {
137 let expected = descriptor.owner.as_str();
138 let owners = descriptor
139 .state
140 .iter()
141 .map(|domain| domain.owner.as_str())
142 .chain(
143 descriptor
144 .reserved_memory
145 .iter()
146 .map(|reservation| reservation.owner.as_str()),
147 );
148 if owners.into_iter().any(|owner| owner != expected) {
149 errors.push(RoleContractFinding::CatalogInvalid {
150 reason: format!(
151 "allocation {:?} contains state metadata owned outside {expected}",
152 descriptor.allocation
153 ),
154 });
155 }
156}
157
158pub fn materialize_state_manifest(
159 contracts: &[ResolvedRoleContract],
160) -> Result<StateManifest, Vec<RoleContractFinding>> {
161 let registry = validate_state_descriptor_registry()?;
162 let mut roles = contracts
163 .iter()
164 .map(|contract| materialize_role(®istry, contract))
165 .collect::<Result<Vec<_>, _>>()?;
166 roles.sort_by(|left, right| left.canister_role.cmp(&right.canister_role));
167 Ok(StateManifest {
168 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
169 roles,
170 })
171}
172
173fn materialize_role(
174 registry: &StateDescriptorRegistry,
175 contract: &ResolvedRoleContract,
176) -> Result<StateRoleManifest, Vec<RoleContractFinding>> {
177 let mut state = Vec::new();
178 let mut reserved_memory = Vec::new();
179 let mut errors = Vec::new();
180 let mut selected = BTreeSet::new();
181
182 for allocation in &contract.allocations {
183 if !selected.insert(allocation.key) {
184 continue;
185 }
186 let Some(descriptor) = registry.descriptor(allocation.key) else {
187 errors.push(RoleContractFinding::AllocationDescriptorMissing {
188 key: allocation.key,
189 });
190 continue;
191 };
192 state.extend(descriptor.state.clone());
193 reserved_memory.extend(descriptor.reserved_memory.clone());
194 }
195
196 if !errors.is_empty() {
197 return Err(errors);
198 }
199
200 state.sort_by(|left, right| left.domain.cmp(&right.domain));
201 reserved_memory.sort_by_key(|reservation| reservation.memory_id);
202 Ok(StateRoleManifest {
203 canister_role: contract.role.as_str().to_string(),
204 state,
205 reserved_memory,
206 })
207}
208
209#[cfg(test)]
210mod tests;