diff --git a/nym-node-status-api/nym-node-status-api/src/db/models.rs b/nym-node-status-api/nym-node-status-api/src/db/models.rs index 9bb9d868d6..1d4c0f1e11 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/models.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use crate::{ http::{self, models::SummaryHistory}, utils::{decimal_to_i64, unix_timestamp_to_utc_rfc3339, NumericalCheckedCast}, @@ -14,6 +12,7 @@ use nym_validator_client::{ }; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use std::str::FromStr; use strum_macros::{EnumString, FromRepr}; use time::{Date, OffsetDateTime}; use utoipa::ToSchema; diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs index 59a7984203..5595b10fcc 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs @@ -36,6 +36,8 @@ pub(crate) async fn get_all_nym_nodes(pool: &DbPool) -> anyhow::Result, ) -> HttpResult>> { let min_node_version: String = min_node_version.unwrap_or_else(|| String::from("1.6.2")); - let min_node_version = semver::Version::parse(&min_node_version) + let _min_node_version = semver::Version::parse(&min_node_version) .map_err(|_| HttpError::invalid_input("Min version must be valid semver"))?; - let db = state.db_pool(); - let res = state.cache().get_dvpn_gateway_list(db).await; - - Ok(Json(res)) + Ok(Json( + state.cache().get_dvpn_gateway_list(state.db_pool()).await, + )) } #[allow(dead_code)] // clippy doesn't detect usage in utoipa macros #[derive(Deserialize, IntoParams)] #[into_params(parameter_in = Path)] struct TwoLetterCountryCodeParam { - #[param(minimum = 2, maximum = 2)] + #[param(min_length = 2, max_length = 2)] two_letter_country_code: String, } diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index 17e0f777c8..6e75f8f8ce 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -6,6 +6,7 @@ use nym_validator_client::{ models::{ AuthenticatorDetails, BinaryBuildInformationOwned, IpPacketRouterDetails, NymNodeData, }, + nym_api::SkimmedNode, nym_nodes::{BasicEntryInformation, NodeRole}, }; use serde::{Deserialize, Serialize}; @@ -143,19 +144,9 @@ pub mod wg_outcome_versions { } } -impl TryFrom<&Gateway> for DVpnGateway { - type Error = anyhow::Error; - - fn try_from(value: &Gateway) -> Result { - Self::try_from(value.to_owned()) - } -} - -impl TryFrom for DVpnGateway { - type Error = anyhow::Error; - - fn try_from(value: Gateway) -> Result { - let location = value +impl DVpnGateway { + pub(crate) fn new(gateway: Gateway, skimmed_node: &SkimmedNode) -> anyhow::Result { + let location = gateway .explorer_pretty_bond .ok_or_else(|| anyhow::anyhow!("Missing explorer_pretty_bond")) .and_then(|value| { @@ -163,50 +154,18 @@ impl TryFrom for DVpnGateway { }) .map(|bond| bond.location)?; - let self_described = value + let self_described = gateway .self_described .ok_or_else(|| anyhow::anyhow!("Missing self_described")) .and_then(|value| serde_json::from_value::(value).map_err(From::from))?; - let last_probe_result = value + let last_probe_result = gateway .last_probe_result .and_then(|value| serde_json::from_value::(value).ok()); - // TODO: polyfill `last_probe`, see below from VPN API - /* - const last_testrun_utc = item.last_testrun_utc; - - const last_probe: DirectoryGatewayProbe | undefined = - last_testrun_utc && item.last_probe_result?.outcome - ? { - last_updated_utc: last_testrun_utc, - outcome: item.last_probe_result.outcome, - } - : undefined; - - // - // reshape test probe - // - if ( - last_probe?.outcome?.wg && - isDirectoryGatewayProbeOutcome_WG_V2(last_probe?.outcome?.wg) && - last_probe?.outcome?.wg?.can_handshake === undefined - ) { - last_probe.outcome.wg = { - ...last_probe.outcome.wg, - can_handshake: last_probe.outcome.wg.can_handshake_v4, - can_resolve_dns: last_probe.outcome.wg.can_resolve_dns_v4, - ping_hosts_performance: - last_probe.outcome.wg.ping_hosts_performance_v4, - ping_ips_performance: last_probe.outcome.wg.ping_ips_performance_v4, - }; - } - - */ - Ok(Self { - identity_key: value.gateway_identity_key, - name: value.description.moniker, + identity_key: gateway.gateway_identity_key, + name: gateway.description.moniker, ip_packet_router: self_described.ip_packet_router, authenticator: self_described.authenticator, location: Location { @@ -215,12 +174,15 @@ impl TryFrom for DVpnGateway { two_letter_iso_country_code: location.two_letter_iso_country_code, }, last_probe: last_probe_result.map(|res| res.outcome), - // TODO - ip_addresses: vec![], // value.ip_addresses (SkimmedNode), - mix_port: 0, // value.mix_port (SkimmedNode), - role: NodeRole::EntryGateway, // value.role (SkimmedNode), - entry: None, // (SkimmedNode) - performance: value.performance, // (SkimmedNode) + ip_addresses: skimmed_node + .ip_addresses + .iter() + .map(|ip| ip.to_string()) + .collect(), + mix_port: skimmed_node.mix_port, + role: skimmed_node.role.clone(), + entry: skimmed_node.entry.clone(), + performance: gateway.performance, build_information: self_described.build_information, }) } diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index b317e8f16c..55338d8782 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -7,7 +7,7 @@ use nym_mixnet_contract_common::NodeId; use nym_validator_client::nym_api::SkimmedNode; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::RwLock; -use tracing::instrument; +use tracing::{error, instrument, warn}; use crate::{ db::{queries, DbPool}, @@ -208,47 +208,84 @@ impl HttpCache { None => { tracing::trace!("No gateways (dVPN) in cache, refreshing from DB..."); - let mut failed_to_convert_gw_count = 0; - let gateways: Vec = self - .get_gateway_list(db) + let gateways = self.get_gateway_list(db).await; + + let started_with = gateways.len(); + let Ok(skimmed_nodes) = crate::db::queries::get_described_bonded_nym_nodes(db) .await + .map(|records| { + records + .into_iter() + .filter_map(|dto| SkimmedNode::try_from(dto).ok()) + .map(|skimmed_node| { + ( + skimmed_node.ed25519_identity_pubkey.to_base58_string(), + skimmed_node, + ) + }) + .collect::>() + }) + .inspect_err(|err| { + // this would fail only in case of internal error: log and return gracefully + error!("Failed to get nym_nodes from DB: {err}"); + }) + else { + return Vec::new(); + }; + + let res_gws = gateways .into_iter() .filter(|gw| gw.bonded) - .filter_map(|d| { - d.try_into() - .inspect_err(|_| failed_to_convert_gw_count += 1) - .ok() + .filter_map(|gw| match skimmed_nodes.get(&gw.gateway_identity_key) { + Some(skimmed_node) => Some((gw, skimmed_node)), + None => { + warn!( + "No SkimmedNode data found for GW, identity_key={}", + gw.gateway_identity_key + ); + None + } }) - .filter(|g: &DVpnGateway| { + .filter_map( + |(gw, skimmed_node)| match DVpnGateway::new(gw, skimmed_node) { + Ok(gw) => Some(gw), + Err(err) => { + error!( + "Failed to convert GW node_id={} to dVPN form: {}", + skimmed_node.node_id, err + ); + None + } + }, + ) + .filter(|gw| { // gateways must have a country - g.location.two_letter_iso_country_code.len() == 2 + if gw.location.two_letter_iso_country_code.len() == 2 { + true + } else { + warn!( + "Invalid country code: {}", + gw.location.two_letter_iso_country_code + ); + false + } }) // sort by country, then by identity key .sorted_by_key(|item| { - // for some reason, closure on this owned iterator takes - // reference, but returns owned element, which means - // we have to clone despite having access to owned data ( - item.identity_key.clone(), item.location.two_letter_iso_country_code.clone(), + item.identity_key.clone(), ) }) - .collect(); + .collect::>(); - if failed_to_convert_gw_count > 0 { - tracing::error!( - "Failed to convert {} DB gateways to dVPN form", - failed_to_convert_gw_count - ); - } - - if gateways.is_empty() { - tracing::warn!("Database contains 0 gateways"); + if res_gws.is_empty() && started_with > 0 { + tracing::warn!("Started with {}, got 0 gateways", started_with); } else { - self.upsert_dvpn_gateway_list(gateways.clone()).await; + self.upsert_dvpn_gateway_list(res_gws.clone()).await; } - gateways + res_gws } } } diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index 8e6ad8f198..728c22eed0 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -151,7 +151,8 @@ impl Monitor { .await .log_error("get_all_basic_nodes")?; - tracing::info!("🟣 get_all_basic_nodes: {}", nym_nodes.len()); + let nym_node_count = nym_nodes.len(); + tracing::info!("🟣 get_all_basic_nodes: {}", nym_node_count); let nym_node_records = self.prepare_nym_node_data(nym_nodes.clone(), &bonded_nym_nodes, &described_nodes); @@ -252,7 +253,7 @@ impl Monitor { // let nodes_summary = vec![ - (NYMNODE_COUNT, nym_nodes.len()), + (NYMNODE_COUNT, nym_node_count), (ASSIGNED_MIXING_COUNT, assigned_mixing_count), (MIXNODES_LEGACY_COUNT, count_legacy_mixnodes), (NYMNODES_DESCRIBED_COUNT, described_nodes.len()), @@ -268,7 +269,7 @@ impl Monitor { let last_updated = now_utc(); let last_updated_utc = last_updated.unix_timestamp().to_string(); let network_summary = NetworkSummary { - total_nodes: nym_nodes.len().cast_checked()?, + total_nodes: nym_node_count.cast_checked()?, mixnodes: mixnode::MixnodeSummary { bonded: mixnode::MixingNodesSummary { count: assigned_mixing_count.cast_checked()?,