ic_query/nns/topology/report/subnet_topology/
model.rs1#[cfg(feature = "host")]
2use crate::cache_file::JsonCacheReport;
3use crate::subnet_catalog::SubnetKind;
4use candid::Principal;
5use serde::{Deserialize, Serialize};
6#[cfg(feature = "host")]
7use std::path::PathBuf;
8use thiserror::Error as ThisError;
9
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
17#[serde(deny_unknown_fields)]
18pub struct NnsSubnetTopologyReport {
19 pub schema_version: u32,
21 pub network: String,
23 pub registry_canister_id: String,
25 pub registry_version: u64,
27 pub fetched_at: String,
29 pub source_endpoint: String,
31 pub fetched_by: String,
33 pub subnet_count: usize,
35 pub node_count: u64,
37 pub subnets: Vec<NnsSubnetTopologyRow>,
39}
40
41#[cfg(feature = "host")]
42impl JsonCacheReport for NnsSubnetTopologyReport {
43 fn schema_version(&self) -> u32 {
44 self.schema_version
45 }
46
47 fn network(&self) -> &str {
48 &self.network
49 }
50}
51
52#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
59#[serde(deny_unknown_fields)]
60pub struct NnsSubnetTopologyRow {
61 pub subnet_principal: String,
63 pub subnet_kind: SubnetKind,
65 pub node_count: u32,
67 pub node_providers: Vec<NnsSubnetNodeProviderRow>,
69}
70
71#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
78#[serde(deny_unknown_fields)]
79pub struct NnsSubnetNodeProviderRow {
80 pub node_provider_principal: String,
82 pub node_count: u32,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct NnsSubnetTopologyFreshness {
94 pub stale: bool,
96 pub reason: String,
98 pub stale_after_seconds: u64,
100 pub fetched_at_unix_secs: Option<u64>,
102 pub age_seconds: Option<u64>,
104}
105
106#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
113pub enum NnsSubnetTopologyValidationError {
114 #[error("unsupported Subnet topology schema version {found}; expected {expected}")]
116 UnsupportedSchemaVersion {
117 found: u32,
119 expected: u32,
121 },
122
123 #[error("Subnet topology report contains no Subnets")]
125 EmptySubnets,
126
127 #[error("invalid principal in {field}: {value}: {reason}")]
129 InvalidPrincipal {
130 field: &'static str,
132 value: String,
134 reason: String,
136 },
137
138 #[error("non-canonical principal in {field}: {value}; expected {canonical}")]
140 NonCanonicalPrincipal {
141 field: &'static str,
143 value: String,
145 canonical: String,
147 },
148
149 #[error("duplicate Subnet row for {subnet_principal}")]
151 DuplicateSubnet {
152 subnet_principal: String,
154 },
155
156 #[error(
158 "Subnet rows are not canonically ordered: {previous_subnet_principal} sorts after {subnet_principal}"
159 )]
160 NonCanonicalSubnetOrder {
161 previous_subnet_principal: String,
163 subnet_principal: String,
165 },
166
167 #[error(
169 "duplicate node-provider row for {node_provider_principal} on Subnet {subnet_principal}"
170 )]
171 DuplicateNodeProvider {
172 subnet_principal: String,
174 node_provider_principal: String,
176 },
177
178 #[error(
180 "node-provider rows on Subnet {subnet_principal} are not canonically ordered: {previous_node_provider_principal} sorts after {node_provider_principal}"
181 )]
182 NonCanonicalNodeProviderOrder {
183 subnet_principal: String,
185 previous_node_provider_principal: String,
187 node_provider_principal: String,
189 },
190
191 #[error("node provider {node_provider_principal} on Subnet {subnet_principal} has zero nodes")]
193 ZeroNodeProviderCount {
194 subnet_principal: String,
196 node_provider_principal: String,
198 },
199
200 #[error(
202 "provider node counts on Subnet {subnet_principal} sum to {provider_node_count}, but the Subnet declares {subnet_node_count} nodes"
203 )]
204 SubnetNodeCountMismatch {
205 subnet_principal: String,
207 subnet_node_count: u32,
209 provider_node_count: u64,
211 },
212
213 #[error("report declares {declared} Subnets but contains {actual} rows")]
215 SubnetCountMismatch {
216 declared: usize,
218 actual: usize,
220 },
221
222 #[error("report declares {declared} nodes but Subnet rows contain {actual}")]
224 NodeCountMismatch {
225 declared: u64,
227 actual: u64,
229 },
230}
231
232impl NnsSubnetTopologyReport {
233 pub fn validate(&self) -> Result<(), NnsSubnetTopologyValidationError> {
235 if self.schema_version != super::NNS_SUBNET_TOPOLOGY_REPORT_SCHEMA_VERSION {
236 return Err(NnsSubnetTopologyValidationError::UnsupportedSchemaVersion {
237 found: self.schema_version,
238 expected: super::NNS_SUBNET_TOPOLOGY_REPORT_SCHEMA_VERSION,
239 });
240 }
241 if self.subnets.is_empty() {
242 return Err(NnsSubnetTopologyValidationError::EmptySubnets);
243 }
244 validate_principal(&self.registry_canister_id, "registry_canister_id")?;
245
246 let mut previous_subnet: Option<&str> = None;
247 let mut actual_node_count = 0_u64;
248 for subnet in &self.subnets {
249 validate_principal(&subnet.subnet_principal, "subnet_principal")?;
250 if let Some(previous) = previous_subnet {
251 if previous == subnet.subnet_principal {
252 return Err(NnsSubnetTopologyValidationError::DuplicateSubnet {
253 subnet_principal: subnet.subnet_principal.clone(),
254 });
255 }
256 if previous > subnet.subnet_principal.as_str() {
257 return Err(NnsSubnetTopologyValidationError::NonCanonicalSubnetOrder {
258 previous_subnet_principal: previous.to_string(),
259 subnet_principal: subnet.subnet_principal.clone(),
260 });
261 }
262 }
263 validate_node_providers(subnet)?;
264 actual_node_count = actual_node_count.saturating_add(u64::from(subnet.node_count));
265 previous_subnet = Some(subnet.subnet_principal.as_str());
266 }
267
268 if self.subnet_count != self.subnets.len() {
269 return Err(NnsSubnetTopologyValidationError::SubnetCountMismatch {
270 declared: self.subnet_count,
271 actual: self.subnets.len(),
272 });
273 }
274 if self.node_count != actual_node_count {
275 return Err(NnsSubnetTopologyValidationError::NodeCountMismatch {
276 declared: self.node_count,
277 actual: actual_node_count,
278 });
279 }
280 Ok(())
281 }
282}
283
284fn validate_node_providers(
285 subnet: &NnsSubnetTopologyRow,
286) -> Result<(), NnsSubnetTopologyValidationError> {
287 let mut previous_provider: Option<&str> = None;
288 let mut provider_node_count = 0_u64;
289 for provider in &subnet.node_providers {
290 validate_principal(&provider.node_provider_principal, "node_provider_principal")?;
291 if provider.node_count == 0 {
292 return Err(NnsSubnetTopologyValidationError::ZeroNodeProviderCount {
293 subnet_principal: subnet.subnet_principal.clone(),
294 node_provider_principal: provider.node_provider_principal.clone(),
295 });
296 }
297 if let Some(previous) = previous_provider {
298 if previous == provider.node_provider_principal {
299 return Err(NnsSubnetTopologyValidationError::DuplicateNodeProvider {
300 subnet_principal: subnet.subnet_principal.clone(),
301 node_provider_principal: provider.node_provider_principal.clone(),
302 });
303 }
304 if previous > provider.node_provider_principal.as_str() {
305 return Err(
306 NnsSubnetTopologyValidationError::NonCanonicalNodeProviderOrder {
307 subnet_principal: subnet.subnet_principal.clone(),
308 previous_node_provider_principal: previous.to_string(),
309 node_provider_principal: provider.node_provider_principal.clone(),
310 },
311 );
312 }
313 }
314 provider_node_count = provider_node_count.saturating_add(u64::from(provider.node_count));
315 previous_provider = Some(provider.node_provider_principal.as_str());
316 }
317 if provider_node_count != u64::from(subnet.node_count) {
318 return Err(NnsSubnetTopologyValidationError::SubnetNodeCountMismatch {
319 subnet_principal: subnet.subnet_principal.clone(),
320 subnet_node_count: subnet.node_count,
321 provider_node_count,
322 });
323 }
324 Ok(())
325}
326
327fn validate_principal(
328 value: &str,
329 field: &'static str,
330) -> Result<(), NnsSubnetTopologyValidationError> {
331 let principal = Principal::from_text(value).map_err(|err| {
332 NnsSubnetTopologyValidationError::InvalidPrincipal {
333 field,
334 value: value.to_string(),
335 reason: err.to_string(),
336 }
337 })?;
338 let canonical = principal.to_text();
339 if canonical != value {
340 return Err(NnsSubnetTopologyValidationError::NonCanonicalPrincipal {
341 field,
342 value: value.to_string(),
343 canonical,
344 });
345 }
346 Ok(())
347}
348
349#[cfg(feature = "host")]
356#[derive(Clone, Debug, Eq, PartialEq)]
357pub struct NnsSubnetTopologyCacheRequest {
358 pub cache_root: PathBuf,
360 pub network: String,
362}
363
364#[cfg(feature = "host")]
365impl NnsSubnetTopologyCacheRequest {
366 #[must_use]
368 pub fn new(cache_root: impl Into<PathBuf>, network: impl Into<String>) -> Self {
369 Self {
370 cache_root: cache_root.into(),
371 network: network.into(),
372 }
373 }
374}
375
376#[cfg(feature = "host")]
383#[derive(Clone, Debug, Eq, PartialEq)]
384pub struct NnsSubnetTopologyRefreshRequest {
385 pub cache: NnsSubnetTopologyCacheRequest,
387 pub source_endpoint: String,
389 pub now_unix_secs: u64,
391 pub lock_stale_after_seconds: u64,
393}
394
395#[cfg(feature = "host")]
396impl NnsSubnetTopologyRefreshRequest {
397 #[must_use]
399 pub fn new(
400 cache: NnsSubnetTopologyCacheRequest,
401 source_endpoint: impl Into<String>,
402 now_unix_secs: u64,
403 lock_stale_after_seconds: u64,
404 ) -> Self {
405 Self {
406 cache,
407 source_endpoint: source_endpoint.into(),
408 now_unix_secs,
409 lock_stale_after_seconds,
410 }
411 }
412}
413
414#[cfg(feature = "host")]
421#[derive(Clone, Debug, Eq, PartialEq)]
422pub struct CachedNnsSubnetTopologyReport {
423 pub path: PathBuf,
425 pub report: NnsSubnetTopologyReport,
427}