added hacky routes to return nymnodes alongside legacy nodes (#5051)

* added hacky routes to return nymnodes alongside legacy nodes

* fixed mixing role

* Update client (#5054)

* removed hacky mixnodes endpoint for its not used

* construct explorer-api client with timeout

---------

Co-authored-by: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com>
This commit is contained in:
Jędrzej Stuczyński
2024-10-29 08:35:07 +00:00
committed by GitHub
parent 4396def133
commit 317f7fffa9
12 changed files with 446 additions and 7 deletions
@@ -30,10 +30,10 @@ use time::Date;
use url::Url;
pub use crate::nym_api::NymApiClientExt;
use nym_mixnet_contract_common::NymNodeDetails;
pub use nym_mixnet_contract_common::{
mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId,
};
// re-export the type to not break existing imports
pub use crate::coconut::EcashApiClient;
@@ -106,7 +106,9 @@ impl Config {
pub struct Client<C, S = NoSigner> {
// ideally they would have been read-only, but unfortunately rust doesn't have such features
// #[deprecated(note = "please use `nym_api_client` instead")]
pub nym_api: nym_api::Client,
// pub nym_api_client: NymApiClient,
pub nyxd: NyxdClient<C, S>,
}
@@ -243,6 +245,50 @@ impl<C, S> Client<C, S> {
Ok(self.nym_api.get_gateways().await?)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_described_nodes(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut descriptions = Vec::new();
loop {
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_bonded_nym_nodes(
&self,
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut bonds = Vec::new();
loop {
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -418,6 +464,27 @@ impl NymApiClient {
Ok(descriptions)
}
pub async fn get_all_bonded_nym_nodes(
&self,
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut bonds = Vec::new();
loop {
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
pub async fn get_gateway_core_status_count(
&self,
identity: IdentityKeyRef<'_>,
@@ -39,7 +39,7 @@ use nym_contracts_common::IdentityKey;
pub use nym_http_api_client::Client;
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId};
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails};
use time::format_description::BorrowedFormatItem;
use time::Date;
@@ -139,6 +139,25 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_nym_nodes(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDetails>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(&[routes::API_VERSION, "nym-nodes", "bonded"], &params)
.await
}
async fn get_basic_mixnodes(
&self,
semver_compatibility: Option<String>,
+15
View File
@@ -12,6 +12,8 @@ pub use nym_explorer_api_requests::{
// Paths
const API_VERSION: &str = "v1";
const TMP: &str = "tmp";
const UNSTABLE: &str = "unstable";
const MIXNODES: &str = "mix-nodes";
const GATEWAYS: &str = "gateways";
@@ -53,6 +55,12 @@ impl ExplorerClient {
Ok(Self { client, url })
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result<Self, ExplorerApiError> {
let client = reqwest::Client::builder().timeout(timeout).build()?;
Ok(Self { client, url })
}
async fn send_get_request(
&self,
paths: &[&str],
@@ -86,6 +94,13 @@ impl ExplorerClient {
pub async fn get_gateways(&self) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
self.query_explorer_api(&[API_VERSION, GATEWAYS]).await
}
pub async fn unstable_get_gateways(
&self,
) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
self.query_explorer_api(&[API_VERSION, TMP, UNSTABLE, GATEWAYS])
.await
}
}
fn combine_url(mut base_url: Url, paths: &[&str]) -> Result<Url, ExplorerApiError> {
@@ -4,6 +4,7 @@
use crate::state::ExplorerApiStateContext;
use log::{info, warn};
use nym_explorer_api_requests::Location;
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_task::TaskClient;
pub(crate) struct GeoLocateTask {
@@ -25,6 +26,7 @@ impl GeoLocateTask {
_ = interval_timer.tick() => {
self.locate_mix_nodes().await;
self.locate_gateways().await;
self.locate_nym_nodes().await;
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
@@ -109,6 +111,83 @@ impl GeoLocateTask {
trace!("All mix nodes located");
}
async fn locate_nym_nodes(&mut self) {
// I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...`
// If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway
let nym_nodes = self.state.inner.nymnodes.get_bonded_nymnodes().await;
let geo_ip = self.state.inner.geo_ip.0.clone();
for (i, cache_item) in nym_nodes.values().enumerate() {
if self
.state
.inner
.nymnodes
.is_location_valid(cache_item.node_id())
.await
{
// when the cached location is valid, don't locate and continue to next mix node
continue;
}
let bonded_host = &cache_item.bond_information.node.host;
match geo_ip.query(
bonded_host,
Some(
cache_item
.bond_information
.node
.custom_http_port
.unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT),
),
) {
Ok(opt) => match opt {
Some(location) => {
let location: Location = location.into();
trace!(
"{} mix nodes already located. host {} is located in {:#?}",
i,
bonded_host,
location.three_letter_iso_country_code,
);
if i > 0 && (i % 100) == 0 {
info!("Located {} nym-nodes...", i + 1,);
}
self.state
.inner
.nymnodes
.set_location(cache_item.node_id(), Some(location))
.await;
// one node has been located, so return out of the loop
return;
}
None => {
warn!("❌ Location for {bonded_host} not found.");
self.state
.inner
.nymnodes
.set_location(cache_item.node_id(), None)
.await;
}
},
Err(_e) => {
// warn!(
// "❌ Oh no! Location for {} failed. Error: {:#?}",
// cache_item.mix_node().host,
// e
// );
}
};
}
trace!("All nym-nodes nodes located");
}
async fn locate_gateways(&mut self) {
let gateways = self.state.inner.gateways.get_gateways().await;
+2
View File
@@ -10,6 +10,7 @@ use crate::gateways::http::gateways_make_default_routes;
use crate::http::swagger::get_docs;
use crate::mix_node::http::mix_node_make_default_routes;
use crate::mix_nodes::http::mix_nodes_make_default_routes;
use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes;
use crate::overview::http::overview_make_default_routes;
use crate::ping::http::ping_make_default_routes;
use crate::service_providers::http::service_providers_make_default_routes;
@@ -58,6 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
"/ping" => ping_make_default_routes(&openapi_settings),
"/validators" => validators_make_default_routes(&openapi_settings),
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
"/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings),
};
building_rocket
+1
View File
@@ -22,6 +22,7 @@ mod http;
mod location;
mod mix_node;
pub(crate) mod mix_nodes;
mod nym_nodes;
mod overview;
mod ping;
pub(crate) mod service_providers;
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::state::ExplorerApiStateContext;
use nym_explorer_api_requests::PrettyDetailedGatewayBond;
use okapi::openapi3::OpenApi;
use rocket::serde::json::Json;
use rocket::{Route, State};
use rocket_okapi::settings::OpenApiSettings;
pub fn unstable_temp_nymnodes_make_default_routes(
settings: &OpenApiSettings,
) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: all_gateways]
}
#[openapi(tag = "UNSTABLE")]
#[get("/gateways")]
pub(crate) async fn all_gateways(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
let mut gateways = state.inner.gateways.get_detailed_gateways().await;
gateways.append(&mut state.inner.nymnodes.pretty_gateways().await);
Json(gateways)
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::NodeId;
use crate::location::LocationCache;
pub(crate) type NymNodeLocationCache = LocationCache<NodeId>;
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::time::Duration;
pub(crate) mod http;
pub(crate) mod location;
pub(crate) mod models;
pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(1200);
+154
View File
@@ -0,0 +1,154 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::location::{LocationCache, LocationCacheItem};
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::CACHE_ENTRY_TTL;
use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond};
use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails};
use nym_validator_client::models::NymNodeDescription;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{RwLock, RwLockReadGuard};
pub(crate) struct NymNodesCache {
pub(crate) valid_until: SystemTime,
pub(crate) bonded_nym_nodes: HashMap<NodeId, NymNodeDetails>,
pub(crate) described_nodes: HashMap<NodeId, NymNodeDescription>,
}
impl NymNodesCache {
fn new() -> Self {
NymNodesCache {
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
bonded_nym_nodes: Default::default(),
described_nodes: Default::default(),
}
}
// fn is_valid(&self) -> bool {
// self.valid_until >= SystemTime::now()
// }
}
#[derive(Clone)]
pub(crate) struct ThreadSafeNymNodesCache {
nymnodes: Arc<RwLock<NymNodesCache>>,
locations: Arc<RwLock<LocationCache<NodeId>>>,
}
impl ThreadSafeNymNodesCache {
pub(crate) fn new() -> Self {
ThreadSafeNymNodesCache {
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
locations: Arc::new(RwLock::new(NymNodeLocationCache::new())),
}
}
pub(crate) fn new_with_location_cache(locations: NymNodeLocationCache) -> Self {
ThreadSafeNymNodesCache {
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
locations: Arc::new(RwLock::new(locations)),
}
}
pub(crate) async fn is_location_valid(&self, node_id: NodeId) -> bool {
self.locations
.read()
.await
.get(&node_id)
.map_or(false, |cache_item| {
cache_item.valid_until > SystemTime::now()
})
}
pub(crate) async fn get_bonded_nymnodes(
&self,
) -> RwLockReadGuard<HashMap<NodeId, NymNodeDetails>> {
let guard = self.nymnodes.read().await;
RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes)
}
pub(crate) async fn get_locations(&self) -> NymNodeLocationCache {
self.locations.read().await.clone()
}
pub(crate) async fn set_location(&self, node_id: NodeId, location: Option<Location>) {
// cache the location for this mix node so that it can be used when the mix node list is refreshed
self.locations
.write()
.await
.insert(node_id, LocationCacheItem::new_from_location(location));
}
pub(crate) async fn update_cache(
&self,
all_bonds: Vec<NymNodeDetails>,
descriptions: Vec<NymNodeDescription>,
) {
let mut guard = self.nymnodes.write().await;
guard.bonded_nym_nodes = all_bonds
.into_iter()
.map(|details| (details.node_id(), details))
.collect();
guard.described_nodes = descriptions
.into_iter()
.map(|description| (description.node_id, description))
.collect();
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
}
pub(crate) async fn pretty_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
let nodes_guard = self.nymnodes.read().await;
let location_guard = self.locations.read().await;
let mut pretty_gateways = vec![];
for (node_id, native_nymnode) in &nodes_guard.bonded_nym_nodes {
let Some(description) = nodes_guard.described_nodes.get(node_id) else {
continue;
};
if description.description.declared_role.entry {
let location = location_guard.get(node_id);
let bond = &native_nymnode.bond_information;
pretty_gateways.push(PrettyDetailedGatewayBond {
pledge_amount: bond.original_pledge.clone(),
owner: bond.owner.clone(),
block_height: bond.bonding_height,
gateway: Gateway {
host: bond.node.host.clone(),
mix_port: description.description.mix_port(),
clients_port: description.description.mixnet_websockets.ws_port,
location: description
.description
.auxiliary_details
.location
.as_ref()
.map(|l| l.to_string())
.unwrap_or_default(),
sphinx_key: description
.description
.host_information
.keys
.x25519
.to_base58_string(),
identity_key: bond.node.identity_key.clone(),
version: description
.description
.build_information
.build_version
.clone(),
},
proxy: None,
location: location.and_then(|l| l.location.clone()),
})
}
}
pretty_gateways
}
}
+9
View File
@@ -18,6 +18,8 @@ use crate::gateways::models::ThreadsafeGatewayCache;
use crate::mix_node::models::ThreadsafeMixNodeCache;
use crate::mix_nodes::location::MixnodeLocationCache;
use crate::mix_nodes::models::ThreadsafeMixNodesCache;
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::models::ThreadSafeNymNodesCache;
use crate::ping::models::ThreadsafePingCache;
use crate::validators::models::ThreadsafeValidatorCache;
@@ -30,6 +32,7 @@ pub struct ExplorerApiState {
pub(crate) gateways: ThreadsafeGatewayCache,
pub(crate) mixnode: ThreadsafeMixNodeCache,
pub(crate) mixnodes: ThreadsafeMixNodesCache,
pub(crate) nymnodes: ThreadSafeNymNodesCache,
pub(crate) ping: ThreadsafePingCache,
pub(crate) validators: ThreadsafeValidatorCache,
pub(crate) geo_ip: ThreadsafeGeoIp,
@@ -49,6 +52,7 @@ pub struct ExplorerApiStateOnDisk {
pub(crate) country_node_distribution: CountryNodesDistribution,
pub(crate) mixnode_location_cache: MixnodeLocationCache,
pub(crate) gateway_location_cache: GatewayLocationCache,
pub(crate) nymnode_location_cache: NymNodeLocationCache,
pub(crate) as_at: DateTime<Utc>,
}
@@ -85,6 +89,9 @@ impl ExplorerApiStateContext {
mixnodes: ThreadsafeMixNodesCache::new_with_location_cache(
state.mixnode_location_cache,
),
nymnodes: ThreadSafeNymNodesCache::new_with_location_cache(
state.nymnode_location_cache,
),
ping: ThreadsafePingCache::new(),
validators: ThreadsafeValidatorCache::new(),
validator_client: ThreadsafeValidatorClient::new(),
@@ -101,6 +108,7 @@ impl ExplorerApiStateContext {
gateways: ThreadsafeGatewayCache::new(),
mixnode: ThreadsafeMixNodeCache::new(),
mixnodes: ThreadsafeMixNodesCache::new(),
nymnodes: ThreadSafeNymNodesCache::new(),
ping: ThreadsafePingCache::new(),
validators: ThreadsafeValidatorCache::new(),
validator_client: ThreadsafeValidatorClient::new(),
@@ -117,6 +125,7 @@ impl ExplorerApiStateContext {
country_node_distribution: self.inner.country_node_distribution.get_all().await,
mixnode_location_cache: self.inner.mixnodes.get_locations().await,
gateway_location_cache: self.inner.gateways.get_locations().await,
nymnode_location_cache: self.inner.nymnodes.get_locations().await,
as_at: Utc::now(),
};
serde_json::to_writer(file, &state).expect("error writing state to disk");
+54 -5
View File
@@ -1,16 +1,16 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::GatewayBond;
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails};
use nym_task::TaskClient;
use nym_validator_client::models::MixNodeBondAnnotated;
use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription};
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse};
use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError};
use std::future::Future;
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
use tokio::time::MissedTickBehavior;
pub(crate) struct ExplorerApiTasks {
state: ExplorerApiStateContext,
@@ -39,6 +39,28 @@ impl ExplorerApiTasks {
bonds
}
async fn retrieve_bonded_nymnodes(&self) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
info!("About to retrieve all nymnode bonds...");
self.state
.inner
.validator_client
.0
.get_all_cached_bonded_nym_nodes()
.await
}
async fn retrieve_node_descriptions(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
info!("About to retrieve node descriptions...");
self.state
.inner
.validator_client
.0
.get_all_cached_described_nodes()
.await
}
async fn retrieve_all_mixnodes(&self) -> Vec<MixNodeBondAnnotated> {
info!("About to retrieve all mixnode bonds...");
self.retrieve_mixnodes(
@@ -130,10 +152,33 @@ impl ExplorerApiTasks {
}
}
async fn update_nymnodes_cache(&self) {
let nym_node_bonds = self.retrieve_bonded_nymnodes().await.unwrap_or_else(|err| {
error!("failed to retrieve nym node bonds: {err}");
Vec::new()
});
let all_descriptions = self
.retrieve_node_descriptions()
.await
.unwrap_or_else(|err| {
error!("failed to retrieve node descriptions: {err}");
Vec::new()
});
self.state
.inner
.nymnodes
.update_cache(nym_node_bonds, all_descriptions)
.await
}
pub(crate) fn start(mut self) {
info!("Spawning mix nodes task runner...");
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE);
interval_timer.set_missed_tick_behavior(MissedTickBehavior::Skip);
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = interval_timer.tick() => {
@@ -147,6 +192,10 @@ impl ExplorerApiTasks {
info!("Updating mix node cache...");
self.update_mixnode_cache().await;
info!("Updating nymnode cache...");
self.update_nymnodes_cache().await;
info!("Done");
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");