Skip to main content

canic_host/role_contract/descriptor/
mod.rs

1//! Module: role_contract::descriptor
2//!
3//! Responsibility: validate owner descriptors and join them to resolved allocations.
4//! Does not own: role selection, state records, audit rendering, or stable-memory access.
5//! Boundary: every materialized manifest passes through one complete registry validation.
6
7use 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(&registry, 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;