canic_core/ops/storage/registry/
subnet.rs

1use crate::{
2    Error, ThisError,
3    cdk::types::Principal,
4    cdk::utils::time::now_secs,
5    config::schema::CanisterCardinality,
6    dto::{
7        registry::SubnetRegistryView,
8        snapshot::{TopologyChildView, TopologyNodeView},
9    },
10    ids::CanisterRole,
11    model::memory::{CanisterEntry, CanisterSummary, registry::SubnetRegistry},
12    ops::{
13        adapter::{
14            canister::{canister_summary_to_topology_child, canister_summary_to_topology_node},
15            registry::subnet_registry_to_view,
16        },
17        config::ConfigOps,
18        storage::registry::RegistryOpsError,
19    },
20};
21use std::collections::HashSet;
22
23///
24/// SubnetRegistryOpsError
25///
26
27#[derive(Debug, ThisError)]
28pub enum SubnetRegistryOpsError {
29    // ---------------------------------------------------------------------
30    // Registration errors
31    // ---------------------------------------------------------------------
32    #[error("canister {0} already registered")]
33    AlreadyRegistered(Principal),
34
35    #[error("role {role} already registered to {pid}")]
36    RoleAlreadyRegistered { role: CanisterRole, pid: Principal },
37
38    // ---------------------------------------------------------------------
39    // Traversal / invariant errors
40    // ---------------------------------------------------------------------
41    #[error("canister {0} not found in subnet registry")]
42    CanisterNotFound(Principal),
43
44    #[error("parent chain contains a cycle at {0}")]
45    ParentChainCycle(Principal),
46
47    #[error("parent chain exceeded registry size ({0})")]
48    ParentChainTooLong(usize),
49
50    #[error("parent chain did not terminate at root (last pid: {0})")]
51    ParentChainNotRootTerminated(Principal),
52}
53
54impl From<SubnetRegistryOpsError> for Error {
55    fn from(err: SubnetRegistryOpsError) -> Self {
56        RegistryOpsError::from(err).into()
57    }
58}
59
60///
61/// SubnetRegistryOps
62///
63/// Semantic operations over the subnet registry.
64/// Enforces cardinality, parent-chain invariants, and traversal safety.
65///
66
67pub struct SubnetRegistryOps;
68
69impl SubnetRegistryOps {
70    // ---------------------------------------------------------------------
71    // Mutation
72    // ---------------------------------------------------------------------
73
74    pub(crate) fn register(
75        pid: Principal,
76        role: &CanisterRole,
77        parent_pid: Principal,
78        module_hash: Vec<u8>,
79    ) -> Result<(), Error> {
80        if SubnetRegistry::get(pid).is_some() {
81            return Err(SubnetRegistryOpsError::AlreadyRegistered(pid).into());
82        }
83
84        if role_requires_singleton(role)?
85            && let Some((existing_pid, _)) = SubnetRegistry::find_first_by_role(role)
86        {
87            return Err(SubnetRegistryOpsError::RoleAlreadyRegistered {
88                role: role.clone(),
89                pid: existing_pid,
90            }
91            .into());
92        }
93
94        let created_at = now_secs();
95        SubnetRegistry::register(pid, role, parent_pid, module_hash, created_at);
96        Ok(())
97    }
98
99    pub(crate) fn remove(pid: &Principal) -> Option<CanisterEntry> {
100        SubnetRegistry::remove(pid)
101    }
102
103    pub(crate) fn register_root(pid: Principal) {
104        let created_at = now_secs();
105        SubnetRegistry::register_root(pid, created_at);
106    }
107
108    pub(crate) fn update_module_hash(pid: Principal, module_hash: Vec<u8>) -> bool {
109        SubnetRegistry::update_module_hash(pid, module_hash)
110    }
111
112    // ---------------------------------------------------------------------
113    // Queries (canonical data)
114    // ---------------------------------------------------------------------
115
116    #[must_use]
117    pub(crate) fn get(pid: Principal) -> Option<CanisterEntry> {
118        SubnetRegistry::get(pid)
119    }
120
121    #[must_use]
122    pub(crate) fn is_registered(pid: Principal) -> bool {
123        SubnetRegistry::get(pid).is_some()
124    }
125
126    #[must_use]
127    pub fn get_parent(pid: Principal) -> Option<Principal> {
128        SubnetRegistry::get_parent(pid)
129    }
130
131    /// Direct children (one level).
132    #[must_use]
133    pub(crate) fn children(pid: Principal) -> Vec<(Principal, CanisterSummary)> {
134        SubnetRegistry::children(pid)
135    }
136
137    /// Direct children (one level) as topology views.
138    #[must_use]
139    pub(crate) fn children_view(pid: Principal) -> Vec<TopologyNodeView> {
140        SubnetRegistry::children(pid)
141            .into_iter()
142            .map(|(pid, summary)| canister_summary_to_topology_node(pid, &summary))
143            .collect()
144    }
145
146    /// Direct children (one level) as topology child views.
147    #[must_use]
148    pub(crate) fn children_child_view(pid: Principal) -> Vec<TopologyChildView> {
149        SubnetRegistry::children(pid)
150            .into_iter()
151            .map(|(pid, summary)| canister_summary_to_topology_child(pid, &summary))
152            .collect()
153    }
154
155    /// Full subtree rooted at `pid`.
156    #[must_use]
157    pub(crate) fn subtree(pid: Principal) -> Vec<(Principal, CanisterSummary)> {
158        SubnetRegistry::subtree(pid)
159    }
160
161    /// Canonical registry export (identity + role only).
162    #[must_use]
163    pub(crate) fn export_roles() -> Vec<(Principal, CanisterRole)> {
164        SubnetRegistry::export()
165            .into_iter()
166            .map(|(pid, entry)| (pid, entry.role))
167            .collect()
168    }
169
170    #[must_use]
171    pub fn export_view() -> SubnetRegistryView {
172        let data = SubnetRegistry::export();
173
174        subnet_registry_to_view(data)
175    }
176
177    // ---------------------------------------------------------------------
178    // Traversal / invariants
179    // ---------------------------------------------------------------------
180
181    /// Return the canonical parent chain for a canister.
182    ///
183    /// Returned order: root → … → target
184    ///
185    /// Invariants enforced:
186    /// - no cycles
187    /// - bounded by registry size
188    /// - terminates at ROOT
189    pub(crate) fn parent_chain(
190        target: Principal,
191    ) -> Result<Vec<(Principal, CanisterSummary)>, Error> {
192        let registry_len = SubnetRegistry::export().len();
193
194        let mut chain: Vec<(Principal, CanisterSummary)> = Vec::new();
195        let mut seen: HashSet<Principal> = HashSet::new();
196        let mut pid = target;
197
198        loop {
199            if !seen.insert(pid) {
200                return Err(SubnetRegistryOpsError::ParentChainCycle(pid).into());
201            }
202
203            let Some(entry) = SubnetRegistry::get(pid) else {
204                return Err(SubnetRegistryOpsError::CanisterNotFound(pid).into());
205            };
206
207            if seen.len() > registry_len {
208                return Err(SubnetRegistryOpsError::ParentChainTooLong(seen.len()).into());
209            }
210
211            let summary = CanisterSummary::from(&entry);
212            let parent = entry.parent_pid;
213
214            chain.push((pid, summary));
215
216            if let Some(parent_pid) = parent {
217                pid = parent_pid;
218            } else {
219                if entry.role != CanisterRole::ROOT {
220                    return Err(SubnetRegistryOpsError::ParentChainNotRootTerminated(pid).into());
221                }
222                break;
223            }
224        }
225
226        chain.reverse();
227        Ok(chain)
228    }
229
230    /// Parent chain as topology views (root → … → target).
231    pub(crate) fn parent_chain_view(target: Principal) -> Result<Vec<TopologyNodeView>, Error> {
232        let chain = Self::parent_chain(target)?;
233        Ok(chain
234            .into_iter()
235            .map(|(pid, summary)| canister_summary_to_topology_node(pid, &summary))
236            .collect())
237    }
238}
239
240// -------------------------------------------------------------------------
241// Helpers
242// -------------------------------------------------------------------------
243
244fn role_requires_singleton(role: &CanisterRole) -> Result<bool, Error> {
245    let cfg = ConfigOps::current_subnet_canister(role)?;
246    Ok(cfg.cardinality == CanisterCardinality::Single)
247}