ic-query 0.35.0

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
//! Module: cloud_engine::node::host
//!
//! Responsibility: validate and build official Dashboard CloudEngine Type4 node reports.
//! Does not own: HTTP transport, native control-plane queries, rendering, or caching.
//! Boundary: validates the explicit Type4/status/provider query echo before projection.

use super::{
    CLOUD_ENGINE_NODE_INCLUDED_STATUSES, CLOUD_ENGINE_NODE_REWARD_TYPE, CloudEngineNodeInfoReport,
    CloudEngineNodeInfoRequest, CloudEngineNodeInfoSourceData, CloudEngineNodeListReport,
    CloudEngineNodeListRequest, CloudEngineNodeListSourceData, CloudEngineNodeRow,
    MAX_CLOUD_ENGINE_NODE_ROWS,
};
use crate::{
    ic::{
        IC_DASHBOARD_AUTHORITY, IC_DASHBOARD_NETWORK, IC_DASHBOARD_REPORT_SCHEMA_VERSION,
        IcDashboardReportProvenance, IcHostError, IcSourceRequest, LiveIcSource,
        canonicalize_node_status_rows_with_policy, node_status_counts,
    },
    subnet_catalog::format_utc_timestamp_secs,
};
use candid::Principal;
use std::collections::HashSet;

///
/// CloudEngineNodeSource
///
/// Official Dashboard capability for complete Type4 and exact node observations.
///

pub trait CloudEngineNodeSource {
    /// Fetch the complete explicitly scoped Type4 node resource once.
    fn fetch_cloud_engine_node_list(
        &self,
        request: &IcSourceRequest,
        node_provider_id: Option<&str>,
    ) -> Result<CloudEngineNodeListSourceData, IcHostError>;

    /// Fetch one exact node observation once.
    fn fetch_cloud_engine_node_info(
        &self,
        request: &IcSourceRequest,
        node_id: &str,
    ) -> Result<CloudEngineNodeInfoSourceData, IcHostError>;
}

/// Build one live complete CloudEngine Type4 node report from the official Dashboard.
pub fn build_cloud_engine_node_list_report(
    request: &CloudEngineNodeListRequest,
) -> Result<CloudEngineNodeListReport, IcHostError> {
    build_cloud_engine_node_list_report_with_source(request, &LiveIcSource)
}

/// Build one complete CloudEngine Type4 node report through a custom Dashboard source.
pub fn build_cloud_engine_node_list_report_with_source(
    request: &CloudEngineNodeListRequest,
    source: &dyn CloudEngineNodeSource,
) -> Result<CloudEngineNodeListReport, IcHostError> {
    validate_network(&request.network)?;
    let requested_node_provider_id = request
        .node_provider_id
        .as_deref()
        .map(|value| canonical_principal("node_provider_id", value))
        .transpose()?;
    let expected_source = source_request(&request.source_endpoint, request.now_unix_secs);
    let mut source_data = source
        .fetch_cloud_engine_node_list(&expected_source, requested_node_provider_id.as_deref())?;
    validate_source(&expected_source, &source_data.source)?;
    if source_data.requested_node_provider_id != requested_node_provider_id {
        return invalid_source(format!(
            "node-provider filter is {:?}, expected {:?}",
            source_data.requested_node_provider_id, requested_node_provider_id
        ));
    }
    if source_data.node_reward_type != CLOUD_ENGINE_NODE_REWARD_TYPE {
        return invalid_source(format!(
            "node reward-type scope is {:?}, expected {CLOUD_ENGINE_NODE_REWARD_TYPE:?}",
            source_data.node_reward_type
        ));
    }
    let expected_statuses = included_statuses();
    if source_data.included_statuses != expected_statuses {
        return invalid_source(format!(
            "included status scope is {:?}, expected {:?}",
            source_data.included_statuses, expected_statuses
        ));
    }
    canonicalize_node_status_rows_with_policy(
        &mut source_data.nodes,
        MAX_CLOUD_ENGINE_NODE_ROWS,
        false,
    )
    .map_err(invalid_source_value)?;
    validate_type4_nodes(
        &source_data.nodes,
        requested_node_provider_id.as_deref(),
        true,
    )?;

    let node_providers = source_data
        .nodes
        .iter()
        .map(|node| node.node_provider_id.as_str())
        .collect::<HashSet<_>>();
    let cloud_engine_subnets = source_data
        .nodes
        .iter()
        .filter_map(|node| node.cloud_engine_subnet_id.as_deref())
        .collect::<HashSet<_>>();
    let unassigned_cloud_engine_node_count = source_data
        .nodes
        .iter()
        .filter(|node| node.cloud_engine_subnet_id.is_none())
        .count();

    Ok(CloudEngineNodeListReport {
        provenance: report_provenance(source_data.source),
        node_reward_type: source_data.node_reward_type,
        included_statuses: source_data.included_statuses,
        requested_node_provider_id,
        node_count: source_data.nodes.len(),
        status_counts: node_status_counts(source_data.nodes.iter()),
        node_provider_count: node_providers.len(),
        cloud_engine_subnet_count: cloud_engine_subnets.len(),
        unassigned_cloud_engine_node_count,
        nodes: source_data.nodes,
    })
}

/// Build one live exact CloudEngine Type4 node report from the official Dashboard.
pub fn build_cloud_engine_node_info_report(
    request: &CloudEngineNodeInfoRequest,
) -> Result<CloudEngineNodeInfoReport, IcHostError> {
    build_cloud_engine_node_info_report_with_source(request, &LiveIcSource)
}

/// Build one exact CloudEngine Type4 node report through a custom Dashboard source.
pub fn build_cloud_engine_node_info_report_with_source(
    request: &CloudEngineNodeInfoRequest,
    source: &dyn CloudEngineNodeSource,
) -> Result<CloudEngineNodeInfoReport, IcHostError> {
    validate_network(&request.network)?;
    let requested_node_id = canonical_principal("node_id", &request.node_id)?;
    let expected_source = source_request(&request.source_endpoint, request.now_unix_secs);
    let source_data = source.fetch_cloud_engine_node_info(&expected_source, &requested_node_id)?;
    validate_source(&expected_source, &source_data.source)?;
    if source_data.node_id != requested_node_id {
        return invalid_source(format!(
            "node target is {:?}, expected {requested_node_id:?}",
            source_data.node_id
        ));
    }
    let mut nodes = vec![source_data.node];
    canonicalize_node_status_rows_with_policy(&mut nodes, 1, true).map_err(invalid_source_value)?;
    validate_type4_nodes(&nodes, None, false)?;
    let node = nodes
        .into_iter()
        .next()
        .ok_or_else(|| invalid_source_value("exact node source returned no row"))?;
    if node.node_id != requested_node_id {
        return invalid_source(format!(
            "returned node id is {:?}, expected {requested_node_id:?}",
            node.node_id
        ));
    }

    Ok(CloudEngineNodeInfoReport {
        provenance: report_provenance(source_data.source),
        node,
    })
}

fn validate_type4_nodes(
    nodes: &[CloudEngineNodeRow],
    requested_node_provider_id: Option<&str>,
    require_included_status: bool,
) -> Result<(), IcHostError> {
    for node in nodes {
        if node.node_reward_type != CLOUD_ENGINE_NODE_REWARD_TYPE {
            return invalid_source(format!(
                "node {} reward type is {:?}, expected {CLOUD_ENGINE_NODE_REWARD_TYPE:?}",
                node.node_id, node.node_reward_type
            ));
        }
        if require_included_status
            && !CLOUD_ENGINE_NODE_INCLUDED_STATUSES.contains(&node.status.as_str())
        {
            return invalid_source(format!(
                "node {} status {:?} was not in the explicitly requested scope",
                node.node_id, node.status
            ));
        }
        if let Some(expected_provider) = requested_node_provider_id
            && node.node_provider_id != expected_provider
        {
            return invalid_source(format!(
                "node {} provider is {:?}, expected requested provider {expected_provider:?}",
                node.node_id, node.node_provider_id
            ));
        }
    }
    Ok(())
}

fn included_statuses() -> Vec<String> {
    CLOUD_ENGINE_NODE_INCLUDED_STATUSES
        .into_iter()
        .map(str::to_string)
        .collect()
}

fn source_request(endpoint: &str, now_unix_secs: u64) -> IcSourceRequest {
    IcSourceRequest::new(
        endpoint,
        format_utc_timestamp_secs(now_unix_secs),
        "ic-query",
    )
}

fn validate_network(network: &str) -> Result<(), IcHostError> {
    if network == IC_DASHBOARD_NETWORK {
        return Ok(());
    }
    Err(IcHostError::InvalidRequest {
        field: "network",
        reason: format!("must be the supported mainnet identity {IC_DASHBOARD_NETWORK:?}"),
    })
}

fn validate_source(
    expected: &IcSourceRequest,
    actual: &IcSourceRequest,
) -> Result<(), IcHostError> {
    if expected == actual {
        return Ok(());
    }
    invalid_source(format!(
        "CloudEngine node source provenance is {actual:?}, expected {expected:?}"
    ))
}

fn canonical_principal(field: &'static str, value: &str) -> Result<String, IcHostError> {
    Principal::from_text(value)
        .map(|principal| principal.to_text())
        .map_err(|error| IcHostError::InvalidPrincipal {
            field,
            reason: error.to_string(),
        })
}

fn report_provenance(source: IcSourceRequest) -> IcDashboardReportProvenance {
    IcDashboardReportProvenance {
        schema_version: IC_DASHBOARD_REPORT_SCHEMA_VERSION,
        network: IC_DASHBOARD_NETWORK.to_string(),
        authority: IC_DASHBOARD_AUTHORITY.to_string(),
        source_endpoint: source.endpoint,
        fetched_at: source.fetched_at,
        fetched_by: source.fetched_by,
        certified: false,
        point_in_time_guaranteed: false,
    }
}

fn invalid_source<T>(reason: impl Into<String>) -> Result<T, IcHostError> {
    Err(invalid_source_value(reason))
}

fn invalid_source_value(reason: impl Into<String>) -> IcHostError {
    IcHostError::InvalidSourceData {
        reason: reason.into(),
    }
}