#[cfg(feature = "host")]
use crate::cache_file::JsonCacheReport;
use crate::subnet_catalog::SubnetKind;
use candid::Principal;
use serde::{Deserialize, Serialize};
#[cfg(feature = "host")]
use std::path::PathBuf;
use thiserror::Error as ThisError;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NnsSubnetTopologyReport {
pub schema_version: u32,
pub network: String,
pub registry_canister_id: String,
pub registry_version: u64,
pub fetched_at: String,
pub source_endpoint: String,
pub fetched_by: String,
pub subnet_count: usize,
pub node_count: u64,
pub subnets: Vec<NnsSubnetTopologyRow>,
}
#[cfg(feature = "host")]
impl JsonCacheReport for NnsSubnetTopologyReport {
fn schema_version(&self) -> u32 {
self.schema_version
}
fn network(&self) -> &str {
&self.network
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NnsSubnetTopologyRow {
pub subnet_principal: String,
pub subnet_kind: SubnetKind,
pub node_count: u32,
pub node_providers: Vec<NnsSubnetNodeProviderRow>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NnsSubnetNodeProviderRow {
pub node_provider_principal: String,
pub node_count: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsSubnetTopologyFreshness {
pub stale: bool,
pub reason: String,
pub stale_after_seconds: u64,
pub fetched_at_unix_secs: Option<u64>,
pub age_seconds: Option<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
pub enum NnsSubnetTopologyValidationError {
#[error("unsupported Subnet topology schema version {found}; expected {expected}")]
UnsupportedSchemaVersion {
found: u32,
expected: u32,
},
#[error("Subnet topology report contains no Subnets")]
EmptySubnets,
#[error("invalid principal in {field}: {value}: {reason}")]
InvalidPrincipal {
field: &'static str,
value: String,
reason: String,
},
#[error("non-canonical principal in {field}: {value}; expected {canonical}")]
NonCanonicalPrincipal {
field: &'static str,
value: String,
canonical: String,
},
#[error("duplicate Subnet row for {subnet_principal}")]
DuplicateSubnet {
subnet_principal: String,
},
#[error(
"Subnet rows are not canonically ordered: {previous_subnet_principal} sorts after {subnet_principal}"
)]
NonCanonicalSubnetOrder {
previous_subnet_principal: String,
subnet_principal: String,
},
#[error(
"duplicate node-provider row for {node_provider_principal} on Subnet {subnet_principal}"
)]
DuplicateNodeProvider {
subnet_principal: String,
node_provider_principal: String,
},
#[error(
"node-provider rows on Subnet {subnet_principal} are not canonically ordered: {previous_node_provider_principal} sorts after {node_provider_principal}"
)]
NonCanonicalNodeProviderOrder {
subnet_principal: String,
previous_node_provider_principal: String,
node_provider_principal: String,
},
#[error("node provider {node_provider_principal} on Subnet {subnet_principal} has zero nodes")]
ZeroNodeProviderCount {
subnet_principal: String,
node_provider_principal: String,
},
#[error(
"provider node counts on Subnet {subnet_principal} sum to {provider_node_count}, but the Subnet declares {subnet_node_count} nodes"
)]
SubnetNodeCountMismatch {
subnet_principal: String,
subnet_node_count: u32,
provider_node_count: u64,
},
#[error("report declares {declared} Subnets but contains {actual} rows")]
SubnetCountMismatch {
declared: usize,
actual: usize,
},
#[error("report declares {declared} nodes but Subnet rows contain {actual}")]
NodeCountMismatch {
declared: u64,
actual: u64,
},
}
impl NnsSubnetTopologyReport {
pub fn validate(&self) -> Result<(), NnsSubnetTopologyValidationError> {
if self.schema_version != super::NNS_SUBNET_TOPOLOGY_REPORT_SCHEMA_VERSION {
return Err(NnsSubnetTopologyValidationError::UnsupportedSchemaVersion {
found: self.schema_version,
expected: super::NNS_SUBNET_TOPOLOGY_REPORT_SCHEMA_VERSION,
});
}
if self.subnets.is_empty() {
return Err(NnsSubnetTopologyValidationError::EmptySubnets);
}
validate_principal(&self.registry_canister_id, "registry_canister_id")?;
let mut previous_subnet: Option<&str> = None;
let mut actual_node_count = 0_u64;
for subnet in &self.subnets {
validate_principal(&subnet.subnet_principal, "subnet_principal")?;
if let Some(previous) = previous_subnet {
if previous == subnet.subnet_principal {
return Err(NnsSubnetTopologyValidationError::DuplicateSubnet {
subnet_principal: subnet.subnet_principal.clone(),
});
}
if previous > subnet.subnet_principal.as_str() {
return Err(NnsSubnetTopologyValidationError::NonCanonicalSubnetOrder {
previous_subnet_principal: previous.to_string(),
subnet_principal: subnet.subnet_principal.clone(),
});
}
}
validate_node_providers(subnet)?;
actual_node_count = actual_node_count.saturating_add(u64::from(subnet.node_count));
previous_subnet = Some(subnet.subnet_principal.as_str());
}
if self.subnet_count != self.subnets.len() {
return Err(NnsSubnetTopologyValidationError::SubnetCountMismatch {
declared: self.subnet_count,
actual: self.subnets.len(),
});
}
if self.node_count != actual_node_count {
return Err(NnsSubnetTopologyValidationError::NodeCountMismatch {
declared: self.node_count,
actual: actual_node_count,
});
}
Ok(())
}
}
fn validate_node_providers(
subnet: &NnsSubnetTopologyRow,
) -> Result<(), NnsSubnetTopologyValidationError> {
let mut previous_provider: Option<&str> = None;
let mut provider_node_count = 0_u64;
for provider in &subnet.node_providers {
validate_principal(&provider.node_provider_principal, "node_provider_principal")?;
if provider.node_count == 0 {
return Err(NnsSubnetTopologyValidationError::ZeroNodeProviderCount {
subnet_principal: subnet.subnet_principal.clone(),
node_provider_principal: provider.node_provider_principal.clone(),
});
}
if let Some(previous) = previous_provider {
if previous == provider.node_provider_principal {
return Err(NnsSubnetTopologyValidationError::DuplicateNodeProvider {
subnet_principal: subnet.subnet_principal.clone(),
node_provider_principal: provider.node_provider_principal.clone(),
});
}
if previous > provider.node_provider_principal.as_str() {
return Err(
NnsSubnetTopologyValidationError::NonCanonicalNodeProviderOrder {
subnet_principal: subnet.subnet_principal.clone(),
previous_node_provider_principal: previous.to_string(),
node_provider_principal: provider.node_provider_principal.clone(),
},
);
}
}
provider_node_count = provider_node_count.saturating_add(u64::from(provider.node_count));
previous_provider = Some(provider.node_provider_principal.as_str());
}
if provider_node_count != u64::from(subnet.node_count) {
return Err(NnsSubnetTopologyValidationError::SubnetNodeCountMismatch {
subnet_principal: subnet.subnet_principal.clone(),
subnet_node_count: subnet.node_count,
provider_node_count,
});
}
Ok(())
}
fn validate_principal(
value: &str,
field: &'static str,
) -> Result<(), NnsSubnetTopologyValidationError> {
let principal = Principal::from_text(value).map_err(|err| {
NnsSubnetTopologyValidationError::InvalidPrincipal {
field,
value: value.to_string(),
reason: err.to_string(),
}
})?;
let canonical = principal.to_text();
if canonical != value {
return Err(NnsSubnetTopologyValidationError::NonCanonicalPrincipal {
field,
value: value.to_string(),
canonical,
});
}
Ok(())
}
#[cfg(feature = "host")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsSubnetTopologyCacheRequest {
pub cache_root: PathBuf,
pub network: String,
}
#[cfg(feature = "host")]
impl NnsSubnetTopologyCacheRequest {
#[must_use]
pub fn new(cache_root: impl Into<PathBuf>, network: impl Into<String>) -> Self {
Self {
cache_root: cache_root.into(),
network: network.into(),
}
}
}
#[cfg(feature = "host")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsSubnetTopologyRefreshRequest {
pub cache: NnsSubnetTopologyCacheRequest,
pub source_endpoint: String,
pub now_unix_secs: u64,
pub lock_stale_after_seconds: u64,
}
#[cfg(feature = "host")]
impl NnsSubnetTopologyRefreshRequest {
#[must_use]
pub fn new(
cache: NnsSubnetTopologyCacheRequest,
source_endpoint: impl Into<String>,
now_unix_secs: u64,
lock_stale_after_seconds: u64,
) -> Self {
Self {
cache,
source_endpoint: source_endpoint.into(),
now_unix_secs,
lock_stale_after_seconds,
}
}
}
#[cfg(feature = "host")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CachedNnsSubnetTopologyReport {
pub path: PathBuf,
pub report: NnsSubnetTopologyReport,
}