From 86c05267c2fd79427985fa8de74531e5b62207af Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:45:28 +0200 Subject: [PATCH 1/3] Expand /v3/nym-nodes - includes node description and geodata - expanded scope of included geodata --- .../nym-node-status-api/src/db/queries/mod.rs | 2 +- .../src/db/queries/nym_nodes.rs | 45 ++++++++++++++- .../src/http/api/nym_nodes.rs | 3 +- .../nym-node-status-api/src/http/models.rs | 17 +++++- .../nym-node-status-api/src/http/server.rs | 11 +++- .../nym-node-status-api/src/http/state.rs | 56 +++++++++++++++---- .../nym-node-status-api/src/main.rs | 9 ++- .../src/monitor/geodata.rs | 35 ++++++++++-- .../nym-node-status-api/src/monitor/mod.rs | 14 ++--- 9 files changed, 163 insertions(+), 29 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs index f9ca28923c..f97291b6c2 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs @@ -16,7 +16,7 @@ pub(crate) use misc::insert_summaries; pub(crate) use mixnodes::{get_all_mixnodes, get_bonded_mix_ids, get_daily_stats, update_mixnodes}; pub(crate) use nym_nodes::{ get_all_nym_nodes, get_described_bonded_nym_nodes, get_described_node_bond_info, - get_node_descriptions, update_nym_nodes, + get_bonded_node_description, get_node_self_description, update_nym_nodes, }; pub(crate) use packet_stats::{ get_raw_node_stats, insert_daily_node_stats, insert_node_packet_stats, 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 6d2aa2aa36..d7d85c28f9 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 @@ -1,4 +1,5 @@ use futures_util::TryStreamExt; +use nym_node_requests::api::v1::node::models::NodeDescription; use nym_validator_client::{ client::{NodeId, NymNodeDetails}, models::NymNodeDescription, @@ -180,7 +181,7 @@ pub(crate) async fn get_described_node_bond_info( .map_err(From::from) } -pub(crate) async fn get_node_descriptions( +pub(crate) async fn get_node_self_description( pool: &DbPool, ) -> anyhow::Result> { let mut conn = pool.acquire().await?; @@ -213,3 +214,45 @@ pub(crate) async fn get_node_descriptions( }) .map_err(From::from) } + +pub(crate) async fn get_bonded_node_description( + pool: &DbPool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + + sqlx::query!( + r#"SELECT + nd.node_id, + moniker, + website, + security_contact, + details + FROM + nym_node_descriptions nd + INNER JOIN + nym_nodes + WHERE + bond_info IS NOT NULL + "#, + ) + .fetch_all(&mut *conn) + .await + .map(|records| { + records + .into_iter() + .map(|elem| { + let node_id: NodeId = elem.node_id.try_into().unwrap_or_default(); + ( + node_id, + NodeDescription { + moniker: elem.moniker.unwrap_or_default(), + website: elem.website.unwrap_or_default(), + security_contact: elem.security_contact.unwrap_or_default(), + details: elem.details.unwrap_or_default(), + }, + ) + }) + .collect::>() + }) + .map_err(From::from) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs b/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs index 0bcb3ceadc..f4a4e07527 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs @@ -32,8 +32,9 @@ async fn nym_nodes( State(state): State, ) -> HttpResult>> { let db = state.db_pool(); + let node_geocache = state.node_geocache(); - let nodes = state.cache().get_nym_nodes_list(db).await.map_err(|e| { + let nodes = state.cache().get_nym_nodes_list(db, node_geocache).await.map_err(|e| { tracing::error!("{e}"); HttpError::internal() })?; 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 3ff313e450..f39decfa73 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 @@ -60,8 +60,23 @@ pub(crate) struct ExtendedNymNode { pub(crate) node_type: String, pub(crate) ip_address: String, pub(crate) accepted_tnc: bool, - pub(crate) description: serde_json::Value, + pub(crate) self_description: serde_json::Value, pub(crate) rewarding_details: serde_json::Value, + pub(crate) description: NodeDescription, + pub(crate) geoip: Option, +} + +#[derive(Clone, Debug, utoipa::ToSchema, Deserialize, Serialize)] +pub(crate) struct NodeGeoData { + pub(crate) city: String, + pub(crate) country: String, + pub(crate) ip_address: String, + pub(crate) latitude: String, + pub(crate) longitude: String, + pub(crate) org: String, + pub(crate) postal: String, + pub(crate) region: String, + pub(crate) timezone: String, } #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] diff --git a/nym-node-status-api/nym-node-status-api/src/http/server.rs b/nym-node-status-api/nym-node-status-api/src/http/server.rs index 3c43c5efea..ce5abe2234 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/server.rs @@ -7,6 +7,7 @@ use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; use crate::{ db::DbPool, http::{api::RouterBuilder, state::AppState}, + monitor::NodeGeoCache, }; /// Return handles that allow for graceful shutdown of server + awaiting its @@ -17,10 +18,18 @@ pub(crate) async fn start_http_api( nym_http_cache_ttl: u64, agent_key_list: Vec, agent_max_count: i64, + node_geocache: NodeGeoCache, ) -> anyhow::Result { let router_builder = RouterBuilder::with_default_routes(); - let state = AppState::new(db_pool, nym_http_cache_ttl, agent_key_list, agent_max_count).await; + let state = AppState::new( + db_pool, + nym_http_cache_ttl, + agent_key_list, + agent_max_count, + node_geocache, + ) + .await; let router = router_builder.with_state(state); let bind_addr = format!("0.0.0.0:{}", http_port); 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 8073a3afd4..cfd10e936c 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 @@ -10,7 +10,8 @@ use tracing::instrument; use crate::{ db::{queries, DbPool}, - http::models::{DailyStats, ExtendedNymNode, Gateway, Mixnode, SummaryHistory}, + http::models::{DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory}, + monitor::NodeGeoCache, }; use super::models::SessionStats; @@ -21,6 +22,7 @@ pub(crate) struct AppState { cache: HttpCache, agent_key_list: Vec, agent_max_count: i64, + node_geocache: NodeGeoCache, } impl AppState { @@ -29,12 +31,14 @@ impl AppState { cache_ttl: u64, agent_key_list: Vec, agent_max_count: i64, + node_geocache: NodeGeoCache, ) -> Self { Self { db_pool, cache: HttpCache::new(cache_ttl).await, agent_key_list, agent_max_count, + node_geocache, } } @@ -53,6 +57,10 @@ impl AppState { pub(crate) fn agent_max_count(&self) -> i64 { self.agent_max_count } + + pub(crate) fn node_geocache(&self) -> NodeGeoCache { + self.node_geocache.clone() + } } static GATEWAYS_LIST_KEY: &str = "gateways"; @@ -210,7 +218,11 @@ impl HttpCache { .await } - pub async fn get_nym_nodes_list(&self, db: &DbPool) -> anyhow::Result> { + pub async fn get_nym_nodes_list( + &self, + db: &DbPool, + node_geocache: NodeGeoCache, + ) -> anyhow::Result> { match self.nym_nodes.get(NYM_NODES_LIST_KEY).await { Some(guard) => { tracing::trace!("Fetching from cache..."); @@ -220,11 +232,13 @@ impl HttpCache { None => { tracing::trace!("No nym nodes in cache, refreshing cache from DB..."); - let nym_nodes = aggregate_node_info_from_db(db).await.inspect(|nym_nodes| { - if nym_nodes.is_empty() { - tracing::warn!("Database contains 0 nym nodes"); - } - })?; + let nym_nodes = aggregate_node_info_from_db(db, node_geocache) + .await + .inspect(|nym_nodes| { + if nym_nodes.is_empty() { + tracing::warn!("Database contains 0 nym nodes"); + } + })?; self.upsert_nym_node_list(nym_nodes.clone()).await; Ok(nym_nodes) @@ -346,7 +360,10 @@ impl HttpCache { } #[instrument(level = "info", skip_all)] -async fn aggregate_node_info_from_db(pool: &DbPool) -> anyhow::Result> { +async fn aggregate_node_info_from_db( + pool: &DbPool, + node_geocache: NodeGeoCache, +) -> anyhow::Result> { let node_bond_info = queries::get_described_node_bond_info(pool).await?; tracing::debug!("Described nodes with bond info: {}", node_bond_info.len()); @@ -359,9 +376,11 @@ async fn aggregate_node_info_from_db(pool: &DbPool) -> anyhow::Result anyhow::Result anyhow::Result anyhow::Result<()> { scraper.start().await; }); + // node geocache is shared between node monitor and HTTP server + let geocache = moka::future::Cache::builder() + .time_to_live(args.geodata_ttl) + .build(); + // Start the monitor let args_clone = args.clone(); + let geocache_clone = geocache.clone(); tokio::spawn(async move { monitor::spawn_in_background( @@ -47,7 +53,7 @@ async fn main() -> anyhow::Result<()> { args_clone.nyxd_addr, args_clone.monitor_refresh_interval, args_clone.ipinfo_api_token, - args_clone.geodata_ttl, + geocache_clone, ) .await; tracing::info!("Started monitor task"); @@ -67,6 +73,7 @@ async fn main() -> anyhow::Result<()> { args.nym_http_cache_ttl, agent_key_list.to_owned(), args.max_agent_count, + geocache, ) .await .expect("Failed to start server"); diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs index 749b6b5add..bad46dd89a 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs @@ -29,6 +29,8 @@ impl IpInfoClient { } anyhow::Error::from(err) })?; + + // extracting text, then deserializing produces better error messages than response.json() let raw_response = response.text().await?; let response: LocationResponse = serde_json::from_str(&raw_response).inspect_err(|e| tracing::error!("{e}"))?; @@ -61,8 +63,9 @@ impl IpInfoClient { } } +// TODO dz: are fields other than location used? #[derive(Debug, Clone, Serialize)] -pub(crate) struct NodeGeoData { +pub(crate) struct ExplorerPrettyBond { pub(crate) identity_key: String, pub(crate) owner: Addr, pub(crate) pledge_amount: Coin, @@ -74,6 +77,12 @@ pub(crate) struct Location { pub(crate) two_letter_iso_country_code: String, #[serde(flatten)] pub(crate) location: Coordinates, + pub(crate) ip_address: String, + pub(crate) city: String, + pub(crate) region: String, + pub(crate) org: String, + pub(crate) postal: String, + pub(crate) timezone: String, } impl From for Location { @@ -81,6 +90,12 @@ impl From for Location { Self { two_letter_iso_country_code: value.two_letter_iso_country_code, location: value.loc, + ip_address: value.ip, + city: value.city, + region: value.region, + org: value.org, + postal: value.postal, + timezone: value.timezone, } } } @@ -91,6 +106,19 @@ pub(crate) struct LocationResponse { pub(crate) two_letter_iso_country_code: String, #[serde(deserialize_with = "deserialize_loc")] pub(crate) loc: Coordinates, + // TODO dz consider making them optional? + #[serde(default = "String::default")] + pub(crate) ip: String, + #[serde(default = "String::default")] + pub(crate) city: String, + #[serde(default = "String::default")] + pub(crate) region: String, + #[serde(default = "String::default")] + pub(crate) org: String, + #[serde(default = "String::default")] + pub(crate) postal: String, + #[serde(default = "String::default")] + pub(crate) timezone: String, } fn deserialize_loc<'de, D>(deserializer: D) -> Result @@ -115,10 +143,7 @@ pub(crate) struct Coordinates { impl Location { pub(crate) fn empty() -> Self { - Self { - two_letter_iso_country_code: String::new(), - location: Coordinates::default(), - } + Self::default() } } 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 d3e9398b5c..d2c96409fb 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 @@ -7,7 +7,7 @@ use crate::db::models::{ NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT, }; use crate::db::{queries, DbPool}; -use crate::monitor::geodata::{Location, NodeGeoData}; +use crate::monitor::geodata::{ExplorerPrettyBond, Location}; use crate::utils::{decimal_to_i64, LogError, NumericalCheckedCast}; use anyhow::anyhow; use moka::future::Cache; @@ -31,8 +31,8 @@ pub(crate) use geodata::IpInfoClient; mod geodata; const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); - static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; +pub(crate) type NodeGeoCache = Cache; struct Monitor { db_pool: DbPool, @@ -40,7 +40,7 @@ struct Monitor { nym_api_client_timeout: Duration, nyxd_addr: Url, ipinfo: IpInfoClient, - geocache: Cache, + geocache: NodeGeoCache, } // TODO dz: query many NYM APIs: @@ -52,9 +52,8 @@ pub(crate) async fn spawn_in_background( nyxd_addr: Url, refresh_interval: Duration, ipinfo_api_token: String, - geodata_ttl: Duration, + geocache: NodeGeoCache, ) { - let geocache = Cache::builder().time_to_live(geodata_ttl).build(); let ipinfo = IpInfoClient::new(ipinfo_api_token.clone()); let mut monitor = Monitor { db_pool, @@ -155,7 +154,7 @@ impl Monitor { for gateway in gateways.iter() { if let Some(node_details) = bonded_nym_nodes.get(&gateway.node_id) { let bond_info = &node_details.bond_information; - let gw_geodata = NodeGeoData { + let gw_geodata = ExplorerPrettyBond { identity_key: bond_info.node.identity_key.to_owned(), owner: bond_info.owner.to_owned(), pledge_amount: bond_info.original_pledge.to_owned(), @@ -181,7 +180,6 @@ impl Monitor { .map(|elem| elem.identity_key().to_owned()) .collect::>(); - // TODO this assumes polyfilling of legacy mixnodes on Nym API let mixnodes_legacy = nym_nodes .iter() .filter(|node| { @@ -360,7 +358,7 @@ impl Monitor { fn prepare_gateway_data( &self, described_gateways: &[&NymNodeDescription], - gateway_geodata: Vec, + gateway_geodata: Vec, skimmed_gateways: &[SkimmedNode], bonded_nodes: &HashMap, ) -> anyhow::Result> { From 0d4188785b854cf54b65ebf617830b5ef756b5ff Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:00:25 +0200 Subject: [PATCH 2/3] Fetch geodata for all nodes --- .../nym-node-status-api/src/db/queries/mod.rs | 4 +- .../src/http/api/nym_nodes.rs | 12 ++++-- .../nym-node-status-api/src/monitor/mod.rs | 41 +++++++++---------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs index f97291b6c2..ad57bd467b 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs @@ -15,8 +15,8 @@ pub(crate) use gateways_stats::{delete_old_records, get_sessions_stats, insert_s pub(crate) use misc::insert_summaries; pub(crate) use mixnodes::{get_all_mixnodes, get_bonded_mix_ids, get_daily_stats, update_mixnodes}; pub(crate) use nym_nodes::{ - get_all_nym_nodes, get_described_bonded_nym_nodes, get_described_node_bond_info, - get_bonded_node_description, get_node_self_description, update_nym_nodes, + get_all_nym_nodes, get_bonded_node_description, get_described_bonded_nym_nodes, + get_described_node_bond_info, get_node_self_description, update_nym_nodes, }; pub(crate) use packet_stats::{ get_raw_node_stats, insert_daily_node_stats, insert_node_packet_stats, diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs b/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs index f4a4e07527..796760de41 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs @@ -34,10 +34,14 @@ async fn nym_nodes( let db = state.db_pool(); let node_geocache = state.node_geocache(); - let nodes = state.cache().get_nym_nodes_list(db, node_geocache).await.map_err(|e| { - tracing::error!("{e}"); - HttpError::internal() - })?; + let nodes = state + .cache() + .get_nym_nodes_list(db, node_geocache) + .await + .map_err(|e| { + tracing::error!("{e}"); + HttpError::internal() + })?; Ok(Json(PagedResult::paginate(pagination, nodes))) } 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 d2c96409fb..56563be519 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 @@ -150,18 +150,9 @@ impl Monitor { tracing::debug!("{} nym nodes written to DB!", inserted); })?; - let mut gateway_geodata = Vec::new(); - for gateway in gateways.iter() { - if let Some(node_details) = bonded_nym_nodes.get(&gateway.node_id) { - let bond_info = &node_details.bond_information; - let gw_geodata = ExplorerPrettyBond { - identity_key: bond_info.node.identity_key.to_owned(), - owner: bond_info.owner.to_owned(), - pledge_amount: bond_info.original_pledge.to_owned(), - location: self.location_cached(gateway).await, - }; - gateway_geodata.push(gw_geodata); - } + // refresh geodata for all nodes + for (_, node_description) in described_nodes.iter() { + self.location_cached(node_description).await; } let mixnodes_detailed = api_client @@ -218,8 +209,9 @@ impl Monitor { let assigned_mixing_count = mixing_assigned_nodes.len(); let count_legacy_mixnodes = mixnodes_legacy.len(); - let gateway_records = - self.prepare_gateway_data(&gateways, gateway_geodata, &nym_nodes, &bonded_nym_nodes)?; + let gateway_records = self + .prepare_gateway_data(&gateways, &nym_nodes, &bonded_nym_nodes) + .await?; let pool = self.db_pool.clone(); let gateways_count = gateway_records.len(); @@ -355,10 +347,9 @@ impl Monitor { .collect::>() } - fn prepare_gateway_data( - &self, + async fn prepare_gateway_data( + &mut self, described_gateways: &[&NymNodeDescription], - gateway_geodata: Vec, skimmed_gateways: &[SkimmedNode], bonded_nodes: &HashMap, ) -> anyhow::Result> { @@ -371,11 +362,19 @@ impl Monitor { let self_described = serde_json::to_string(&gateway.description)?; - let explorer_pretty_bond = gateway_geodata - .iter() - .find(|g| g.identity_key.eq(&identity_key)); + let explorer_pretty_bond = { + let location = self.location_cached(gateway).await; + bonded_nodes + .get(&gateway.node_id) + .map(|details| ExplorerPrettyBond { + identity_key: gateway.ed25519_identity_key().to_base58_string(), + owner: details.bond_information.owner.to_owned(), + pledge_amount: details.bond_information.original_pledge.to_owned(), + location, + }) + }; let explorer_pretty_bond = - explorer_pretty_bond.and_then(|g| serde_json::to_string(g).ok()); + explorer_pretty_bond.and_then(|g| serde_json::to_string(&g).ok()); let performance = skimmed_gateways .iter() From 33c783bb7c5bde20bd9bd7a72737e2099c5df1e3 Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:22:21 +0200 Subject: [PATCH 3/3] Bump package version --- Cargo.lock | 2 +- nym-node-status-api/nym-node-status-api/Cargo.toml | 2 +- nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 569e611054..9d4ada68d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6235,7 +6235,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "2.0.0" +version = "2.1.0" dependencies = [ "ammonia", "anyhow", diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 5acd155f94..ef40d8fde8 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "2.0.0" +version = "2.1.0" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs index bad46dd89a..9a7fb3a0d2 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs @@ -63,7 +63,6 @@ impl IpInfoClient { } } -// TODO dz: are fields other than location used? #[derive(Debug, Clone, Serialize)] pub(crate) struct ExplorerPrettyBond { pub(crate) identity_key: String, @@ -106,7 +105,6 @@ pub(crate) struct LocationResponse { pub(crate) two_letter_iso_country_code: String, #[serde(deserialize_with = "deserialize_loc")] pub(crate) loc: Coordinates, - // TODO dz consider making them optional? #[serde(default = "String::default")] pub(crate) ip: String, #[serde(default = "String::default")]